@mostajs/ticketing 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +479 -0
- package/dist/api/scan.route.d.ts +16 -0
- package/dist/api/scan.route.d.ts.map +1 -0
- package/dist/api/scan.route.js +75 -0
- package/dist/api/scan.route.js.map +1 -0
- package/dist/api/tickets.route.d.ts +15 -0
- package/dist/api/tickets.route.d.ts.map +1 -0
- package/dist/api/tickets.route.js +91 -0
- package/dist/api/tickets.route.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/quota-manager.d.ts +18 -0
- package/dist/lib/quota-manager.d.ts.map +1 -0
- package/dist/lib/quota-manager.js +31 -0
- package/dist/lib/quota-manager.js.map +1 -0
- package/dist/lib/scan-processor.d.ts +39 -0
- package/dist/lib/scan-processor.d.ts.map +1 -0
- package/dist/lib/scan-processor.js +168 -0
- package/dist/lib/scan-processor.js.map +1 -0
- package/dist/lib/validity-checker.d.ts +19 -0
- package/dist/lib/validity-checker.d.ts.map +1 -0
- package/dist/lib/validity-checker.js +45 -0
- package/dist/lib/validity-checker.js.map +1 -0
- package/dist/repositories/activity.repository.d.ts +38 -0
- package/dist/repositories/activity.repository.d.ts.map +1 -0
- package/dist/repositories/activity.repository.js +23 -0
- package/dist/repositories/activity.repository.js.map +1 -0
- package/dist/repositories/client-access.repository.d.ts +31 -0
- package/dist/repositories/client-access.repository.d.ts.map +1 -0
- package/dist/repositories/client-access.repository.js +31 -0
- package/dist/repositories/client-access.repository.js.map +1 -0
- package/dist/repositories/index.d.ts +11 -0
- package/dist/repositories/index.d.ts.map +1 -0
- package/dist/repositories/index.js +8 -0
- package/dist/repositories/index.js.map +1 -0
- package/dist/repositories/scan-log.repository.d.ts +32 -0
- package/dist/repositories/scan-log.repository.d.ts.map +1 -0
- package/dist/repositories/scan-log.repository.js +48 -0
- package/dist/repositories/scan-log.repository.js.map +1 -0
- package/dist/repositories/subscription-plan.repository.d.ts +27 -0
- package/dist/repositories/subscription-plan.repository.d.ts.map +1 -0
- package/dist/repositories/subscription-plan.repository.js +19 -0
- package/dist/repositories/subscription-plan.repository.js.map +1 -0
- package/dist/repositories/ticket.repository.d.ts +48 -0
- package/dist/repositories/ticket.repository.d.ts.map +1 -0
- package/dist/repositories/ticket.repository.js +65 -0
- package/dist/repositories/ticket.repository.js.map +1 -0
- package/dist/schemas/activity.schema.d.ts +3 -0
- package/dist/schemas/activity.schema.d.ts.map +1 -0
- package/dist/schemas/activity.schema.js +39 -0
- package/dist/schemas/activity.schema.js.map +1 -0
- package/dist/schemas/client-access.schema.d.ts +3 -0
- package/dist/schemas/client-access.schema.d.ts.map +1 -0
- package/dist/schemas/client-access.schema.js +25 -0
- package/dist/schemas/client-access.schema.js.map +1 -0
- package/dist/schemas/counter.schema.d.ts +3 -0
- package/dist/schemas/counter.schema.d.ts.map +1 -0
- package/dist/schemas/counter.schema.js +11 -0
- package/dist/schemas/counter.schema.js.map +1 -0
- package/dist/schemas/index.d.ts +7 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +9 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/scan-log.schema.d.ts +3 -0
- package/dist/schemas/scan-log.schema.d.ts.map +1 -0
- package/dist/schemas/scan-log.schema.js +26 -0
- package/dist/schemas/scan-log.schema.js.map +1 -0
- package/dist/schemas/subscription-plan.schema.d.ts +3 -0
- package/dist/schemas/subscription-plan.schema.d.ts.map +1 -0
- package/dist/schemas/subscription-plan.schema.js +29 -0
- package/dist/schemas/subscription-plan.schema.js.map +1 -0
- package/dist/schemas/ticket.schema.d.ts +3 -0
- package/dist/schemas/ticket.schema.d.ts.map +1 -0
- package/dist/schemas/ticket.schema.js +35 -0
- package/dist/schemas/ticket.schema.js.map +1 -0
- package/dist/types/index.d.ts +99 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +84 -0
package/README.md
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# @mostajs/ticketing
|
|
2
|
+
|
|
3
|
+
> Reusable ticketing module — ticket lifecycle, scan validation, quota management, API route factories.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@mostajs/ticketing)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Part of the [@mosta suite](https://mostajs.dev). Depends only on `@mostajs/orm`. **Zero coupling** with auth, RBAC, or any framework beyond standard `Request`/`Response`.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table des matieres
|
|
13
|
+
|
|
14
|
+
1. [Installation](#installation)
|
|
15
|
+
2. [Concepts cles](#concepts-cles)
|
|
16
|
+
3. [Quick Start (5 etapes)](#quick-start)
|
|
17
|
+
4. [Formats de codes supportes](#formats-de-codes)
|
|
18
|
+
5. [API Route Factories](#api-route-factories)
|
|
19
|
+
6. [Core Logic (fonctions pures)](#core-logic)
|
|
20
|
+
7. [Schemas & Repositories](#schemas--repositories)
|
|
21
|
+
8. [Integration complete dans une nouvelle app](#integration-complete)
|
|
22
|
+
9. [API Reference](#api-reference)
|
|
23
|
+
10. [Architecture](#architecture)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install @mostajs/ticketing
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Prerequis : `@mostajs/orm` doit etre configure avec votre base de donnees.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Concepts cles
|
|
38
|
+
|
|
39
|
+
### Cycle de vie d'un ticket
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
[create] → active → [scan] → used → (expired)
|
|
43
|
+
↑ ↓
|
|
44
|
+
└── day_reentry (re-scan meme jour)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Modes de validite
|
|
48
|
+
|
|
49
|
+
| Mode | Comportement |
|
|
50
|
+
|------|-------------|
|
|
51
|
+
| `single_use` | Un seul scan, puis `used` |
|
|
52
|
+
| `day_reentry` | Re-entree illimitee le meme jour, quota decremente une seule fois |
|
|
53
|
+
| `time_slot` | Valide pendant N minutes apres creation |
|
|
54
|
+
| `unlimited` | Pas d'expiration |
|
|
55
|
+
|
|
56
|
+
### Quota
|
|
57
|
+
|
|
58
|
+
- `totalQuota` : nombre total de tickets autorise pour un acces
|
|
59
|
+
- `remainingQuota` : decremente a chaque scan (sauf re-entrees day_reentry)
|
|
60
|
+
- Status `depleted` automatique quand quota atteint 0
|
|
61
|
+
|
|
62
|
+
### Formats de codes
|
|
63
|
+
|
|
64
|
+
Le champ `codeFormat` permet de generer et scanner differents types de codes :
|
|
65
|
+
|
|
66
|
+
| Format | Type | Usage typique |
|
|
67
|
+
|--------|------|--------------|
|
|
68
|
+
| `qrcode` | 2D | Le plus courant, haute capacite |
|
|
69
|
+
| `code128` | 1D | Alphanumerique, logistique |
|
|
70
|
+
| `code39` | 1D | Alphanumerique, industrie |
|
|
71
|
+
| `ean13` | 1D | 13 chiffres, retail Europe |
|
|
72
|
+
| `ean8` | 1D | 8 chiffres, petits produits |
|
|
73
|
+
| `upc_a` | 1D | 12 chiffres, retail USA |
|
|
74
|
+
| `itf` | 1D | Paires numeriques, colis |
|
|
75
|
+
| `pdf417` | 2D | Haute capacite, cartes d'identite |
|
|
76
|
+
| `datamatrix` | 2D | Petit format, composants |
|
|
77
|
+
| `aztec` | 2D | Compact, cartes d'embarquement |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Quick Start
|
|
82
|
+
|
|
83
|
+
### Etape 1 — Enregistrer les schemas
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// src/dal/registry.ts
|
|
87
|
+
import { registerSchema } from '@mostajs/orm'
|
|
88
|
+
import {
|
|
89
|
+
TicketSchema,
|
|
90
|
+
ClientAccessSchema,
|
|
91
|
+
ScanLogSchema,
|
|
92
|
+
ActivitySchema,
|
|
93
|
+
SubscriptionPlanSchema,
|
|
94
|
+
CounterSchema,
|
|
95
|
+
} from '@mostajs/ticketing'
|
|
96
|
+
|
|
97
|
+
registerSchema(TicketSchema)
|
|
98
|
+
registerSchema(ClientAccessSchema)
|
|
99
|
+
registerSchema(ScanLogSchema)
|
|
100
|
+
registerSchema(ActivitySchema)
|
|
101
|
+
registerSchema(SubscriptionPlanSchema)
|
|
102
|
+
registerSchema(CounterSchema)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Etape 2 — Instancier les repositories
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// src/dal/service.ts
|
|
109
|
+
import { getDialect } from '@mostajs/orm'
|
|
110
|
+
import {
|
|
111
|
+
TicketRepository,
|
|
112
|
+
ClientAccessRepository,
|
|
113
|
+
ScanLogRepository,
|
|
114
|
+
ActivityRepository,
|
|
115
|
+
SubscriptionPlanRepository,
|
|
116
|
+
} from '@mostajs/ticketing'
|
|
117
|
+
|
|
118
|
+
export async function ticketRepo() {
|
|
119
|
+
return new TicketRepository(await getDialect())
|
|
120
|
+
}
|
|
121
|
+
export async function clientAccessRepo() {
|
|
122
|
+
return new ClientAccessRepository(await getDialect())
|
|
123
|
+
}
|
|
124
|
+
export async function scanLogRepo() {
|
|
125
|
+
return new ScanLogRepository(await getDialect())
|
|
126
|
+
}
|
|
127
|
+
export async function activityRepo() {
|
|
128
|
+
return new ActivityRepository(await getDialect())
|
|
129
|
+
}
|
|
130
|
+
export async function planRepo() {
|
|
131
|
+
return new SubscriptionPlanRepository(await getDialect())
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Etape 3 — Route scan (POST /api/scan)
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// src/app/api/scan/route.ts
|
|
139
|
+
import { createScanHandler } from '@mostajs/ticketing/api/scan.route'
|
|
140
|
+
import { ticketRepo, clientAccessRepo, scanLogRepo, clientRepo } from '@/dal/service'
|
|
141
|
+
import { checkPermission } from '@/lib/authCheck'
|
|
142
|
+
|
|
143
|
+
export const { POST } = createScanHandler({
|
|
144
|
+
checkAuth: async () => {
|
|
145
|
+
const { error, session } = await checkPermission('scan:validate')
|
|
146
|
+
return { error: error || null, userId: session?.user?.id || '' }
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
getRepositories: async () => ({
|
|
150
|
+
ticketRepo: await ticketRepo(),
|
|
151
|
+
clientAccessRepo: await clientAccessRepo(),
|
|
152
|
+
scanLogRepo: await scanLogRepo(),
|
|
153
|
+
clientRepo: await clientRepo(),
|
|
154
|
+
}),
|
|
155
|
+
|
|
156
|
+
// Optionnel : audit apres scan reussi
|
|
157
|
+
onGranted: async ({ ticket, client, isReentry, userId }) => {
|
|
158
|
+
console.log(`Scan ${isReentry ? 'reentry' : 'granted'}: ${ticket.ticketNumber}`)
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Etape 4 — Route tickets (GET + POST /api/tickets)
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// src/app/api/tickets/route.ts
|
|
167
|
+
import { createTicketsHandler } from '@mostajs/ticketing/api/tickets.route'
|
|
168
|
+
import { ticketRepo, clientRepo, clientAccessRepo, activityRepo } from '@/dal/service'
|
|
169
|
+
import { checkPermission } from '@/lib/authCheck'
|
|
170
|
+
|
|
171
|
+
export const { GET, POST } = createTicketsHandler({
|
|
172
|
+
checkAuth: async (req, permission) => {
|
|
173
|
+
const { error, session } = await checkPermission(permission)
|
|
174
|
+
return { error: error || null, userId: session?.user?.id || '' }
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
getRepositories: async () => ({
|
|
178
|
+
ticketRepo: await ticketRepo(),
|
|
179
|
+
clientRepo: await clientRepo(),
|
|
180
|
+
clientAccessRepo: await clientAccessRepo(),
|
|
181
|
+
activityRepo: await activityRepo(),
|
|
182
|
+
}),
|
|
183
|
+
|
|
184
|
+
// Format de code par defaut pour les nouveaux tickets
|
|
185
|
+
defaultCodeFormat: 'qrcode',
|
|
186
|
+
|
|
187
|
+
// Optionnel : callback apres creation
|
|
188
|
+
onCreated: async ({ ticket, userId }) => {
|
|
189
|
+
console.log(`Ticket ${ticket.ticketNumber} cree`)
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Etape 5 — Tester
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
# Creer un ticket
|
|
198
|
+
curl -X POST http://localhost:3000/api/tickets \
|
|
199
|
+
-H 'Content-Type: application/json' \
|
|
200
|
+
-d '{"clientId": "abc", "activityId": "xyz"}'
|
|
201
|
+
|
|
202
|
+
# Scanner un ticket
|
|
203
|
+
curl -X POST http://localhost:3000/api/scan \
|
|
204
|
+
-H 'Content-Type: application/json' \
|
|
205
|
+
-d '{"code": "uuid-du-ticket"}'
|
|
206
|
+
|
|
207
|
+
# Avec un code-barres specifique
|
|
208
|
+
curl -X POST http://localhost:3000/api/tickets \
|
|
209
|
+
-H 'Content-Type: application/json' \
|
|
210
|
+
-d '{"clientId": "abc", "activityId": "xyz", "codeFormat": "code128"}'
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## API Route Factories
|
|
216
|
+
|
|
217
|
+
### createScanHandler(config)
|
|
218
|
+
|
|
219
|
+
| Option | Type | Description |
|
|
220
|
+
|--------|------|-------------|
|
|
221
|
+
| `getRepositories` | `() => Promise<{...}>` | Fournit ticketRepo, clientAccessRepo, scanLogRepo, clientRepo |
|
|
222
|
+
| `checkAuth` | `(req) => Promise<{error, userId}>` | Verifie auth + permissions |
|
|
223
|
+
| `onGranted?` | `(data) => Promise<void>` | Callback apres scan reussi (audit, notifications) |
|
|
224
|
+
| `onDenied?` | `(data) => Promise<void>` | Callback apres scan refuse (alertes) |
|
|
225
|
+
|
|
226
|
+
**Requete :**
|
|
227
|
+
```json
|
|
228
|
+
POST /api/scan
|
|
229
|
+
{ "code": "uuid-or-barcode-value", "scanMethod": "webcam" }
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Reponse (granted) :**
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"data": {
|
|
236
|
+
"result": "granted",
|
|
237
|
+
"isReentry": false,
|
|
238
|
+
"ticket": { "ticketNumber": "TKT-20260306-0001", "clientName": "Alice Dupont", ... },
|
|
239
|
+
"client": { "name": "Alice Dupont", "photo": "/photos/alice.jpg" },
|
|
240
|
+
"access": { "remainingQuota": 9, "totalQuota": 10, "status": "active" }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Reponse (denied) :**
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"data": {
|
|
249
|
+
"result": "denied",
|
|
250
|
+
"reason": "ticket_already_used",
|
|
251
|
+
"ticket": { "ticketNumber": "TKT-20260306-0001", ... }
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### createTicketsHandler(config)
|
|
257
|
+
|
|
258
|
+
| Option | Type | Description |
|
|
259
|
+
|--------|------|-------------|
|
|
260
|
+
| `getRepositories` | `() => Promise<{...}>` | Fournit ticketRepo, clientRepo, clientAccessRepo, activityRepo |
|
|
261
|
+
| `checkAuth` | `(req, permission) => Promise<{error, userId}>` | Auth avec nom de permission (`ticket:view`, `ticket:create`) |
|
|
262
|
+
| `onCreated?` | `(data) => Promise<void>` | Callback apres creation ticket |
|
|
263
|
+
| `defaultCodeFormat?` | `CodeFormat` | Format de code par defaut (defaut: `'qrcode'`) |
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Core Logic
|
|
268
|
+
|
|
269
|
+
Fonctions pures, utilisables partout (serveur, worker, CLI, tests) :
|
|
270
|
+
|
|
271
|
+
### processScan(code, scanMethod, scannedBy, deps)
|
|
272
|
+
|
|
273
|
+
Pipeline 8 etapes de validation. Toute l'I/O est injectee via `deps`.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { processScan } from '@mostajs/ticketing'
|
|
277
|
+
import type { ScanDeps } from '@mostajs/ticketing'
|
|
278
|
+
|
|
279
|
+
const deps: ScanDeps = {
|
|
280
|
+
findTicketByCode: async (code) => db.tickets.findOne({ code }),
|
|
281
|
+
findAccessById: async (id) => db.accesses.findById(id),
|
|
282
|
+
findClientById: async (id) => db.clients.findById(id),
|
|
283
|
+
wasScannedToday: async (ticketId) => { /* ... */ },
|
|
284
|
+
updateTicket: async (id, data) => db.tickets.update(id, data),
|
|
285
|
+
updateAccess: async (id, data) => db.accesses.update(id, data),
|
|
286
|
+
createScanLog: async (data) => db.scanLogs.create(data),
|
|
287
|
+
resolveId: (ref) => typeof ref === 'string' ? ref : ref.id,
|
|
288
|
+
formatClientName: (c) => `${c.firstName} ${c.lastName}`,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const result = await processScan('ticket-uuid', 'webcam', 'user-123', deps)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### computeValidUntil(mode, durationMinutes)
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { computeValidUntil } from '@mostajs/ticketing'
|
|
298
|
+
|
|
299
|
+
computeValidUntil('day_reentry', null) // → fin de journee (23:59:59)
|
|
300
|
+
computeValidUntil('time_slot', 90) // → now + 90 minutes
|
|
301
|
+
computeValidUntil('single_use', null) // → null
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### decrementQuota(remainingQuota)
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { decrementQuota } from '@mostajs/ticketing'
|
|
308
|
+
|
|
309
|
+
decrementQuota(5) // → { remainingQuota: 4 }
|
|
310
|
+
decrementQuota(1) // → { remainingQuota: 0, status: 'depleted' }
|
|
311
|
+
decrementQuota(null) // → null (unlimited)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Schemas & Repositories
|
|
317
|
+
|
|
318
|
+
### Schemas disponibles
|
|
319
|
+
|
|
320
|
+
| Schema | Collection | Description |
|
|
321
|
+
|--------|-----------|-------------|
|
|
322
|
+
| `TicketSchema` | `tickets` | Ticket avec code (QR/barcode), validite, statut |
|
|
323
|
+
| `ClientAccessSchema` | `client_accesses` | Acces client-activite avec quota |
|
|
324
|
+
| `ScanLogSchema` | `scan_logs` | Journal des scans (granted/denied) |
|
|
325
|
+
| `ActivitySchema` | `activities` | Activites avec planning et mode de validite |
|
|
326
|
+
| `SubscriptionPlanSchema` | `subscription_plans` | Plans d'abonnement |
|
|
327
|
+
| `CounterSchema` | `counters` | Auto-increment interne |
|
|
328
|
+
|
|
329
|
+
### Repositories
|
|
330
|
+
|
|
331
|
+
| Repository | Methodes cles |
|
|
332
|
+
|-----------|--------------|
|
|
333
|
+
| `TicketRepository` | `createWithAutoFields()`, `findByCode()`, `markUsed()`, `countByAccess()` |
|
|
334
|
+
| `ClientAccessRepository` | `findActiveAccess()`, `decrementQuota()`, `block()` |
|
|
335
|
+
| `ScanLogRepository` | `wasScannedToday()`, `countToday()`, `findDistinctClientsToday()` |
|
|
336
|
+
| `ActivityRepository` | `findActive()`, `findBySlug()`, `findAllOrdered()` |
|
|
337
|
+
| `SubscriptionPlanRepository` | `findActive()`, `findAllWithActivities()` |
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Integration complete
|
|
342
|
+
|
|
343
|
+
### Nouvelle app Next.js depuis zero
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
# 1. Creer le projet
|
|
347
|
+
npx create-next-app@latest my-ticketing-app
|
|
348
|
+
cd my-ticketing-app
|
|
349
|
+
|
|
350
|
+
# 2. Installer
|
|
351
|
+
npm install @mostajs/orm @mostajs/ticketing
|
|
352
|
+
|
|
353
|
+
# 3. Configurer la DB
|
|
354
|
+
echo 'DATABASE_URL=mongodb://localhost:27017/myapp' >> .env.local
|
|
355
|
+
|
|
356
|
+
# 4. Enregistrer les schemas dans src/dal/registry.ts
|
|
357
|
+
# 5. Creer les repo helpers dans src/dal/service.ts
|
|
358
|
+
# 6. Creer les routes API (2 fichiers, ~30 lignes chacun)
|
|
359
|
+
# 7. npm run dev
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Avec audit (@mostajs/audit)
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { createScanHandler } from '@mostajs/ticketing/api/scan.route'
|
|
366
|
+
import { logAudit, getAuditUser } from '@mostajs/audit/lib/audit'
|
|
367
|
+
|
|
368
|
+
export const { POST } = createScanHandler({
|
|
369
|
+
// ...
|
|
370
|
+
onGranted: async ({ ticket, client, isReentry, userId }) => {
|
|
371
|
+
await logAudit({
|
|
372
|
+
userId,
|
|
373
|
+
action: isReentry ? 'scan_reentry' : 'scan_granted',
|
|
374
|
+
module: 'scan',
|
|
375
|
+
resource: ticket.ticketNumber,
|
|
376
|
+
})
|
|
377
|
+
},
|
|
378
|
+
onDenied: async ({ reason, ticket, userId }) => {
|
|
379
|
+
await logAudit({
|
|
380
|
+
userId,
|
|
381
|
+
action: 'scan_denied',
|
|
382
|
+
module: 'scan',
|
|
383
|
+
resource: ticket?.ticketNumber,
|
|
384
|
+
details: { reason },
|
|
385
|
+
})
|
|
386
|
+
},
|
|
387
|
+
})
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Avec un scanner de code-barres physique
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// Le scanner physique envoie le meme POST /api/scan
|
|
394
|
+
// Seul le scanMethod change
|
|
395
|
+
fetch('/api/scan', {
|
|
396
|
+
method: 'POST',
|
|
397
|
+
body: JSON.stringify({
|
|
398
|
+
code: 'TKT-20260306-0001', // valeur lue par le scanner
|
|
399
|
+
scanMethod: 'handheld_scanner',
|
|
400
|
+
}),
|
|
401
|
+
})
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## API Reference
|
|
407
|
+
|
|
408
|
+
### Types
|
|
409
|
+
|
|
410
|
+
| Type | Description |
|
|
411
|
+
|------|-------------|
|
|
412
|
+
| `CodeFormat` | `'qrcode' \| 'code128' \| 'code39' \| 'ean13' \| 'ean8' \| 'upc_a' \| 'itf' \| 'pdf417' \| 'datamatrix' \| 'aztec'` |
|
|
413
|
+
| `ValidityMode` | `'day_reentry' \| 'single_use' \| 'time_slot' \| 'unlimited'` |
|
|
414
|
+
| `TicketStatus` | `'active' \| 'used' \| 'expired' \| 'cancelled'` |
|
|
415
|
+
| `ScanResult` | `'granted' \| 'denied'` |
|
|
416
|
+
| `ScanMethod` | `'webcam' \| 'pwa_camera' \| 'handheld_scanner' \| 'nfc'` |
|
|
417
|
+
| `AccessType` | `'unlimited' \| 'count' \| 'temporal' \| 'mixed'` |
|
|
418
|
+
| `DenyReason` | `'invalid_ticket' \| 'ticket_already_used' \| 'ticket_expired' \| 'ticket_cancelled' \| 'quota_depleted' \| 'access_expired' \| 'client_suspended'` |
|
|
419
|
+
| `ScanDeps` | Interface d'injection pour `processScan()` |
|
|
420
|
+
| `ScanHandlerConfig` | Config de `createScanHandler()` |
|
|
421
|
+
| `TicketsHandlerConfig` | Config de `createTicketsHandler()` |
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Architecture
|
|
426
|
+
|
|
427
|
+
```
|
|
428
|
+
@mostajs/ticketing
|
|
429
|
+
├── schemas/
|
|
430
|
+
│ ├── ticket.schema.ts # Ticket (code multi-format, validite, statut)
|
|
431
|
+
│ ├── client-access.schema.ts # Acces client-activite avec quota
|
|
432
|
+
│ ├── scan-log.schema.ts # Journal des scans
|
|
433
|
+
│ ├── activity.schema.ts # Activites (planning, mode validite)
|
|
434
|
+
│ ├── subscription-plan.schema.ts # Plans d'abonnement
|
|
435
|
+
│ └── counter.schema.ts # Auto-increment sequences
|
|
436
|
+
├── repositories/
|
|
437
|
+
│ ├── ticket.repository.ts # CRUD + findByCode, createWithAutoFields
|
|
438
|
+
│ ├── client-access.repository.ts # Quota, findActiveAccess
|
|
439
|
+
│ ├── scan-log.repository.ts # wasScannedToday, countToday
|
|
440
|
+
│ ├── activity.repository.ts # findActive, findBySlug
|
|
441
|
+
│ └── subscription-plan.repository.ts
|
|
442
|
+
├── lib/
|
|
443
|
+
│ ├── scan-processor.ts # Pipeline 8 etapes (pure, injectable)
|
|
444
|
+
│ ├── validity-checker.ts # computeValidUntil, isExpired
|
|
445
|
+
│ └── quota-manager.ts # decrementQuota, wouldExceedQuota
|
|
446
|
+
├── api/
|
|
447
|
+
│ ├── scan.route.ts # Factory createScanHandler(config)
|
|
448
|
+
│ └── tickets.route.ts # Factory createTicketsHandler(config)
|
|
449
|
+
├── types/
|
|
450
|
+
│ └── index.ts # CodeFormat, ValidityMode, ScanDeps, etc.
|
|
451
|
+
└── index.ts # Barrel exports
|
|
452
|
+
|
|
453
|
+
Dependances:
|
|
454
|
+
@mostajs/orm (seule dep runtime)
|
|
455
|
+
next >= 14 (peer, optionnel)
|
|
456
|
+
|
|
457
|
+
Zero dependance sur: @mostajs/auth, @mostajs/rbac, @mostajs/audit
|
|
458
|
+
(l'app injecte ses propres callbacks auth/audit)
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Pattern Factory (injection de dependances)
|
|
462
|
+
|
|
463
|
+
```
|
|
464
|
+
┌──────────────────────┐ inject callbacks ┌──────────────────────┐
|
|
465
|
+
│ @mostajs/ticketing │ ◄──────────────────────── │ Votre app │
|
|
466
|
+
│ │ │ │
|
|
467
|
+
│ createScanHandler({ │ │ checkAuth: () => │
|
|
468
|
+
│ checkAuth, │ │ verifyToken(...) │
|
|
469
|
+
│ getRepositories, │ │ getRepositories: () │
|
|
470
|
+
│ onGranted, │ │ => { ticketRepo, │
|
|
471
|
+
│ }) │ │ scanLogRepo } │
|
|
472
|
+
└──────────────────────┘ └──────────────────────┘
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## License
|
|
478
|
+
|
|
479
|
+
MIT — Dr Hamid MADANI <drmdh@msn.com>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ScanHandlerConfig } from '../types/index';
|
|
2
|
+
/**
|
|
3
|
+
* Factory: creates a POST handler for ticket scanning.
|
|
4
|
+
*
|
|
5
|
+
* The host app injects its repositories, auth check, and optional audit callbacks.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* // src/app/api/scan/route.ts
|
|
9
|
+
* import { createScanHandler } from '@mostajs/ticketing/api/scan.route'
|
|
10
|
+
* export const { POST } = createScanHandler({ ... })
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function createScanHandler(config: ScanHandlerConfig): {
|
|
14
|
+
POST: (req: Request) => Promise<Response>;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=scan.route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.route.d.ts","sourceRoot":"","sources":["../../api/scan.route.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAc,MAAM,gBAAgB,CAAC;AAEpE;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB;gBAChC,OAAO;EAmEjC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @mostajs/ticketing — Scan route factory
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
import { processScan } from '../lib/scan-processor';
|
|
4
|
+
/**
|
|
5
|
+
* Factory: creates a POST handler for ticket scanning.
|
|
6
|
+
*
|
|
7
|
+
* The host app injects its repositories, auth check, and optional audit callbacks.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* // src/app/api/scan/route.ts
|
|
11
|
+
* import { createScanHandler } from '@mostajs/ticketing/api/scan.route'
|
|
12
|
+
* export const { POST } = createScanHandler({ ... })
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function createScanHandler(config) {
|
|
16
|
+
async function POST(req) {
|
|
17
|
+
const { error, userId } = await config.checkAuth(req);
|
|
18
|
+
if (error)
|
|
19
|
+
return error;
|
|
20
|
+
const body = await req.json();
|
|
21
|
+
const { code, qrCode, scanMethod = 'webcam' } = body;
|
|
22
|
+
// Support both "code" (generic) and "qrCode" (legacy) field names
|
|
23
|
+
const codeValue = code || qrCode;
|
|
24
|
+
if (!codeValue) {
|
|
25
|
+
return Response.json({ error: { code: 'INVALID', message: 'Code manquant' } }, { status: 400 });
|
|
26
|
+
}
|
|
27
|
+
const repos = await config.getRepositories();
|
|
28
|
+
const resolveId = (ref) => {
|
|
29
|
+
if (!ref)
|
|
30
|
+
return '';
|
|
31
|
+
if (typeof ref === 'string')
|
|
32
|
+
return ref;
|
|
33
|
+
return ref.id ?? ref._id ?? String(ref);
|
|
34
|
+
};
|
|
35
|
+
const result = await processScan(codeValue, scanMethod, userId, {
|
|
36
|
+
findTicketByCode: (c) => repos.ticketRepo.findByCode?.(c) ?? repos.ticketRepo.findOne({ code: c }),
|
|
37
|
+
findAccessById: (id) => repos.clientAccessRepo.findById(id),
|
|
38
|
+
findClientById: (id) => repos.clientRepo.findById(id),
|
|
39
|
+
wasScannedToday: async (ticketId) => {
|
|
40
|
+
const start = new Date();
|
|
41
|
+
start.setHours(0, 0, 0, 0);
|
|
42
|
+
const log = await repos.scanLogRepo.findOne({
|
|
43
|
+
ticket: ticketId,
|
|
44
|
+
result: 'granted',
|
|
45
|
+
timestamp: { $gte: start },
|
|
46
|
+
});
|
|
47
|
+
return log !== null;
|
|
48
|
+
},
|
|
49
|
+
updateTicket: (id, data) => repos.ticketRepo.update(id, data),
|
|
50
|
+
updateAccess: (id, data) => repos.clientAccessRepo.update(id, data),
|
|
51
|
+
createScanLog: (data) => repos.scanLogRepo.create(data),
|
|
52
|
+
resolveId,
|
|
53
|
+
formatClientName: (client) => `${client.firstName} ${client.lastName}`,
|
|
54
|
+
});
|
|
55
|
+
if (result.result === 'granted' && config.onGranted) {
|
|
56
|
+
await config.onGranted({
|
|
57
|
+
ticket: result.ticket,
|
|
58
|
+
client: result.client,
|
|
59
|
+
access: result.access,
|
|
60
|
+
isReentry: result.isReentry,
|
|
61
|
+
userId,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (result.result === 'denied' && config.onDenied) {
|
|
65
|
+
await config.onDenied({
|
|
66
|
+
reason: result.reason,
|
|
67
|
+
ticket: result.ticket,
|
|
68
|
+
userId,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return Response.json({ data: result });
|
|
72
|
+
}
|
|
73
|
+
return { POST };
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=scan.route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.route.js","sourceRoot":"","sources":["../../api/scan.route.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TicketsHandlerConfig } from '../types/index';
|
|
2
|
+
/**
|
|
3
|
+
* Factory: creates GET + POST handlers for ticket management.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* // src/app/api/tickets/route.ts
|
|
7
|
+
* import { createTicketsHandler } from '@mostajs/ticketing/api/tickets.route'
|
|
8
|
+
* export const { GET, POST } = createTicketsHandler({ ... })
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare function createTicketsHandler(config: TicketsHandlerConfig): {
|
|
12
|
+
GET: (req: Request) => Promise<Response>;
|
|
13
|
+
POST: (req: Request) => Promise<Response>;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=tickets.route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tickets.route.d.ts","sourceRoot":"","sources":["../../api/tickets.route.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAc,MAAM,gBAAgB,CAAC;AAEvE;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB;eAEvC,OAAO;gBAmBN,OAAO;EA+EjC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @mostajs/ticketing — Tickets route factory (GET list + POST create)
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
import { computeValidUntil } from '../lib/validity-checker';
|
|
4
|
+
import { wouldExceedQuota } from '../lib/quota-manager';
|
|
5
|
+
/**
|
|
6
|
+
* Factory: creates GET + POST handlers for ticket management.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // src/app/api/tickets/route.ts
|
|
10
|
+
* import { createTicketsHandler } from '@mostajs/ticketing/api/tickets.route'
|
|
11
|
+
* export const { GET, POST } = createTicketsHandler({ ... })
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function createTicketsHandler(config) {
|
|
15
|
+
async function GET(req) {
|
|
16
|
+
const { error } = await config.checkAuth(req, 'ticket:view');
|
|
17
|
+
if (error)
|
|
18
|
+
return error;
|
|
19
|
+
const url = new URL(req.url);
|
|
20
|
+
const clientId = url.searchParams.get('clientId');
|
|
21
|
+
const status = url.searchParams.get('status');
|
|
22
|
+
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
23
|
+
const filter = {};
|
|
24
|
+
if (clientId)
|
|
25
|
+
filter.client = clientId;
|
|
26
|
+
if (status)
|
|
27
|
+
filter.status = status;
|
|
28
|
+
const repos = await config.getRepositories();
|
|
29
|
+
const tickets = await repos.ticketRepo.findAll(filter, { sort: { createdAt: -1 }, limit });
|
|
30
|
+
return Response.json({ data: tickets });
|
|
31
|
+
}
|
|
32
|
+
async function POST(req) {
|
|
33
|
+
const { error, userId } = await config.checkAuth(req, 'ticket:create');
|
|
34
|
+
if (error)
|
|
35
|
+
return error;
|
|
36
|
+
const body = await req.json();
|
|
37
|
+
const { clientId, activityId, ticketType = 'standard', sourceClientId, amount = 0, codeFormat } = body;
|
|
38
|
+
if (!clientId || !activityId) {
|
|
39
|
+
return Response.json({ error: { code: 'VALIDATION_ERROR', message: 'clientId et activityId requis' } }, { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
// For gift tickets, the access check is on the sourceClient
|
|
42
|
+
const accessClientId = ticketType === 'cadeau' && sourceClientId ? sourceClientId : clientId;
|
|
43
|
+
const repos = await config.getRepositories();
|
|
44
|
+
const [client, activity, clientAccess] = await Promise.all([
|
|
45
|
+
repos.clientRepo.findById(clientId),
|
|
46
|
+
repos.activityRepo.findById(activityId),
|
|
47
|
+
repos.clientAccessRepo.findActiveAccess(accessClientId, activityId),
|
|
48
|
+
]);
|
|
49
|
+
if (!client) {
|
|
50
|
+
return Response.json({ error: { code: 'NOT_FOUND', message: 'Client non trouvé' } }, { status: 404 });
|
|
51
|
+
}
|
|
52
|
+
if (!activity) {
|
|
53
|
+
return Response.json({ error: { code: 'NOT_FOUND', message: 'Activité non trouvée' } }, { status: 404 });
|
|
54
|
+
}
|
|
55
|
+
if (!clientAccess) {
|
|
56
|
+
return Response.json({ error: { code: 'NO_ACCESS', message: "Ce client n'a pas accès à cette activité" } }, { status: 403 });
|
|
57
|
+
}
|
|
58
|
+
// Check quota
|
|
59
|
+
if (clientAccess.totalQuota != null) {
|
|
60
|
+
const ticketCount = await repos.ticketRepo.countByAccess(clientAccess.id);
|
|
61
|
+
if (wouldExceedQuota(clientAccess.totalQuota, ticketCount)) {
|
|
62
|
+
return Response.json({ error: { code: 'QUOTA_EXCEEDED', message: `Quota atteint : ${ticketCount}/${clientAccess.totalQuota} tickets` } }, { status: 403 });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Gift tickets don't expire (usable another day)
|
|
66
|
+
const validUntil = ticketType === 'cadeau'
|
|
67
|
+
? null
|
|
68
|
+
: computeValidUntil(activity.ticketValidityMode, activity.ticketDuration);
|
|
69
|
+
const resolvedFormat = codeFormat || config.defaultCodeFormat || 'qrcode';
|
|
70
|
+
const ticket = await repos.ticketRepo.createWithAutoFields({
|
|
71
|
+
client: clientId,
|
|
72
|
+
clientAccess: clientAccess.id,
|
|
73
|
+
activity: activityId,
|
|
74
|
+
ticketType,
|
|
75
|
+
sourceClient: ticketType === 'cadeau' ? sourceClientId : null,
|
|
76
|
+
clientName: `${client.firstName} ${client.lastName}`,
|
|
77
|
+
activityName: activity.name,
|
|
78
|
+
validityMode: activity.ticketValidityMode,
|
|
79
|
+
validUntil: validUntil?.toISOString() || null,
|
|
80
|
+
amount,
|
|
81
|
+
codeFormat: resolvedFormat,
|
|
82
|
+
createdBy: userId,
|
|
83
|
+
});
|
|
84
|
+
if (config.onCreated) {
|
|
85
|
+
await config.onCreated({ ticket, userId });
|
|
86
|
+
}
|
|
87
|
+
return Response.json({ data: ticket }, { status: 201 });
|
|
88
|
+
}
|
|
89
|
+
return { GET, POST };
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=tickets.route.js.map
|