Les comptes déjà créés ne sont pas inscrits. Le choix est facultatif à l’inscription et dans les paramètres. Un lien public demande confirmation avant d’arrêter les actualités, sans toucher aux messages de compte.
206 lines
6.2 KiB
TypeScript
206 lines
6.2 KiB
TypeScript
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<MarketingPreferenceView> {
|
|
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 }
|
|
}
|