Closer/server/src/services/entitlement.ts

55 lines
1.7 KiB
TypeScript
Raw Normal View History

import { db } from '../config/firebase'
import { RevenueCatEvent } from '../types'
const PREMIUM_ACTIVE_TYPES = new Set([
'INITIAL_PURCHASE',
'RENEWAL',
'PRODUCT_CHANGE',
'TRANSFER',
'UNCANCELLATION',
])
const PREMIUM_REVOKED_TYPES = new Set([
'EXPIRATION',
'CANCELLATION',
'SUBSCRIBER_ALIAS',
])
const PREMIUM_ENTITLEMENT_ID = 'closer_premium'
export async function syncEntitlement(event: RevenueCatEvent): Promise<void> {
const { type, app_user_id: uid, id: eventId } = event.event
// Check idempotency - skip if we've already processed this event
const eventRef = db().collection('entitlement_events').doc(eventId)
const eventDoc = await eventRef.get()
if (eventDoc.exists) {
console.log(`[entitlement] skipping duplicate event: ${eventId}`)
return
}
// Store event for idempotency (with 7-day TTL via Firestore rule or scheduled cleanup)
await eventRef.set({ processedAt: new Date() })
// Verify entitlement_id matches expected premium entitlement
const entitlementId = event.event.entitlement?.identifier || event.event.entitlement_id
if (entitlementId !== PREMIUM_ENTITLEMENT_ID) {
console.log(`[entitlement] ignored event for wrong entitlement: ${entitlementId} (expected: ${PREMIUM_ENTITLEMENT_ID})`)
return
}
if (PREMIUM_ACTIVE_TYPES.has(type)) {
await db().collection('users').doc(uid).update({ hasPremium: true })
console.log(`[entitlement] hasPremium=true for ${uid} (${type})`)
return
}
if (PREMIUM_REVOKED_TYPES.has(type)) {
await db().collection('users').doc(uid).update({ hasPremium: false })
console.log(`[entitlement] hasPremium=false for ${uid} (${type})`)
return
}
console.log(`[entitlement] ignored event type: ${type}`)
}