import prisma from '@/lib/prisma' import { MARKETING_CONSENT_VERSION, marketingConsentText } from '@/lib/marketing/consent-copy' import { normalizeMarketingEmail } from '@/lib/marketing/normalize-email' export const MARKETING_BLOCKING_REASONS = ['COMPLAINT', 'HARD_BOUNCE', 'INVALID'] as const export type MarketingSource = | 'settings' | 'register' | 'unsubscribe' | 'unsubscribe-one-click' | 'complaint' | 'bounce' export type MarketingPreferenceView = { optedIn: boolean emailNormalized: string consentVersion: string | null updatedAt: Date | null blocked: boolean blockReason: string | null } type RequestMeta = { ip?: string | null userAgent?: string | null } export async function getMarketingPreferenceView(userId: string): Promise { const user = await prisma.user.findUnique({ where: { id: userId }, select: { email: true, marketingPreference: true, }, }) const emailNormalized = normalizeMarketingEmail(user?.email || '') const suppression = emailNormalized ? await prisma.emailSuppression.findUnique({ where: { emailNormalized } }) : null const blocked = Boolean( suppression && MARKETING_BLOCKING_REASONS.includes(suppression.reason as (typeof MARKETING_BLOCKING_REASONS)[number]), ) return { optedIn: user?.marketingPreference?.optedIn === true && !suppression, emailNormalized, consentVersion: user?.marketingPreference?.consentVersion ?? null, updatedAt: user?.marketingPreference?.updatedAt ?? null, blocked, blockReason: blocked ? suppression?.reason ?? null : null, } } export async function canReceiveMarketing(opts: { userId?: string | null email: string requireVerified?: boolean }): Promise<{ allowed: boolean; reason: string }> { const emailNormalized = normalizeMarketingEmail(opts.email) if (!emailNormalized) return { allowed: false, reason: 'invalid_email' } const suppression = await prisma.emailSuppression.findUnique({ where: { emailNormalized } }) if (suppression) return { allowed: false, reason: `suppressed:${suppression.reason}` } const user = opts.userId ? await prisma.user.findUnique({ where: { id: opts.userId }, select: { emailVerified: true, marketingPreference: { select: { optedIn: true, emailNormalized: true } }, }, }) : await prisma.user.findUnique({ where: { email: emailNormalized }, select: { emailVerified: true, marketingPreference: { select: { optedIn: true, emailNormalized: true } }, }, }) if (!user) return { allowed: false, reason: 'no_account' } if (opts.requireVerified !== false && !user.emailVerified) { return { allowed: false, reason: 'unverified' } } if (!user.marketingPreference?.optedIn) return { allowed: false, reason: 'not_opted_in' } return { allowed: true, reason: 'ok' } } export async function setMarketingOptIn(opts: { userId: string optedIn: boolean source: MarketingSource language?: string | null meta?: RequestMeta }): Promise<{ ok: true } | { ok: false; error: 'blocked' | 'missing_user' }> { const user = await prisma.user.findUnique({ where: { id: opts.userId }, select: { id: true, email: true }, }) if (!user) return { ok: false, error: 'missing_user' } const emailNormalized = normalizeMarketingEmail(user.email) const existingSuppression = await prisma.emailSuppression.findUnique({ where: { emailNormalized }, }) if ( opts.optedIn && existingSuppression && MARKETING_BLOCKING_REASONS.includes(existingSuppression.reason as (typeof MARKETING_BLOCKING_REASONS)[number]) ) { return { ok: false, error: 'blocked' } } const consentText = opts.optedIn ? marketingConsentText(opts.language) : null const consentVersion = opts.optedIn ? MARKETING_CONSENT_VERSION : null const preference = await prisma.marketingPreference.upsert({ where: { userId: user.id }, create: { userId: user.id, emailNormalized, optedIn: opts.optedIn, preferredLanguage: opts.language || null, consentVersion, consentText, source: opts.source, }, update: { emailNormalized, optedIn: opts.optedIn, preferredLanguage: opts.language || undefined, consentVersion: opts.optedIn ? consentVersion : undefined, consentText: opts.optedIn ? consentText : undefined, source: opts.source, }, }) await prisma.marketingPreferenceEvent.create({ data: { preferenceId: preference.id, userId: user.id, emailNormalized, optedIn: opts.optedIn, source: opts.source, consentVersion, consentText, ip: opts.meta?.ip || null, userAgent: opts.meta?.userAgent || null, }, }) if (opts.optedIn) { if (existingSuppression?.reason === 'UNSUBSCRIBE') { await prisma.emailSuppression.delete({ where: { emailNormalized } }) } } else { await prisma.emailSuppression.upsert({ where: { emailNormalized }, create: { emailNormalized, reason: 'UNSUBSCRIBE', source: opts.source, userId: user.id, }, update: { reason: existingSuppression && MARKETING_BLOCKING_REASONS.includes(existingSuppression.reason as (typeof MARKETING_BLOCKING_REASONS)[number]) ? existingSuppression.reason : 'UNSUBSCRIBE', source: opts.source, userId: user.id, }, }) } await prisma.auditLog.create({ data: { userId: user.id, action: opts.optedIn ? 'marketing.opt_in' : 'marketing.opt_out', resource: 'MarketingPreference', metadata: { source: opts.source, consentVersion }, ip: opts.meta?.ip || null, userAgent: opts.meta?.userAgent || null, }, }).catch(() => {}) return { ok: true } } export async function unsubscribeByUserId(opts: { userId: string source: MarketingSource meta?: RequestMeta }): Promise<{ ok: true; already: boolean } | { ok: false }> { const view = await getMarketingPreferenceView(opts.userId) if (!view.emailNormalized) return { ok: false } const already = !view.optedIn const result = await setMarketingOptIn({ userId: opts.userId, optedIn: false, source: opts.source, meta: opts.meta, }) if (!result.ok) return { ok: false } return { ok: true, already } }