Add consented social milestone posting

This commit is contained in:
2026-08-14 09:03:10 +02:00
parent 14c429ff30
commit f7d82aa5bd
14 changed files with 613 additions and 3 deletions

View File

@@ -0,0 +1,69 @@
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<string, string> = {
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 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<string, string> = { 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 normalizeXHandle(value: string): string | null {
const handle = value.trim().replace(/^@/, '');
return /^[A-Za-z0-9_]{1,15}$/.test(handle) ? handle : null;
}