import crypto from 'node:crypto'; export const PUSH_PROVIDERS = { FCM: 'FCM', APNS: 'APNS', WEB_PUSH: 'WEB_PUSH', }; export const PUSH_PLATFORMS = { IOS: 'IOS', ANDROID: 'ANDROID', WEB: 'WEB', }; export function hashPushToken(pushToken) { return crypto.createHash('sha256').update(`${pushToken || ''}`).digest('hex'); } export async function registerPushToken(client, { tenantId, userId, staffId = null, businessMembershipId = null, vendorMembershipId = null, provider = PUSH_PROVIDERS.FCM, platform, pushToken, deviceId = null, appVersion = null, appBuild = null, locale = null, timezone = null, notificationsEnabled = true, metadata = {}, }) { const tokenHash = hashPushToken(pushToken); const result = await client.query( ` INSERT INTO device_push_tokens ( tenant_id, user_id, staff_id, business_membership_id, vendor_membership_id, provider, platform, push_token, token_hash, device_id, app_version, app_build, locale, timezone, notifications_enabled, invalidated_at, invalidation_reason, last_registered_at, last_seen_at, metadata ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NULL, NULL, NOW(), NOW(), $16::jsonb ) ON CONFLICT (provider, token_hash) DO UPDATE SET tenant_id = EXCLUDED.tenant_id, user_id = EXCLUDED.user_id, staff_id = EXCLUDED.staff_id, business_membership_id = EXCLUDED.business_membership_id, vendor_membership_id = EXCLUDED.vendor_membership_id, platform = EXCLUDED.platform, push_token = EXCLUDED.push_token, device_id = EXCLUDED.device_id, app_version = EXCLUDED.app_version, app_build = EXCLUDED.app_build, locale = EXCLUDED.locale, timezone = EXCLUDED.timezone, notifications_enabled = EXCLUDED.notifications_enabled, invalidated_at = NULL, invalidation_reason = NULL, last_registered_at = NOW(), last_seen_at = NOW(), metadata = COALESCE(device_push_tokens.metadata, '{}'::jsonb) || EXCLUDED.metadata, updated_at = NOW() RETURNING id, tenant_id AS "tenantId", user_id AS "userId", staff_id AS "staffId", business_membership_id AS "businessMembershipId", vendor_membership_id AS "vendorMembershipId", provider, platform, device_id AS "deviceId", notifications_enabled AS "notificationsEnabled" `, [ tenantId, userId, staffId, businessMembershipId, vendorMembershipId, provider, platform, pushToken, tokenHash, deviceId, appVersion, appBuild, locale, timezone, notificationsEnabled, JSON.stringify(metadata || {}), ] ); return result.rows[0]; } export async function unregisterPushToken(client, { tenantId, userId, tokenId = null, pushToken = null, reason = 'USER_REQUESTED', }) { const tokenHash = pushToken ? hashPushToken(pushToken) : null; const result = await client.query( ` UPDATE device_push_tokens SET notifications_enabled = FALSE, invalidated_at = NOW(), invalidation_reason = $4, updated_at = NOW() WHERE tenant_id = $1 AND user_id = $2 AND ( ($3::uuid IS NOT NULL AND id = $3::uuid) OR ($5::text IS NOT NULL AND token_hash = $5::text) ) RETURNING id, provider, platform, device_id AS "deviceId" `, [tenantId, userId, tokenId, reason, tokenHash] ); return result.rows; } export async function resolveNotificationTargetTokens(client, notification) { const result = await client.query( ` WITH recipient_users AS ( SELECT $2::text AS user_id WHERE $2::text IS NOT NULL UNION SELECT bm.user_id FROM business_memberships bm WHERE $3::uuid IS NOT NULL AND bm.id = $3::uuid UNION SELECT s.user_id FROM staffs s WHERE $4::uuid IS NOT NULL AND s.id = $4::uuid ) SELECT dpt.id, dpt.user_id AS "userId", dpt.staff_id AS "staffId", dpt.provider, dpt.platform, dpt.push_token AS "pushToken", dpt.device_id AS "deviceId", dpt.metadata FROM device_push_tokens dpt JOIN recipient_users ru ON ru.user_id = dpt.user_id WHERE dpt.tenant_id = $1 AND dpt.notifications_enabled = TRUE AND dpt.invalidated_at IS NULL ORDER BY dpt.last_seen_at DESC, dpt.created_at DESC `, [ notification.tenant_id, notification.recipient_user_id, notification.recipient_business_membership_id, notification.recipient_staff_id, ] ); return result.rows; } export async function markPushTokenInvalid(client, tokenId, reason) { await client.query( ` UPDATE device_push_tokens SET notifications_enabled = FALSE, invalidated_at = NOW(), invalidation_reason = $2, updated_at = NOW() WHERE id = $1 `, [tokenId, reason] ); } export async function touchPushTokenDelivery(client, tokenId) { await client.query( ` UPDATE device_push_tokens SET last_delivery_at = NOW(), last_seen_at = NOW(), updated_at = NOW() WHERE id = $1 `, [tokenId] ); }