Monorepo skeleton, Prisma schema, core domain and services with tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zabudko-ruandClaude Opus 5.5 committed 2026-10-17 19:45:51 +04:00
commit 45d7512d2b
64 files changed
+9483

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
import { isLocale, type Locale } from './types.ts';
/**
* Payload для t.me/<bot>?start=<payload> (LND-3, ONB-1).
* Telegram ограничивает его 64 символами из [A-Za-z0-9_-], поэтому формат компактный:
*
* l-ru_a-AGENCY1 язык и код агентства
* s-k3j9x2 короткий токен; язык, агентство и UTM лежат в БД (StartToken)
*
* Части разделены "_", ключ и значение — "-". Неизвестные части игнорируются.
*/
export interface StartPayload {
locale?: Locale;
agency?: string;
token?: string;
}
export const START_PAYLOAD_MAX = 64;
const SAFE = /^[A-Za-z0-9]+$/;
export function normalizeAgencyCode(code: string): string | null {
const c = code.trim().toUpperCase();
return SAFE.test(c) && c.length <= 32 ? c : null;
}
export function encodeStartPayload(p: StartPayload): string {
const parts: string[] = [];
if (p.token) {
if (!SAFE.test(p.token)) throw new Error('Bad token');
parts.push(`s-${p.token}`);
}
if (p.locale) parts.push(`l-${p.locale}`);
if (p.agency) {
const a = normalizeAgencyCode(p.agency);
if (a) parts.push(`a-${a}`);
}
const out = parts.join('_');
if (out.length > START_PAYLOAD_MAX) throw new Error('Start payload too long');
return out;
}
export function decodeStartPayload(raw: string | undefined | null): StartPayload {
const out: StartPayload = {};
if (!raw) return out;
for (const part of raw.slice(0, START_PAYLOAD_MAX).split('_')) {
const [key, value] = [part.slice(0, 1), part.slice(2)];
if (part[1] !== '-' || !value) continue;
if (key === 'l' && isLocale(value)) out.locale = value;
else if (key === 'a') {
const a = normalizeAgencyCode(value);
if (a) out.agency = a;
} else if (key === 's' && SAFE.test(value)) out.token = value;
}
return out;
}
export const UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'] as const;
export type Utm = Partial<Record<(typeof UTM_KEYS)[number], string>>;
export function pickUtm(params: URLSearchParams): Utm {
const utm: Utm = {};
for (const k of UTM_KEYS) {
const v = params.get(k);
if (v) utm[k] = v.slice(0, 120);
}
return utm;
}
export function botLink(username: string, payload: string): string {
return `https://t.me/${username}${payload ? `?start=${payload}` : ''}`;
}