# @mostajs/ticketing — fiche LLM > Module de billetterie : tickets, scan QR/code-barres, reconnaissance faciale et gestion de quotas. - Version: 3.0.4 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI - Chemin: mostajs/mosta-ticketing · Statut audit: complet (dist/) ## RÔLE Module métier complet de billetterie pour centre/club : génération de tickets (numéros Wiegand 24-bit), pipeline de validation de scan en 8 étapes (pur, sans couplage framework), 4 modes de validité, reconnaissance faciale (détection + matching via face-api.js), composants React (scanner, caméra), hooks, handlers de routes Next.js et intégration au socle `@mostajs/socle` (schémas, permissions, menu). Version 3.x = fusion scan + face. ## INSTALLATION npm i @mostajs/ticketing ## EXPORTS Entrée `.` (client + logique pure) : - Schémas: `TicketSchema`, `ScanLogSchema`, `CounterSchema` - Scan: `processScan` (+ type `ScanDeps`) - Validité: `computeValidUntil`, `isExpired`, `isToday` - Quota: `decrementQuota`, `isQuotaDepleted`, `wouldExceedQuota` - Numéro de ticket: `getDayOfYear`, `computeCombinedDay`, `extractCombinedDayFromTicket`, `extractDayFromTicket`, `extractYearFromTicket`, `extractSequenceFromTicket`, `isTicketFromToday`, `isValidTicketFormat` ; constantes `DAY_MULTIPLIER`, `DAY_MODULO`, `YEAR_MODULO`, `MAX_SEQUENCE`, `MAX_TICKET_VALUE` - Permissions: `TICKETING_PERMISSIONS`, `TICKETING_PERMISSION_DEFINITIONS`, `TICKETING_CATEGORY_DEFINITIONS` - Menu: `ticketingMenuContribution` - Composants React: `ScannerView`, `ScanResultCard`, `ScanEmptyState`, `FaceDetector` - Hooks React: `useScan`, `useCamera`, `useFaceDetection` - Audio: `playBeep`, `playGranted`, `playDenied` - Face: `loadModels`, `isLoaded`, `detectFace`, `detectAllFaces`, `extractDescriptor`, `compareFaces`, `findMatch`, `findAllMatches`, `descriptorToArray`, `arrayToDescriptor`, `isValidDescriptor`, `drawDetection` - Constante: `DEFAULT_FACE_SETTINGS` - Types: `CodeFormat`, `ValidityMode`, `TicketStatus`, `ScanResult`, `ScanMethod`, `DenyReason`, `ScanInput`, `ScanOutput`, `ScanGrantedResult`, `ScanDeniedResult`, `TicketInfo`, `ClientInfo`, `AccessInfo`, `CreateTicketInput`, `ScanHandlerConfig`, `TicketsHandlerConfig`, `ScanResultData`, `MostaFaceConfig`, `FaceDetectionResult`, `FaceMatchResult`, `FaceDescriptor`, `FaceSettings`, etc. ## EXPORTS PAR SOUS-CHEMIN - `@mostajs/ticketing/server` — repos `TicketRepository`, `ScanLogRepository` (+ `TicketDTO`, `ScanLogDTO`) ; handlers `createScanHandler`, `createTicketsHandler`, `createDetectHandler`, `createRecognizeHandler` (+ `DetectHandlerConfig`, `RecognizeHandlerConfig`, `FaceCandidate`) - `@mostajs/ticketing/register` — `register(registry)` : enregistre le module dans le socle - `@mostajs/ticketing/types` — tous les types - `@mostajs/ticketing/lib/permissions`, `/lib/menu`, `/lib/audio`, `/lib/face-api`, `/lib/face-matcher`, `/lib/face-utils` - `@mostajs/ticketing/pages/TicketsPage`, `/pages/ScanPage` — pages React prêtes à l'emploi - `@mostajs/ticketing/components/ScannerView`, `/components/ScanResultCard`, `/components/FaceDetector` - `@mostajs/ticketing/hooks/useScan`, `/hooks/useCamera`, `/hooks/useFaceDetection` - `@mostajs/ticketing/api/detect.route`, `/api/recognize.route` - `@mostajs/ticketing/i18n/*` — fichiers de traduction ## API — SIGNATURES - `processScan(code: string, scanMethod: ScanMethod, scannedBy: string, deps: ScanDeps): Promise` — pipeline pur, toute I/O via `deps` - `computeValidUntil(mode: ValidityMode, durationMinutes: number|null): Date | null` - `decrementQuota(remainingQuota: number|null): QuotaUpdate | null` · `wouldExceedQuota(totalQuota: number|null, currentCount: number): boolean` - `getNextTicketNumber(date?: Date): Promise` (méthode `TicketRepository`) - `createScanHandler(config: ScanHandlerConfig): { POST }` - `createTicketsHandler(config: TicketsHandlerConfig): { GET; POST }` - `createRecognizeHandler(config: RecognizeHandlerConfig): { POST }` · `createDetectHandler(config?): { POST }` - `loadModels(config?: MostaFaceConfig): Promise` · `extractDescriptor(input, config?): Promise` - `compareFaces(d1, d2): number` · `findMatch(descriptor, candidates, threshold?): FaceMatchResult|null` - `useScan(options?: UseScanOptions): UseScanReturn` · `useCamera(options?)` · `useFaceDetection(videoRef, options?)` Repositories (étendent `BaseRepository`) : - `TicketRepository`: `getNextTicketNumber`, `createWithAutoFields`, `findByCode`, `findByClient`, `countByAccess`, `countsByAccess`, `markUsed` - `ScanLogRepository`: `findRecent`, `findByClient`, `countToday`, `countGrantedToday`, `findDistinctClientsToday`, `wasScannedToday` ## TYPES CLÉS - `CodeFormat = 'qrcode'|'code128'|'code39'|'ean13'|'ean8'|'upc_a'|'itf'|'pdf417'|'datamatrix'|'aztec'` - `ValidityMode = 'day_reentry'|'single_use'|'time_slot'|'unlimited'` - `TicketStatus = 'active'|'used'|'expired'|'cancelled'` - `ScanMethod = 'webcam'|'pwa_camera'|'handheld_scanner'|'nfc'` - `DenyReason = 'invalid_ticket'|'ticket_already_used'|'ticket_expired'|'ticket_cancelled'|'quota_depleted'|'access_expired'|'client_suspended'` - `ScanOutput = ScanGrantedResult | ScanDeniedResult` - `ScanGrantedResult { result: 'granted'; isReentry: boolean; ticket: TicketInfo; client: ClientInfo; access: AccessInfo }` - `ScanDeniedResult { result: 'denied'; reason: DenyReason; ticket?: TicketInfo }` - `ScanDeps` : callbacks injectés — `findTicketByCode`, `findAccessById`, `findClientById`, `wasScannedToday`, `updateTicket`, `updateAccess`, `createScanLog`, `resolveId`, `formatClientName` - `ScanHandlerConfig { getRepositories(); checkAuth(req); onGranted?; onDenied? }` - `TicketsHandlerConfig { getRepositories(); checkAuth(req, permission); onCreated?; defaultCodeFormat? }` - `CreateTicketInput { clientId; activityId; ticketType?; sourceClientId?; amount?; codeFormat? }` - `MostaFaceConfig { modelsPath?; scoreThreshold?; inputSize?; matchThreshold? }` — defaults `/models/face-api`, 0.5, 320, 0.6 - `FaceSettings { faceRecognitionEnabled; faceRecognitionThreshold; faceRequireForCapture; faceAutoVerify }` (cf. `DEFAULT_FACE_SETTINGS`) - `FaceDescriptor = Float32Array | number[]` - `TicketingPermission` : codes `ticket:create`, `ticket:view`, `scan:validate`, `scan:view_history` ## PATTERN ```ts // Route de scan Next.js — l'app injecte ses repos + auth import { createScanHandler } from '@mostajs/ticketing/server' export const { POST } = createScanHandler({ getRepositories: async () => ({ ticketRepo, clientAccessRepo, scanLogRepo, clientRepo }), checkAuth: async req => ({ error: null, userId: 'u-1' }), onGranted: async d => { /* audit */ }, }) // Pipeline de scan pur (sans framework) import { processScan } from '@mostajs/ticketing' const out = await processScan(code, 'webcam', userId, deps /* ScanDeps */) if (out.result === 'granted') { /* out.ticket, out.access */ } // Numéro de ticket Wiegand import { TicketRepository } from '@mostajs/ticketing/server' const num = await new TicketRepository(dialect).getNextTicketNumber() ``` ## DÉPEND DE - `@mostajs/orm` (dependency `^1.4.12`) — `BaseRepository`, `IDialect`, `QueryOptions` (repos importent l'ORM directement). - `@mostajs/octoswitcher` (dependency) — switch interne. - `@vladmandic/face-api` (dependency) — détection/reconnaissance faciale. - `@mostajs/socle` (peer dep `>=2.0.0`) — `ModuleRegistration`, cible de `register()`. - `@mostajs/subscriptions` (peer dep optionnelle `>=1.0.0`) — fournit ClientAccess/Activity. - Peer deps optionnelles : `html5-qrcode` (requis pour `useScan`/`ScannerView`), `lucide-react`, `next`, `react`. ## PIÈGES - `processScan` est une fonction PURE : toute I/O passe par l'objet `deps` (ScanDeps) que l'app fournit — aucune connexion DB interne. - Pipeline de scan en 8 étapes : trouver ticket → statut → expiry → access client → statut client → mode de validité → MAJ ticket+access+log → résultat. - Numéros Wiegand 24-bit : `Ticket = ((Année%4)*32 + (JourAnnée%32)) * 131072 + Séquence` ; bornes 24-bit (`MAX_TICKET_VALUE = 16777215`). `isTicketFromToday` sert d'anti-replay. - `useScan`/`ScannerView` exigent `html5-qrcode` (peer dep optionnelle) et un élément DOM `id="qr-reader"` présent dans le composant. - Reconnaissance faciale : `loadModels()` charge 3 modèles face-api.js depuis `modelsPath` (default `/models/face-api`) — la détection tourne côté client ; `/api/face/detect` est un placeholder de gating. - `createRecognizeHandler` reçoit un descripteur de 128 floats ; `getCandidates` doit retourner les `faceDescriptor` ; `publicFields` filtre la sortie (par défaut tout sauf `faceDescriptor`). - Stocker les descripteurs en DB via `descriptorToArray` (Float32Array → number[]) et relire via `arrayToDescriptor`. - Seuil de matching : plus bas = plus strict (default 0.6) ; `compareFaces` retourne une distance euclidienne. - Ne pas importer `/server` côté client (logique ORM + auth). ## RÉFÉRENCES README.md · wire.json · ticketing.wire.json · i18n/