export const DEFAULT_SOCIAL_MILESTONE_THRESHOLDS = [1000, 10000] as const; export type SocialMilestoneKind = `unique_scans_${number}`; /** * Staging can set SOCIAL_MILESTONE_THRESHOLDS=1 (or e.g. 1,2) so the complete * flow is testable without fabricating thousands of scans. Production keeps * the conservative defaults unless its environment explicitly changes them. */ export function getSocialMilestoneThresholds(): number[] { const configured = process.env.SOCIAL_MILESTONE_THRESHOLDS; if (!configured) return [...DEFAULT_SOCIAL_MILESTONE_THRESHOLDS]; const thresholds = Array.from(new Set( configured.split(',') .map(value => Number(value.trim())) .filter(value => Number.isInteger(value) && value > 0 && value <= 1_000_000) )).sort((a, b) => a - b); return thresholds.length ? thresholds : [...DEFAULT_SOCIAL_MILESTONE_THRESHOLDS]; } const useCaseLabels: Record = { menu_pdf: 'menu QR code', marketing_campaign: 'campaign QR code', vcard: 'digital business-card QR code', event: 'event QR code', feedback: 'feedback QR code', }; export function milestoneKind(threshold: number): SocialMilestoneKind { return `unique_scans_${threshold}` as SocialMilestoneKind; } export function milestoneThreshold(kind: string): number | null { const result = /^unique_scans_(\d+)$/.exec(kind); return result ? Number(result[1]) : null; } export type SocialLocale = 'en' | 'de'; export type SocialMilestoneCard = { version: 'milestone-card-v2'; language: SocialLocale; qrTitle: string; label: string; title: string; totalScans: number; totalUniqueScans: number; milestoneThreshold: number; reachedAt: string; trend: { points: Array<{ at: string; total: number }>; startLabel: string; endLabel: string; target: number; ceiling: number; } | null; }; export function socialLocale(value?: string | null): SocialLocale { return value === 'de' ? 'de' : 'en'; } export function usageLabel(primaryUseCase: string | null, locale: SocialLocale = 'en'): string { if (locale === 'de') { const german: Record = { menu_pdf: 'Speisekarten-QR-Code', marketing_campaign: 'Kampagnen-QR-Code', vcard: 'Visitenkarten-QR-Code', event: 'Event-QR-Code', feedback: 'Feedback-QR-Code' }; return (primaryUseCase && german[primaryUseCase]) || 'QR-Code'; } return (primaryUseCase && useCaseLabels[primaryUseCase]) || 'QR code'; } export function buildMilestonePost(primaryUseCase: string | null, threshold: number, xHandle?: string | null, locale: SocialLocale = 'en'): string { const count = threshold.toLocaleString(locale === 'de' ? 'de-DE' : 'en-US'); const base = locale === 'de' ? `Ein ${usageLabel(primaryUseCase, locale)} hat gerade ${count} eindeutige Scans erreicht. 🎉` : `A ${usageLabel(primaryUseCase, locale)} just reached ${count} unique scans. 🎉`; return xHandle ? `${base} By @${xHandle.replace(/^@/, '')}.` : base; } export function buildMilestoneCard(primaryUseCase: string | null, threshold: number, locale: SocialLocale) { return { version: 'milestone-card-v1', language: locale, threshold, label: usageLabel(primaryUseCase, locale), title: locale === 'de' ? 'Erfolgsmeilenstein' : 'Success milestone' }; } export function buildMilestonePostForQr(primaryUseCase: string | null, totalUniqueScans: number, xHandle: string | null | undefined, locale: SocialLocale, qrTitle: string): string { const count = totalUniqueScans.toLocaleString(locale === 'de' ? 'de-DE' : 'en-US'); const subject = qrTitle.trim() || usageLabel(primaryUseCase, locale); const base = locale === 'de' ? `„${subject}“ hat ${count} eindeutige Scans erreicht.` : `“${subject}” reached ${count} unique scans.`; return xHandle ? `${base} By @${xHandle.replace(/^@/, '')}.` : base; } export function buildMilestoneCardSnapshot(input: { primaryUseCase: string | null; qrTitle: string; totalScans: number; totalUniqueScans: number; milestoneThreshold: number; reachedAt: Date; trend: SocialMilestoneCard['trend']; locale: SocialLocale; }): SocialMilestoneCard { return { version: 'milestone-card-v2', language: input.locale, qrTitle: input.qrTitle, label: usageLabel(input.primaryUseCase, input.locale), title: input.locale === 'de' ? 'Erfolgsmeilenstein' : 'Success milestone', totalScans: input.totalScans, totalUniqueScans: input.totalUniqueScans, milestoneThreshold: input.milestoneThreshold, reachedAt: input.reachedAt.toISOString(), trend: input.trend, }; } export function normalizeXHandle(value: string): string | null { const handle = value.trim().replace(/^@/, ''); return /^[A-Za-z0-9_]{1,15}$/.test(handle) ? handle : null; }