@ingenx-io/valets-schema-mcp-server 0.1.1 → 0.1.3

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.
Files changed (43) hide show
  1. package/data/docs/collections/firestore-paths.md +49 -0
  2. package/data/docs/decisions/migrations.md +56 -0
  3. package/data/docs/decisions/summary.md +78 -0
  4. package/data/docs/enums/booking-status.md +26 -0
  5. package/data/docs/enums/customer-payment-status.md +26 -0
  6. package/data/docs/enums/customer-payment-target-type.md +23 -0
  7. package/data/docs/enums/delivery-type.md +23 -0
  8. package/data/docs/enums/event-status.md +30 -0
  9. package/data/docs/enums/fulfillment-status.md +32 -0
  10. package/data/docs/enums/loyalty-transaction-type.md +32 -0
  11. package/data/docs/enums/order-status.md +65 -0
  12. package/data/docs/enums/payment-method.md +36 -0
  13. package/data/docs/enums/payment-proof-status.md +23 -0
  14. package/data/docs/enums/payment-status.md +34 -0
  15. package/data/docs/enums/return-status.md +32 -0
  16. package/data/docs/enums/session-status.md +32 -0
  17. package/data/docs/enums/ticket-status.md +29 -0
  18. package/data/docs/index.md +102 -0
  19. package/data/docs/models/booking-version.md +295 -0
  20. package/data/docs/models/booking.md +1754 -0
  21. package/data/docs/models/customer-payment-allocation.md +336 -0
  22. package/data/docs/models/customer-payment.md +392 -0
  23. package/data/docs/models/customer.md +475 -0
  24. package/data/docs/models/event.md +386 -0
  25. package/data/docs/models/loyalty-config.md +317 -0
  26. package/data/docs/models/loyalty-reward.md +236 -0
  27. package/data/docs/models/loyalty-status.md +328 -0
  28. package/data/docs/models/loyalty-transaction.md +326 -0
  29. package/data/docs/models/metrics-current.md +532 -0
  30. package/data/docs/models/metrics-daily.md +548 -0
  31. package/data/docs/models/metrics-monthly.md +548 -0
  32. package/data/docs/models/order-item.md +361 -0
  33. package/data/docs/models/order.md +1637 -0
  34. package/data/docs/models/payment-summary.md +123 -0
  35. package/data/docs/models/sale.md +540 -0
  36. package/data/docs/models/ticket.md +405 -0
  37. package/data/docs/triggers/event-ticket-triggers.md +204 -0
  38. package/data/docs/triggers/loyalty-automation.md +123 -0
  39. package/data/static/decisions.json +966 -0
  40. package/data/static/llms.txt +1056 -0
  41. package/data/static/openapi.yaml +3090 -0
  42. package/data/static/schemas.json +4055 -0
  43. package/package.json +1 -1
@@ -0,0 +1,405 @@
1
+ ---
2
+ title: "Ticket"
3
+ sidebar_label: "Ticket"
4
+ sidebar_position: 17
5
+ ---
6
+
7
+ # Ticket
8
+
9
+ <details>
10
+ <summary>Example JSON</summary>
11
+
12
+ ```json
13
+ {
14
+ "id": "bk_abc123def456",
15
+ "eventId": "eve_ref123",
16
+ "companyId": "comp_xyz789",
17
+ "customerId": null,
18
+ "customerName": null,
19
+ "customerEmail": null,
20
+ "customerPhone": null,
21
+ "status": "status",
22
+ "usedAt": "usedAt",
23
+ "usedBy": null,
24
+ "usedByName": null,
25
+ "price": null,
26
+ "notes": null,
27
+ "createdAt": "createdAt",
28
+ "updatedAt": "updatedAt",
29
+ "createdBy": null
30
+ }
31
+ ```
32
+
33
+ </details>
34
+
35
+
36
+ - [1. Property `id`](#id)
37
+ - [2. Property `eventId`](#eventId)
38
+ - [3. Property `companyId`](#companyId)
39
+ - [4. Property `customerId`](#customerId)
40
+ - [5. Property `customerName`](#customerName)
41
+ - [6. Property `customerEmail`](#customerEmail)
42
+ - [7. Property `customerPhone`](#customerPhone)
43
+ - [8. Property `status`](#status)
44
+ - [9. Property `usedAt`](#usedAt)
45
+ - [9.1. Property `firestore-timestamp`](#usedAt_anyOf_i0)
46
+ - [9.1.1. Property `_seconds`](#usedAt_anyOf_i0__seconds)
47
+ - [9.1.2. Property `_nanoseconds`](#usedAt_anyOf_i0__nanoseconds)
48
+ - [9.2. Property `item 1`](#usedAt_anyOf_i1)
49
+ - [10. Property `usedBy`](#usedBy)
50
+ - [11. Property `usedByName`](#usedByName)
51
+ - [12. Property `price`](#price)
52
+ - [13. Property `notes`](#notes)
53
+ - [14. Property `createdAt`](#createdAt)
54
+ - [14.1. Property `_seconds`](#usedAt_anyOf_i0__seconds)
55
+ - [14.2. Property `_nanoseconds`](#usedAt_anyOf_i0__nanoseconds)
56
+ - [15. Property `updatedAt`](#updatedAt)
57
+ - [15.1. Property `_seconds`](#usedAt_anyOf_i0__seconds)
58
+ - [15.2. Property `_nanoseconds`](#usedAt_anyOf_i0__nanoseconds)
59
+ - [16. Property `createdBy`](#createdBy)
60
+
61
+ | | |
62
+ | ------------------------- | -------------------- |
63
+ | **Type** | `object` |
64
+ | **Required** | No |
65
+ | **Additional properties** | Not allowed |
66
+ | **Defined in** | #/definitions/ticket |
67
+
68
+ **Description:** Ticket model (D32). Collection: companies/\{companyId\}/events/\{eventId\}/tickets/\{ticketId\}. Mobile-only today; Dashboard in Wave 4.
69
+
70
+ | Property | Pattern | Type | Deprecated | Definition | Title/Description |
71
+ | ---------------------------------- | ------- | ---------------- | ---------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
72
+ | + [id](#id ) | No | string | No | - | (Read-only) Firestore document ID. Note: Ticket does not have a uid field. |
73
+ | + [eventId](#eventId ) | No | string | No | - | (Immutable) FK → Event.id. Parent event this ticket belongs to. Set at creation. |
74
+ | + [companyId](#companyId ) | No | string | No | - | (Immutable, Denormalized) FK → Company document ID. Denormalized from parent Event for direct queries. |
75
+ | - [customerId](#customerId ) | No | string or null | No | - | FK → Customer.id. Optional structured link to Customer document (D29). Added additively alongside denormalized fields. |
76
+ | - [customerName](#customerName ) | No | string or null | No | - | (Denormalized) From Customer.name at write time. |
77
+ | - [customerEmail](#customerEmail ) | No | string or null | No | - | (Denormalized) From Customer.email at write time. |
78
+ | - [customerPhone](#customerPhone ) | No | string or null | No | - | (Denormalized) From Customer.phone at write time. |
79
+ | + [status](#status ) | No | enum (of string) | No | In #/definitions/ticket-status | Ticket lifecycle (D32). SCREAMING_SNAKE per D04. MIG-10 migrates legacy lowercase values. |
80
+ | - [usedAt](#usedAt ) | No | Combination | No | - | (Read-only) Timestamp when ticket was scanned/used. Set by scan operation. |
81
+ | - [usedBy](#usedBy ) | No | string or null | No | - | (Read-only) FK → User/staff UID who scanned the ticket. |
82
+ | - [usedByName](#usedByName ) | No | string or null | No | - | (Read-only, Denormalized) From User display name at scan time. |
83
+ | - [price](#price ) | No | number or null | No | - | - |
84
+ | - [notes](#notes ) | No | string or null | No | - | - |
85
+ | + [createdAt](#createdAt ) | No | object | No | In #/definitions/firestore-timestamp | (Read-only) Server-generated creation timestamp. |
86
+ | + [updatedAt](#updatedAt ) | No | object | No | In #/definitions/firestore-timestamp | (Read-only) Server-generated update timestamp. |
87
+ | - [createdBy](#createdBy ) | No | string or null | No | - | (Immutable) FK → User/staff UID who created this ticket. |
88
+
89
+ ## <a name="id"></a>1. Property `id`
90
+
91
+ | | |
92
+ | ------------ | -------- |
93
+ | **Type** | `string` |
94
+ | **Required** | Yes |
95
+
96
+ **Description:** (Read-only) Firestore document ID. Note: Ticket does not have a uid field.
97
+
98
+ :::warning Server-set
99
+ Do not include in write requests. This field is set exclusively by the server (Firestore trigger or Admin SDK). Clients that send it will have the value silently ignored or may receive a validation error.
100
+ :::
101
+
102
+ ## <a name="eventId"></a>2. Property `eventId`
103
+
104
+ | | |
105
+ | ------------ | -------- |
106
+ | **Type** | `string` |
107
+ | **Required** | Yes |
108
+
109
+ **Description:** (Immutable) FK → Event.id. Parent event this ticket belongs to. Set at creation.
110
+
111
+ :::info Immutable
112
+ Set at creation only. This field cannot be modified after the document is created. Include it in CREATE payloads; omit it (or leave unchanged) in UPDATE payloads.
113
+ :::
114
+
115
+ ## <a name="companyId"></a>3. Property `companyId`
116
+
117
+ | | |
118
+ | ------------ | -------- |
119
+ | **Type** | `string` |
120
+ | **Required** | Yes |
121
+
122
+ **Description:** (Immutable, Denormalized) FK → Company document ID. Denormalized from parent Event for direct queries.
123
+
124
+ :::info Immutable
125
+ Set at creation only. This field cannot be modified after the document is created. Include it in CREATE payloads; omit it (or leave unchanged) in UPDATE payloads.
126
+ :::
127
+
128
+ ## <a name="customerId"></a>4. Property `customerId`
129
+
130
+ | | |
131
+ | ------------ | ---------------- |
132
+ | **Type** | `string or null` |
133
+ | **Required** | No |
134
+
135
+ **Description:** FK → Customer.id. Optional structured link to Customer document (D29). Added additively alongside denormalized fields.
136
+
137
+ :::note
138
+ Added additively per D29. Denormalized customerName/email/phone are kept for backwards compatibility. Null on tickets created before D29 was applied.
139
+ :::
140
+
141
+ :::info See also
142
+ **Decisions:** `D29`
143
+ :::
144
+
145
+ ## <a name="customerName"></a>5. Property `customerName`
146
+
147
+ | | |
148
+ | ------------ | ---------------- |
149
+ | **Type** | `string or null` |
150
+ | **Required** | No |
151
+
152
+ **Description:** (Denormalized) From Customer.name at write time.
153
+
154
+ ## <a name="customerEmail"></a>6. Property `customerEmail`
155
+
156
+ | | |
157
+ | ------------ | ---------------- |
158
+ | **Type** | `string or null` |
159
+ | **Required** | No |
160
+
161
+ **Description:** (Denormalized) From Customer.email at write time.
162
+
163
+ ## <a name="customerPhone"></a>7. Property `customerPhone`
164
+
165
+ | | |
166
+ | ------------ | ---------------- |
167
+ | **Type** | `string or null` |
168
+ | **Required** | No |
169
+
170
+ **Description:** (Denormalized) From Customer.phone at write time.
171
+
172
+ ## <a name="status"></a>8. Property `status`
173
+
174
+ | | |
175
+ | -------------- | --------------------------- |
176
+ | **Type** | `enum (of string)` |
177
+ | **Required** | Yes |
178
+ | **Defined in** | #/definitions/ticket-status |
179
+
180
+ **Description:** Ticket lifecycle (D32). SCREAMING_SNAKE per D04. MIG-10 migrates legacy lowercase values.
181
+
182
+ Must be one of:
183
+ * "VALID"
184
+ * "USED"
185
+ * "CANCELLED"
186
+
187
+ ## <a name="usedAt"></a>9. Property `usedAt`
188
+
189
+ | | |
190
+ | ------------------------- | ---------------- |
191
+ | **Type** | `combining` |
192
+ | **Required** | No |
193
+ | **Additional properties** | Any type allowed |
194
+
195
+ **Description:** (Read-only) Timestamp when ticket was scanned/used. Set by scan operation.
196
+
197
+ | Any of(Option) |
198
+ | --------------------------------------- |
199
+ | [firestore-timestamp](#usedAt_anyOf_i0) |
200
+ | [item 1](#usedAt_anyOf_i1) |
201
+
202
+ ### <a name="usedAt_anyOf_i0"></a>9.1. Property `firestore-timestamp`
203
+
204
+ | | |
205
+ | ------------------------- | --------------------------------- |
206
+ | **Type** | `object` |
207
+ | **Required** | No |
208
+ | **Additional properties** | Not allowed |
209
+ | **Defined in** | #/definitions/firestore-timestamp |
210
+
211
+ **Description:** Firestore Timestamp serialized representation
212
+
213
+ | Property | Pattern | Type | Deprecated | Definition | Title/Description |
214
+ | ------------------------------------------------ | ------- | ------- | ---------- | ---------- | ----------------- |
215
+ | + [_seconds](#usedAt_anyOf_i0__seconds ) | No | integer | No | - | - |
216
+ | + [_nanoseconds](#usedAt_anyOf_i0__nanoseconds ) | No | integer | No | - | - |
217
+
218
+ #### <a name="usedAt_anyOf_i0__seconds"></a>9.1.1. Property `_seconds`
219
+
220
+ | | |
221
+ | ------------ | --------- |
222
+ | **Type** | `integer` |
223
+ | **Required** | Yes |
224
+
225
+ | Restrictions | |
226
+ | ------------ | ---------------------- |
227
+ | **Minimum** | &ge; -9007199254740991 |
228
+ | **Maximum** | &le; 9007199254740991 |
229
+
230
+ #### <a name="usedAt_anyOf_i0__nanoseconds"></a>9.1.2. Property `_nanoseconds`
231
+
232
+ | | |
233
+ | ------------ | --------- |
234
+ | **Type** | `integer` |
235
+ | **Required** | Yes |
236
+
237
+ | Restrictions | |
238
+ | ------------ | ---------------------- |
239
+ | **Minimum** | &ge; -9007199254740991 |
240
+ | **Maximum** | &le; 9007199254740991 |
241
+
242
+ ### <a name="usedAt_anyOf_i1"></a>9.2. Property `item 1`
243
+
244
+ | | |
245
+ | ------------ | ------ |
246
+ | **Type** | `null` |
247
+ | **Required** | No |
248
+
249
+ :::warning Server-set
250
+ Do not include in write requests. This field is set exclusively by the server (Firestore trigger or Admin SDK). Clients that send it will have the value silently ignored or may receive a validation error.
251
+ :::
252
+
253
+ ## <a name="usedBy"></a>10. Property `usedBy`
254
+
255
+ | | |
256
+ | ------------ | ---------------- |
257
+ | **Type** | `string or null` |
258
+ | **Required** | No |
259
+
260
+ **Description:** (Read-only) FK → User/staff UID who scanned the ticket.
261
+
262
+ :::warning Server-set
263
+ Do not include in write requests. This field is set exclusively by the server (Firestore trigger or Admin SDK). Clients that send it will have the value silently ignored or may receive a validation error.
264
+ :::
265
+
266
+ ## <a name="usedByName"></a>11. Property `usedByName`
267
+
268
+ | | |
269
+ | ------------ | ---------------- |
270
+ | **Type** | `string or null` |
271
+ | **Required** | No |
272
+
273
+ **Description:** (Read-only, Denormalized) From User display name at scan time.
274
+
275
+ :::warning Server-set
276
+ Do not include in write requests. This field is set exclusively by the server (Firestore trigger or Admin SDK). Clients that send it will have the value silently ignored or may receive a validation error.
277
+ :::
278
+
279
+ ## <a name="price"></a>12. Property `price`
280
+
281
+ | | |
282
+ | ------------ | ---------------- |
283
+ | **Type** | `number or null` |
284
+ | **Required** | No |
285
+
286
+ ## <a name="notes"></a>13. Property `notes`
287
+
288
+ | | |
289
+ | ------------ | ---------------- |
290
+ | **Type** | `string or null` |
291
+ | **Required** | No |
292
+
293
+ ## <a name="createdAt"></a>14. Property `createdAt`
294
+
295
+ | | |
296
+ | ------------------------- | --------------------------------- |
297
+ | **Type** | `object` |
298
+ | **Required** | Yes |
299
+ | **Additional properties** | Not allowed |
300
+ | **Defined in** | #/definitions/firestore-timestamp |
301
+
302
+ **Description:** (Read-only) Server-generated creation timestamp.
303
+
304
+ | Property | Pattern | Type | Deprecated | Definition | Title/Description |
305
+ | ------------------------------------------------ | ------- | ------- | ---------- | ---------- | ----------------- |
306
+ | + [_seconds](#usedAt_anyOf_i0__seconds ) | No | integer | No | - | - |
307
+ | + [_nanoseconds](#usedAt_anyOf_i0__nanoseconds ) | No | integer | No | - | - |
308
+
309
+ ### <a name="usedAt_anyOf_i0__seconds"></a>14.1. Property `_seconds`
310
+
311
+ | | |
312
+ | ------------ | --------- |
313
+ | **Type** | `integer` |
314
+ | **Required** | Yes |
315
+
316
+ | Restrictions | |
317
+ | ------------ | ---------------------- |
318
+ | **Minimum** | &ge; -9007199254740991 |
319
+ | **Maximum** | &le; 9007199254740991 |
320
+
321
+ ### <a name="usedAt_anyOf_i0__nanoseconds"></a>14.2. Property `_nanoseconds`
322
+
323
+ | | |
324
+ | ------------ | --------- |
325
+ | **Type** | `integer` |
326
+ | **Required** | Yes |
327
+
328
+ | Restrictions | |
329
+ | ------------ | ---------------------- |
330
+ | **Minimum** | &ge; -9007199254740991 |
331
+ | **Maximum** | &le; 9007199254740991 |
332
+
333
+ :::warning Server-set
334
+ Do not include in write requests. This field is set exclusively by the server (Firestore trigger or Admin SDK). Clients that send it will have the value silently ignored or may receive a validation error.
335
+ :::
336
+
337
+ ## <a name="updatedAt"></a>15. Property `updatedAt`
338
+
339
+ | | |
340
+ | ------------------------- | --------------------------------- |
341
+ | **Type** | `object` |
342
+ | **Required** | Yes |
343
+ | **Additional properties** | Not allowed |
344
+ | **Defined in** | #/definitions/firestore-timestamp |
345
+
346
+ **Description:** (Read-only) Server-generated update timestamp.
347
+
348
+ | Property | Pattern | Type | Deprecated | Definition | Title/Description |
349
+ | ------------------------------------------------ | ------- | ------- | ---------- | ---------- | ----------------- |
350
+ | + [_seconds](#usedAt_anyOf_i0__seconds ) | No | integer | No | - | - |
351
+ | + [_nanoseconds](#usedAt_anyOf_i0__nanoseconds ) | No | integer | No | - | - |
352
+
353
+ ### <a name="usedAt_anyOf_i0__seconds"></a>15.1. Property `_seconds`
354
+
355
+ | | |
356
+ | ------------ | --------- |
357
+ | **Type** | `integer` |
358
+ | **Required** | Yes |
359
+
360
+ | Restrictions | |
361
+ | ------------ | ---------------------- |
362
+ | **Minimum** | &ge; -9007199254740991 |
363
+ | **Maximum** | &le; 9007199254740991 |
364
+
365
+ ### <a name="usedAt_anyOf_i0__nanoseconds"></a>15.2. Property `_nanoseconds`
366
+
367
+ | | |
368
+ | ------------ | --------- |
369
+ | **Type** | `integer` |
370
+ | **Required** | Yes |
371
+
372
+ | Restrictions | |
373
+ | ------------ | ---------------------- |
374
+ | **Minimum** | &ge; -9007199254740991 |
375
+ | **Maximum** | &le; 9007199254740991 |
376
+
377
+ :::warning Server-set
378
+ Do not include in write requests. This field is set exclusively by the server (Firestore trigger or Admin SDK). Clients that send it will have the value silently ignored or may receive a validation error.
379
+ :::
380
+
381
+ ## <a name="createdBy"></a>16. Property `createdBy`
382
+
383
+ | | |
384
+ | ------------ | ---------------- |
385
+ | **Type** | `string or null` |
386
+ | **Required** | No |
387
+
388
+ **Description:** (Immutable) FK → User/staff UID who created this ticket.
389
+
390
+ ----------------------------------------------------------------------------------------------------------------------------
391
+ Generated using [json-schema-for-humans](https://github.com/coveooss/json-schema-for-humans) on 2026-04-06 at 19:27:48 +0000
392
+
393
+ :::info Immutable
394
+ Set at creation only. This field cannot be modified after the document is created. Include it in CREATE payloads; omit it (or leave unchanged) in UPDATE payloads.
395
+ :::
396
+
397
+ ## Related Decisions
398
+
399
+ | Decision | Title |
400
+ |---|---|
401
+ | **D27** | Dashboard ticket scanning |
402
+ | **D28** | Firebase Event/Ticket triggers |
403
+ | **D29** | Ticket-to-customer linkage model |
404
+ | **D30** | Ticket tiers/types support timing |
405
+ | **D31** | LegacyTicketMapper deprecation gate |
@@ -0,0 +1,204 @@
1
+ ---
2
+ title: "Event & Ticket Triggers"
3
+ sidebar_label: "Event & Ticket Triggers"
4
+ sidebar_position: 2
5
+ ---
6
+
7
+ # Event & Ticket Triggers
8
+
9
+ > **Decision**: D28 (locked, deferred to Wave 4) — Add Firebase Event/Ticket triggers when Dashboard Event support (D26) is built. Not before — Events are Mobile-only today.
10
+ > **Wave**: 4 — Feature Parity.
11
+ > **Status**: Specified. **Do not implement until D26 (Dashboard Events) is in progress.**
12
+
13
+ These triggers maintain denormalized counters on [`Event`](/models/event) documents and send notifications on ticket lifecycle changes.
14
+
15
+ ---
16
+
17
+ ## `onTicketCreate`
18
+
19
+ Fires when a new [`Ticket`](/models/ticket) document is created.
20
+
21
+ **Path**: `companies/{companyId}/events/{eventId}/tickets/{ticketId}`
22
+ **Event**: `onCreate`
23
+
24
+ ### Behavior
25
+
26
+ 1. Atomically increment `Event.ticketsSold` on the parent event document.
27
+ 2. Check capacity: if `Event.maxTickets` is set and `ticketsSold >= maxTickets`, update `Event.status` to `COMPLETED` (sold out). This is an optimistic check — a secondary validation should run on ticket creation too.
28
+ 3. Send a confirmation notification to the customer (email/WhatsApp) if `Ticket.customerEmail` or `Ticket.customerPhone` is set.
29
+
30
+ ### Inputs
31
+
32
+ ```typescript
33
+ interface TicketCreateEvent {
34
+ data: Ticket; // the new ticket document
35
+ params: {
36
+ companyId: string;
37
+ eventId: string;
38
+ ticketId: string;
39
+ };
40
+ }
41
+ ```
42
+
43
+ ### Outputs
44
+
45
+ **Write 1** — Increment counter on parent Event:
46
+
47
+ ```typescript
48
+ // Atomic increment on: companies/{companyId}/events/{eventId}
49
+ {
50
+ ticketsSold: FieldValue.increment(1),
51
+ updatedAt: FieldValue.serverTimestamp(),
52
+ }
53
+ ```
54
+
55
+ **Write 2** (conditional) — Mark event as sold out:
56
+
57
+ ```typescript
58
+ // Only if ticketsSold >= maxTickets after increment
59
+ {
60
+ status: 'COMPLETED', // EventStatus.COMPLETED = sold out
61
+ updatedAt: FieldValue.serverTimestamp(),
62
+ }
63
+ ```
64
+
65
+ :::warning Capacity enforcement
66
+ Firestore has no native "max documents" guard. The trigger is the enforcement layer. For high-concurrency events, also add a Firestore Security Rule that rejects ticket writes when `ticketsSold >= maxTickets` using the current document value.
67
+ :::
68
+
69
+ ---
70
+
71
+ ## `onTicketUpdate`
72
+
73
+ Fires when an existing [`Ticket`](/models/ticket) document is updated.
74
+
75
+ **Path**: `companies/{companyId}/events/{eventId}/tickets/{ticketId}`
76
+ **Event**: `onUpdate`
77
+
78
+ ### Behavior
79
+
80
+ Compare `before.status` vs `after.status`:
81
+
82
+ | Transition | Action |
83
+ |---|---|
84
+ | any → `USED` | Increment `Event.ticketsUsed`. Set `Ticket.usedAt` and `Ticket.usedBy` (if not already set by client). |
85
+ | any → `CANCELLED` | Decrement `Event.ticketsSold`. If event was `COMPLETED` (sold out), revert to `ACTIVE`. |
86
+ | `CANCELLED` → `VALID` | Increment `Event.ticketsSold` (re-activation). |
87
+ | any other | No counter change. |
88
+
89
+ ### Inputs
90
+
91
+ ```typescript
92
+ interface TicketUpdateEvent {
93
+ data: {
94
+ before: Ticket; // document state before the update
95
+ after: Ticket; // document state after the update
96
+ };
97
+ params: {
98
+ companyId: string;
99
+ eventId: string;
100
+ ticketId: string;
101
+ };
102
+ }
103
+ ```
104
+
105
+ ### Outputs (status-dependent)
106
+
107
+ **On → `USED`**:
108
+
109
+ ```typescript
110
+ // Atomic update on Event
111
+ { ticketsUsed: FieldValue.increment(1), updatedAt: FieldValue.serverTimestamp() }
112
+
113
+ // Patch usedAt on Ticket if client didn't set it
114
+ { usedAt: FieldValue.serverTimestamp() }
115
+ ```
116
+
117
+ **On → `CANCELLED`**:
118
+
119
+ ```typescript
120
+ // Atomic update on Event
121
+ { ticketsSold: FieldValue.increment(-1), updatedAt: FieldValue.serverTimestamp() }
122
+
123
+ // Conditional: if event was COMPLETED (sold out), revert to ACTIVE
124
+ { status: 'ACTIVE', updatedAt: FieldValue.serverTimestamp() }
125
+ ```
126
+
127
+ ---
128
+
129
+ ## `onEventUpdate` (metrics)
130
+
131
+ Fires when an [`Event`](/models/event) document is updated.
132
+
133
+ **Path**: `companies/{companyId}/events/{eventId}`
134
+ **Event**: `onUpdate`
135
+
136
+ ### Behavior
137
+
138
+ This trigger handles notification side-effects of event-level status changes.
139
+
140
+ | Status transition | Action |
141
+ |---|---|
142
+ | any → `CANCELLED` | Notify all ticket holders (customerEmail / customerPhone on each Ticket subcollection). Set `Ticket.status = CANCELLED` for all non-cancelled tickets. |
143
+ | `DRAFT` → `ACTIVE` | Send event launch notification to opted-in customers (if notification list implemented). |
144
+
145
+ :::info Scope for Wave 4
146
+ The `CANCELLED` → notify-all-ticket-holders path is the critical one to implement. The `DRAFT → ACTIVE` notification is a nice-to-have.
147
+ :::
148
+
149
+ ---
150
+
151
+ ## BookingVersion trigger (`onBookingWrite`)
152
+
153
+ > **Decision**: D18 (locked) — Firebase owns server-side BookingVersion generation.
154
+ > **Wave**: 3 — Server-Side Logic.
155
+
156
+ Fires on any write to a [`Booking`](/models/booking) document.
157
+
158
+ **Path**: `companies/{companyId}/bookings/{bookingId}`
159
+ **Event**: `onWrite` (covers onCreate, onUpdate, onDelete)
160
+
161
+ ### Behavior
162
+
163
+ 1. Determine `changeType`: `CREATE` if `before` is null, `DELETE` if `after` is null, otherwise `UPDATE`.
164
+ 2. Compute `fieldsChanged` by diffing `before` and `after` document data (top-level keys for now; dot-notation for known nested objects like `bookingDates`).
165
+ 3. Write a new [`BookingVersion`](/models/booking-version) document to the `versions/` subcollection.
166
+
167
+ ### Inputs
168
+
169
+ ```typescript
170
+ interface BookingWriteEvent {
171
+ data: {
172
+ before: Booking | null; // null on CREATE
173
+ after: Booking | null; // null on DELETE
174
+ };
175
+ params: {
176
+ companyId: string;
177
+ bookingId: string;
178
+ };
179
+ }
180
+ ```
181
+
182
+ ### Output — Write 1: new BookingVersion document
183
+
184
+ ```typescript
185
+ // Written to: companies/{companyId}/bookings/{bookingId}/versions/{auto-id}
186
+ {
187
+ bookingId: params.bookingId,
188
+ companyId: params.companyId,
189
+ changeType: 'CREATE' | 'UPDATE' | 'DELETE',
190
+ changedAt: FieldValue.serverTimestamp(),
191
+ changedBy: context.auth?.uid ?? null,
192
+ changedByRole: detectRole(context), // 'dashboard' | 'mobile' | 'firebase' | 'cli' | 'unknown'
193
+ fieldsChanged: computeDiff(before, after), // null for CREATE
194
+ snapshot: after ?? before, // full document snapshot
195
+ }
196
+ ```
197
+
198
+ :::note changedByRole detection
199
+ `changedByRole` is inferred from the auth token claims in the write context:
200
+ - `custom_claims.role === 'staff'` → `'dashboard'`
201
+ - `custom_claims.role === 'mobile'` → `'mobile'`
202
+ - No auth (Admin SDK) → `'firebase'` or `'cli'`
203
+ - Otherwise → `'unknown'`
204
+ :::