@ingenx-io/valets-schema-mcp-server 0.1.4 → 0.1.6

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 (54) hide show
  1. package/data/docs/collections/firestore-paths.md +42 -8
  2. package/data/docs/enums/attention-status.md +1 -1
  3. package/data/docs/enums/booking-status.md +1 -1
  4. package/data/docs/enums/customer-payment-status.md +1 -1
  5. package/data/docs/enums/customer-payment-target-type.md +1 -1
  6. package/data/docs/enums/delivery-type.md +1 -1
  7. package/data/docs/enums/deployment-link-type.md +26 -0
  8. package/data/docs/enums/event-status.md +2 -2
  9. package/data/docs/enums/fulfillment-status.md +2 -2
  10. package/data/docs/enums/loyalty-transaction-type.md +2 -2
  11. package/data/docs/enums/order-status.md +2 -2
  12. package/data/docs/enums/payment-method.md +2 -2
  13. package/data/docs/enums/payment-proof-status.md +2 -2
  14. package/data/docs/enums/payment-status.md +2 -2
  15. package/data/docs/enums/pending-issue.md +2 -2
  16. package/data/docs/enums/return-status.md +2 -2
  17. package/data/docs/enums/session-status.md +2 -2
  18. package/data/docs/enums/site-status.md +24 -0
  19. package/data/docs/enums/stocktake-frequency.md +24 -0
  20. package/data/docs/enums/stocktake-item-status.md +24 -0
  21. package/data/docs/enums/stocktake-status.md +24 -0
  22. package/data/docs/enums/ticket-status.md +2 -2
  23. package/data/docs/index.md +14 -3
  24. package/data/docs/models/allowed-user.md +1 -1
  25. package/data/docs/models/analytics-backfill.md +398 -0
  26. package/data/docs/models/analytics-daily.md +351 -0
  27. package/data/docs/models/analytics-event.md +2 -2
  28. package/data/docs/models/analytics-hourly.md +372 -0
  29. package/data/docs/models/booking-version.md +2 -2
  30. package/data/docs/models/booking.md +2 -2
  31. package/data/docs/models/customer-payment-allocation.md +2 -2
  32. package/data/docs/models/customer-payment.md +2 -2
  33. package/data/docs/models/customer.md +2 -2
  34. package/data/docs/models/event.md +2 -2
  35. package/data/docs/models/loyalty-config.md +2 -2
  36. package/data/docs/models/loyalty-reward.md +2 -2
  37. package/data/docs/models/loyalty-status.md +2 -2
  38. package/data/docs/models/loyalty-transaction.md +2 -2
  39. package/data/docs/models/magic-link-request.md +2 -2
  40. package/data/docs/models/metrics-current.md +2 -2
  41. package/data/docs/models/metrics-daily.md +2 -2
  42. package/data/docs/models/metrics-monthly.md +2 -2
  43. package/data/docs/models/order-item.md +2 -2
  44. package/data/docs/models/order.md +248 -220
  45. package/data/docs/models/sale.md +2 -2
  46. package/data/docs/models/site-payment.md +2 -2
  47. package/data/docs/models/site.md +561 -0
  48. package/data/docs/models/stocktake-item.md +500 -0
  49. package/data/docs/models/stocktake.md +649 -0
  50. package/data/docs/models/ticket.md +2 -2
  51. package/data/static/llms.txt +309 -2
  52. package/data/static/openapi.yaml +972 -0
  53. package/data/static/schemas.json +1249 -77
  54. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "description": "@valets/schema \u2014 consolidated schema bundle",
4
- "generated": "2026-04-18T21:21:09.696794+00:00",
4
+ "generated": "2026-05-05T12:09:28.979656+00:00",
5
5
  "schemas": {
6
6
  "allowed-user": {
7
7
  "type": "object",
@@ -74,6 +74,292 @@
74
74
  "paidAt": "pai_ref123"
75
75
  }
76
76
  },
77
+ "analytics-backfill": {
78
+ "type": "object",
79
+ "properties": {
80
+ "id": {
81
+ "readOnly": true,
82
+ "description": "(Read-only) Firestore document ID, auto-generated.",
83
+ "type": [
84
+ "string",
85
+ "null"
86
+ ]
87
+ },
88
+ "companyId": {
89
+ "type": "string",
90
+ "x-immutable": true,
91
+ "description": "(Immutable) FK \u2192 Company document ID."
92
+ },
93
+ "siteId": {
94
+ "type": "string",
95
+ "x-immutable": true,
96
+ "description": "(Immutable) FK \u2192 Site document ID (D40)."
97
+ },
98
+ "status": {
99
+ "type": "string",
100
+ "enum": [
101
+ "pending",
102
+ "running",
103
+ "completed",
104
+ "failed",
105
+ "cancelled"
106
+ ],
107
+ "description": "Run status."
108
+ },
109
+ "from": {
110
+ "type": "string",
111
+ "x-immutable": true,
112
+ "description": "(Immutable) Inclusive start date (`YYYY-MM-DD`, UTC)."
113
+ },
114
+ "to": {
115
+ "type": "string",
116
+ "x-immutable": true,
117
+ "description": "(Immutable) Inclusive end date (`YYYY-MM-DD`, UTC)."
118
+ },
119
+ "dryRun": {
120
+ "type": "boolean",
121
+ "x-immutable": true,
122
+ "description": "(Immutable) When true, rollup writes are skipped \u2014 only source counts are returned for diffing."
123
+ },
124
+ "processedDates": {
125
+ "type": "array",
126
+ "items": {
127
+ "type": "string"
128
+ },
129
+ "description": "Ordered list of `YYYY-MM-DD` dates already processed by this run."
130
+ },
131
+ "errors": {
132
+ "type": "array",
133
+ "items": {
134
+ "type": "object",
135
+ "properties": {
136
+ "date": {
137
+ "type": "string",
138
+ "description": "`YYYY-MM-DD` date that failed."
139
+ },
140
+ "message": {
141
+ "type": "string",
142
+ "description": "Error message captured at failure."
143
+ },
144
+ "at": {
145
+ "$ref": "#/definitions/firestore-timestamp",
146
+ "description": "When the error occurred."
147
+ }
148
+ },
149
+ "required": [
150
+ "date",
151
+ "message",
152
+ "at"
153
+ ],
154
+ "additionalProperties": false
155
+ },
156
+ "description": "Per-date errors collected during the run."
157
+ },
158
+ "triggeredBy": {
159
+ "type": "string",
160
+ "x-immutable": true,
161
+ "description": "(Immutable) UID of the admin who triggered the backfill."
162
+ },
163
+ "startedAt": {
164
+ "$ref": "#/definitions/firestore-timestamp",
165
+ "description": "(Read-only) When the run actually started.",
166
+ "readOnly": true
167
+ },
168
+ "completedAt": {
169
+ "anyOf": [
170
+ {
171
+ "$ref": "#/definitions/firestore-timestamp"
172
+ },
173
+ {
174
+ "type": "null"
175
+ }
176
+ ],
177
+ "readOnly": true,
178
+ "description": "(Read-only) When the run reached a terminal status."
179
+ }
180
+ },
181
+ "required": [
182
+ "companyId",
183
+ "siteId",
184
+ "status",
185
+ "from",
186
+ "to",
187
+ "dryRun",
188
+ "processedDates",
189
+ "errors",
190
+ "triggeredBy",
191
+ "startedAt"
192
+ ],
193
+ "additionalProperties": false,
194
+ "description": "AnalyticsBackfill run (D42 / ING-304). Collection: companies/{companyId}/sites/{siteId}/analytics_backfills/{runId}. Tracks admin-triggered rollup backfill jobs; supports dry-run.",
195
+ "example": {
196
+ "id": null,
197
+ "companyId": "comp_xyz789",
198
+ "siteId": "sit_ref123",
199
+ "status": "pending",
200
+ "from": "from",
201
+ "to": "to",
202
+ "dryRun": true,
203
+ "processedDates": [
204
+ "example"
205
+ ],
206
+ "errors": [
207
+ {
208
+ "date": "2026-02-15",
209
+ "message": "message",
210
+ "at": "at"
211
+ }
212
+ ],
213
+ "triggeredBy": "triggeredBy",
214
+ "startedAt": "startedAt",
215
+ "completedAt": "completedAt"
216
+ }
217
+ },
218
+ "analytics-daily": {
219
+ "type": "object",
220
+ "properties": {
221
+ "totalEvents": {
222
+ "type": "integer",
223
+ "minimum": -9007199254740991,
224
+ "maximum": 9007199254740991,
225
+ "description": "Total event count within the bucket."
226
+ },
227
+ "pageViews": {
228
+ "type": "integer",
229
+ "minimum": -9007199254740991,
230
+ "maximum": 9007199254740991,
231
+ "description": "Count of `page_view` + `screen_view` events."
232
+ },
233
+ "sessions": {
234
+ "type": "integer",
235
+ "minimum": -9007199254740991,
236
+ "maximum": 9007199254740991,
237
+ "description": "Distinct session count within the bucket (by `sessionId`)."
238
+ },
239
+ "uniqueUsers": {
240
+ "type": "integer",
241
+ "minimum": -9007199254740991,
242
+ "maximum": 9007199254740991,
243
+ "description": "Distinct identified `userId` count. Anonymous sessions (userId=null) are excluded."
244
+ },
245
+ "anonymousSessions": {
246
+ "type": "integer",
247
+ "minimum": -9007199254740991,
248
+ "maximum": 9007199254740991,
249
+ "description": "Distinct session count where `userId` was null for the entire session."
250
+ },
251
+ "orders": {
252
+ "type": "integer",
253
+ "minimum": -9007199254740991,
254
+ "maximum": 9007199254740991,
255
+ "description": "Count of `order_submitted` events (or Order documents scoped to this site, D43) within the bucket."
256
+ },
257
+ "paymentsCompleted": {
258
+ "type": "integer",
259
+ "minimum": -9007199254740991,
260
+ "maximum": 9007199254740991,
261
+ "description": "Count of `payment_completed` events within the bucket."
262
+ },
263
+ "paymentsFailed": {
264
+ "type": "integer",
265
+ "minimum": -9007199254740991,
266
+ "maximum": 9007199254740991,
267
+ "description": "Count of `payment_failed` events within the bucket."
268
+ },
269
+ "errors": {
270
+ "type": "integer",
271
+ "minimum": -9007199254740991,
272
+ "maximum": 9007199254740991,
273
+ "description": "Count of `error_occurred` + `exception_caught` events within the bucket."
274
+ },
275
+ "eventCounts": {
276
+ "type": "object",
277
+ "propertyNames": {
278
+ "type": "string"
279
+ },
280
+ "additionalProperties": {
281
+ "type": "integer",
282
+ "minimum": -9007199254740991,
283
+ "maximum": 9007199254740991
284
+ },
285
+ "description": "Per-event-name counts \u2014 key is the event name (canonical or custom), value is the count within the bucket."
286
+ },
287
+ "id": {
288
+ "readOnly": true,
289
+ "description": "(Read-only) Firestore document ID = `YYYY-MM-DD`.",
290
+ "type": [
291
+ "string",
292
+ "null"
293
+ ]
294
+ },
295
+ "companyId": {
296
+ "type": "string",
297
+ "x-immutable": true,
298
+ "description": "(Immutable) FK \u2192 Company document ID."
299
+ },
300
+ "siteId": {
301
+ "type": "string",
302
+ "x-immutable": true,
303
+ "description": "(Immutable) FK \u2192 Site document ID (D40)."
304
+ },
305
+ "date": {
306
+ "type": "string",
307
+ "x-immutable": true,
308
+ "description": "(Immutable) `YYYY-MM-DD` UTC \u2014 matches document ID."
309
+ },
310
+ "computedAt": {
311
+ "$ref": "#/definitions/firestore-timestamp",
312
+ "description": "(Read-only) When this rollup was last (re)computed. Updated on every set/merge.",
313
+ "readOnly": true
314
+ },
315
+ "sourceEventCount": {
316
+ "readOnly": true,
317
+ "description": "(Read-only, Optional) Total source events scanned when producing this rollup. Useful for dry-run diffing.",
318
+ "type": [
319
+ "integer",
320
+ "null"
321
+ ],
322
+ "minimum": -9007199254740991,
323
+ "maximum": 9007199254740991
324
+ }
325
+ },
326
+ "required": [
327
+ "totalEvents",
328
+ "pageViews",
329
+ "sessions",
330
+ "uniqueUsers",
331
+ "anonymousSessions",
332
+ "orders",
333
+ "paymentsCompleted",
334
+ "paymentsFailed",
335
+ "errors",
336
+ "eventCounts",
337
+ "companyId",
338
+ "siteId",
339
+ "date",
340
+ "computedAt"
341
+ ],
342
+ "additionalProperties": false,
343
+ "description": "AnalyticsDaily rollup (D42 / ING-304). Collection: companies/{companyId}/sites/{siteId}/analytics_daily/{YYYY-MM-DD}. Idempotent set/merge \u2014 reruns overwrite. Fall back to raw analytics_events for uncovered slices.",
344
+ "example": {
345
+ "totalEvents": 0,
346
+ "pageViews": 0,
347
+ "sessions": 0,
348
+ "uniqueUsers": 0,
349
+ "anonymousSessions": 0,
350
+ "orders": 0,
351
+ "paymentsCompleted": 0,
352
+ "paymentsFailed": 0,
353
+ "errors": 0,
354
+ "eventCounts": {},
355
+ "id": null,
356
+ "companyId": "comp_xyz789",
357
+ "siteId": "sit_ref123",
358
+ "date": "2026-02-15",
359
+ "computedAt": "computedAt",
360
+ "sourceEventCount": null
361
+ }
362
+ },
77
363
  "analytics-event": {
78
364
  "type": "object",
79
365
  "properties": {
@@ -277,50 +563,204 @@
277
563
  "userTraits": null
278
564
  }
279
565
  },
280
- "attention-status": {
281
- "type": "string",
282
- "enum": [
283
- "ACTIVE",
284
- "STALE",
285
- "ON_HOLD",
286
- "ESCALATED"
287
- ],
288
- "description": "Operational attention status for Order and Booking (D39). Controls home-page queue assignment. Server-owned \u2014 set via triggers or callable fn only.",
289
- "x-note": "ACTIVE = normal urgent queue (default). STALE = idle past threshold, moved to review queue \u2014 excluded from pendingOrdersCount. ON_HOLD = intentionally paused by manager via callable fn. ESCALATED = ignored past second threshold.",
290
- "x-see": {
291
- "decisions": [
292
- "D39"
293
- ]
294
- },
295
- "x-when": "Set by server automation or callable fn. Mobile and Dashboard read this field; never write it directly. ON_HOLD is the only human-initiated transition \u2014 goes through a dedicated callable function.",
296
- "x-status": "proposed"
297
- },
298
- "booking": {
566
+ "analytics-hourly": {
299
567
  "type": "object",
300
568
  "properties": {
301
- "id": {
302
- "type": "string",
303
- "readOnly": true,
304
- "description": "(Read-only) Firestore document ID."
569
+ "totalEvents": {
570
+ "type": "integer",
571
+ "minimum": -9007199254740991,
572
+ "maximum": 9007199254740991,
573
+ "description": "Total event count within the bucket."
305
574
  },
306
- "uid": {
307
- "type": "string",
308
- "readOnly": true,
309
- "description": "(Read-only) Entity UID. Often mirrors id."
575
+ "pageViews": {
576
+ "type": "integer",
577
+ "minimum": -9007199254740991,
578
+ "maximum": 9007199254740991,
579
+ "description": "Count of `page_view` + `screen_view` events."
310
580
  },
311
- "companyId": {
312
- "x-immutable": true,
313
- "description": "(Immutable) FK \u2192 Company document ID. Note: optional in current schema \u2014 should be required (see ID consistency audit).",
581
+ "sessions": {
582
+ "type": "integer",
583
+ "minimum": -9007199254740991,
584
+ "maximum": 9007199254740991,
585
+ "description": "Distinct session count within the bucket (by `sessionId`)."
586
+ },
587
+ "uniqueUsers": {
588
+ "type": "integer",
589
+ "minimum": -9007199254740991,
590
+ "maximum": 9007199254740991,
591
+ "description": "Distinct identified `userId` count. Anonymous sessions (userId=null) are excluded."
592
+ },
593
+ "anonymousSessions": {
594
+ "type": "integer",
595
+ "minimum": -9007199254740991,
596
+ "maximum": 9007199254740991,
597
+ "description": "Distinct session count where `userId` was null for the entire session."
598
+ },
599
+ "orders": {
600
+ "type": "integer",
601
+ "minimum": -9007199254740991,
602
+ "maximum": 9007199254740991,
603
+ "description": "Count of `order_submitted` events (or Order documents scoped to this site, D43) within the bucket."
604
+ },
605
+ "paymentsCompleted": {
606
+ "type": "integer",
607
+ "minimum": -9007199254740991,
608
+ "maximum": 9007199254740991,
609
+ "description": "Count of `payment_completed` events within the bucket."
610
+ },
611
+ "paymentsFailed": {
612
+ "type": "integer",
613
+ "minimum": -9007199254740991,
614
+ "maximum": 9007199254740991,
615
+ "description": "Count of `payment_failed` events within the bucket."
616
+ },
617
+ "errors": {
618
+ "type": "integer",
619
+ "minimum": -9007199254740991,
620
+ "maximum": 9007199254740991,
621
+ "description": "Count of `error_occurred` + `exception_caught` events within the bucket."
622
+ },
623
+ "eventCounts": {
624
+ "type": "object",
625
+ "propertyNames": {
626
+ "type": "string"
627
+ },
628
+ "additionalProperties": {
629
+ "type": "integer",
630
+ "minimum": -9007199254740991,
631
+ "maximum": 9007199254740991
632
+ },
633
+ "description": "Per-event-name counts \u2014 key is the event name (canonical or custom), value is the count within the bucket."
634
+ },
635
+ "id": {
636
+ "readOnly": true,
637
+ "description": "(Read-only) Firestore document ID = `YYYY-MM-DD-HH`.",
314
638
  "type": [
315
639
  "string",
316
640
  "null"
317
641
  ]
318
642
  },
319
- "status": {
320
- "$ref": "#/definitions/booking-status",
321
- "description": "Booking lifecycle status. COMPLETED_MIXED = some sessions completed, others cancelled/no-show."
322
- },
323
- "totalAmount": {
643
+ "companyId": {
644
+ "type": "string",
645
+ "x-immutable": true,
646
+ "description": "(Immutable) FK \u2192 Company document ID."
647
+ },
648
+ "siteId": {
649
+ "type": "string",
650
+ "x-immutable": true,
651
+ "description": "(Immutable) FK \u2192 Site document ID (D40)."
652
+ },
653
+ "date": {
654
+ "type": "string",
655
+ "x-immutable": true,
656
+ "description": "(Immutable) `YYYY-MM-DD` UTC for the bucket day."
657
+ },
658
+ "hour": {
659
+ "type": "integer",
660
+ "minimum": -9007199254740991,
661
+ "maximum": 9007199254740991,
662
+ "x-immutable": true,
663
+ "description": "(Immutable) UTC hour of day, 0\u201323."
664
+ },
665
+ "computedAt": {
666
+ "$ref": "#/definitions/firestore-timestamp",
667
+ "description": "(Read-only) When this rollup was last (re)computed.",
668
+ "readOnly": true
669
+ },
670
+ "sourceEventCount": {
671
+ "readOnly": true,
672
+ "description": "(Read-only, Optional) Total source events scanned when producing this rollup.",
673
+ "type": [
674
+ "integer",
675
+ "null"
676
+ ],
677
+ "minimum": -9007199254740991,
678
+ "maximum": 9007199254740991
679
+ }
680
+ },
681
+ "required": [
682
+ "totalEvents",
683
+ "pageViews",
684
+ "sessions",
685
+ "uniqueUsers",
686
+ "anonymousSessions",
687
+ "orders",
688
+ "paymentsCompleted",
689
+ "paymentsFailed",
690
+ "errors",
691
+ "eventCounts",
692
+ "companyId",
693
+ "siteId",
694
+ "date",
695
+ "hour",
696
+ "computedAt"
697
+ ],
698
+ "additionalProperties": false,
699
+ "description": "AnalyticsHourly rollup (D42 / ING-304). Collection: companies/{companyId}/sites/{siteId}/analytics_hourly/{YYYY-MM-DD-HH}. Scheduled-CF writer; idempotent set/merge.",
700
+ "example": {
701
+ "totalEvents": 0,
702
+ "pageViews": 0,
703
+ "sessions": 0,
704
+ "uniqueUsers": 0,
705
+ "anonymousSessions": 0,
706
+ "orders": 0,
707
+ "paymentsCompleted": 0,
708
+ "paymentsFailed": 0,
709
+ "errors": 0,
710
+ "eventCounts": {},
711
+ "id": null,
712
+ "companyId": "comp_xyz789",
713
+ "siteId": "sit_ref123",
714
+ "date": "2026-02-15",
715
+ "hour": 0,
716
+ "computedAt": "computedAt",
717
+ "sourceEventCount": null
718
+ }
719
+ },
720
+ "attention-status": {
721
+ "type": "string",
722
+ "enum": [
723
+ "ACTIVE",
724
+ "STALE",
725
+ "ON_HOLD",
726
+ "ESCALATED"
727
+ ],
728
+ "description": "Operational attention status for Order and Booking (D39). Controls home-page queue assignment. Server-owned \u2014 set via triggers or callable fn only.",
729
+ "x-note": "ACTIVE = normal urgent queue (default). STALE = idle past threshold, moved to review queue \u2014 excluded from pendingOrdersCount. ON_HOLD = intentionally paused by manager via callable fn. ESCALATED = ignored past second threshold.",
730
+ "x-see": {
731
+ "decisions": [
732
+ "D39"
733
+ ]
734
+ },
735
+ "x-when": "Set by server automation or callable fn. Mobile and Dashboard read this field; never write it directly. ON_HOLD is the only human-initiated transition \u2014 goes through a dedicated callable function.",
736
+ "x-status": "proposed"
737
+ },
738
+ "booking": {
739
+ "type": "object",
740
+ "properties": {
741
+ "id": {
742
+ "type": "string",
743
+ "readOnly": true,
744
+ "description": "(Read-only) Firestore document ID."
745
+ },
746
+ "uid": {
747
+ "type": "string",
748
+ "readOnly": true,
749
+ "description": "(Read-only) Entity UID. Often mirrors id."
750
+ },
751
+ "companyId": {
752
+ "x-immutable": true,
753
+ "description": "(Immutable) FK \u2192 Company document ID. Note: optional in current schema \u2014 should be required (see ID consistency audit).",
754
+ "type": [
755
+ "string",
756
+ "null"
757
+ ]
758
+ },
759
+ "status": {
760
+ "$ref": "#/definitions/booking-status",
761
+ "description": "Booking lifecycle status. COMPLETED_MIXED = some sessions completed, others cancelled/no-show."
762
+ },
763
+ "totalAmount": {
324
764
  "type": "number",
325
765
  "x-note": "Booking uses `totalAmount`; Order uses `amount` for the equivalent field (D05 locked `amount` as canonical for orders). Pending cross-model alignment.",
326
766
  "x-see": {
@@ -1624,6 +2064,24 @@
1624
2064
  ],
1625
2065
  "description": "Fulfillment channel for an order. Determines whether the customer comes to the business (ON_SITE), collects their order themselves (PICK_UP), or receives a physical delivery (DELIVERY). Drives whether fulfillmentStatus is relevant."
1626
2066
  },
2067
+ "deployment-link-type": {
2068
+ "type": "string",
2069
+ "enum": [
2070
+ "web",
2071
+ "mobile",
2072
+ "pwa",
2073
+ "app-store",
2074
+ "play-store",
2075
+ "other"
2076
+ ],
2077
+ "description": "Category of a Site deployment link (D41). Used for UI icon/labelling and deep-link handling.",
2078
+ "x-note": "Lowercase by convention \u2014 deployment link types are display-oriented labels, not lifecycle states.",
2079
+ "x-see": {
2080
+ "decisions": [
2081
+ "D41"
2082
+ ]
2083
+ }
2084
+ },
1627
2085
  "event": {
1628
2086
  "type": "object",
1629
2087
  "properties": {
@@ -3037,6 +3495,21 @@
3037
3495
  "x-immutable": true,
3038
3496
  "description": "(Immutable) FK \u2192 Company document ID. Scopes all queries."
3039
3497
  },
3498
+ "siteId": {
3499
+ "x-immutable": true,
3500
+ "x-note": "D43 / ADR-003: optional site attribution. Absent/null means company-wide (legacy). When set, the order is attributed to a specific site for analytics and site-scoped dashboards. No migration \u2014 existing orders keep null. Flat path; Order continues to live at `companies/{companyId}/orders/{orderId}`.",
3501
+ "x-see": {
3502
+ "decisions": [
3503
+ "D43"
3504
+ ]
3505
+ },
3506
+ "x-when": "Set at order creation when the client knows which site the order originated from (e.g. SR Single, Lifesense). Leave null for legacy/company-wide orders. Composite index: (companyId, siteId, createdAt).",
3507
+ "description": "(Immutable, Optional) FK \u2192 Site document ID (D43 / ADR-003). null = company-wide.",
3508
+ "type": [
3509
+ "string",
3510
+ "null"
3511
+ ]
3512
+ },
3040
3513
  "orderNumber": {
3041
3514
  "type": "string",
3042
3515
  "readOnly": true,
@@ -3626,6 +4099,7 @@
3626
4099
  "id": "bk_abc123def456",
3627
4100
  "uid": "user_u8x92kqm",
3628
4101
  "companyId": "comp_xyz789",
4102
+ "siteId": null,
3629
4103
  "orderNumber": "ORD-2026-0042",
3630
4104
  "status": "status",
3631
4105
  "paymentStatus": "paymentStatus",
@@ -4121,7 +4595,7 @@
4121
4595
  ],
4122
4596
  "description": "Per-date/per-slot booking session status (D19). Dashboard is sole writer; Mobile is read-only."
4123
4597
  },
4124
- "site-payment": {
4598
+ "site": {
4125
4599
  "type": "object",
4126
4600
  "properties": {
4127
4601
  "id": {
@@ -4135,69 +4609,767 @@
4135
4609
  "companyId": {
4136
4610
  "type": "string",
4137
4611
  "x-immutable": true,
4138
- "description": "(Immutable) FK \u2192 Company document ID."
4612
+ "description": "(Immutable) FK \u2192 Company document ID. Scopes all site sub-collections."
4139
4613
  },
4140
- "siteId": {
4614
+ "name": {
4141
4615
  "type": "string",
4142
- "x-immutable": true,
4143
- "description": "(Immutable) FK \u2192 Site document ID (D40 sub-tenant scope)."
4616
+ "description": "Human-readable site name shown in dashboards."
4144
4617
  },
4145
- "contact": {
4146
- "type": "string",
4147
- "description": "Email or E.164 phone number. Matches AllowedUser.contact."
4618
+ "description": {
4619
+ "description": "Optional freeform description.",
4620
+ "type": [
4621
+ "string",
4622
+ "null"
4623
+ ]
4148
4624
  },
4149
- "sessionId": {
4150
- "type": "string",
4151
- "description": "Payment provider checkout session ID (e.g. Wave). Usable for dedup across dual writes."
4625
+ "status": {
4626
+ "$ref": "#/definitions/site-status",
4627
+ "description": "Lifecycle status (D41). Clients filter by ACTIVE.",
4628
+ "x-note": "ACTIVE = live and serving traffic. INACTIVE = intentionally disabled by operators. EXPIRED = past expiresAt (derived when isExpired is true; may also be stored explicitly). ARCHIVED = soft-deleted; retained for audit but not listed by default.",
4629
+ "x-see": {
4630
+ "decisions": [
4631
+ "D41"
4632
+ ]
4633
+ },
4634
+ "x-when": "Written by operators via dashboard or by server triggers (expiration). Clients filter by ACTIVE."
4152
4635
  },
4153
- "transactionId": {
4154
- "type": "string",
4155
- "description": "Payment provider transaction ID."
4636
+ "deploymentLinks": {
4637
+ "type": "array",
4638
+ "items": {
4639
+ "type": "object",
4640
+ "properties": {
4641
+ "label": {
4642
+ "type": "string",
4643
+ "description": "Human-readable label shown in dashboards (e.g. \"Production\", \"Staging\")."
4644
+ },
4645
+ "url": {
4646
+ "type": "string",
4647
+ "description": "Absolute URL or store link."
4648
+ },
4649
+ "type": {
4650
+ "$ref": "#/definitions/deployment-link-type",
4651
+ "description": "Link category \u2014 drives icon/handler selection.",
4652
+ "x-note": "Lowercase by convention \u2014 deployment link types are display-oriented labels, not lifecycle states.",
4653
+ "x-see": {
4654
+ "decisions": [
4655
+ "D41"
4656
+ ]
4657
+ }
4658
+ },
4659
+ "isPrimary": {
4660
+ "description": "If true, this link is used as the canonical deployment URL for the Site.",
4661
+ "type": "boolean"
4662
+ }
4663
+ },
4664
+ "required": [
4665
+ "label",
4666
+ "url",
4667
+ "type"
4668
+ ],
4669
+ "additionalProperties": false,
4670
+ "description": "Deployment link entry embedded on Site.deploymentLinks[] (D41)."
4671
+ },
4672
+ "description": "Ordered list of deployment URLs (web, mobile, PWA, store links)."
4156
4673
  },
4157
- "tier": {
4674
+ "expiresAt": {
4675
+ "anyOf": [
4676
+ {
4677
+ "$ref": "#/definitions/firestore-timestamp"
4678
+ },
4679
+ {
4680
+ "type": "null"
4681
+ }
4682
+ ],
4683
+ "description": "Optional expiration timestamp. When set and elapsed, `isExpired` flips true and status typically moves to EXPIRED."
4684
+ },
4685
+ "isExpired": {
4686
+ "readOnly": true,
4687
+ "description": "(Read-only) Derived \u2014 true when `expiresAt` is in the past. Maintained by server trigger.",
4688
+ "type": [
4689
+ "boolean",
4690
+ "null"
4691
+ ]
4692
+ },
4693
+ "createdAt": {
4694
+ "anyOf": [
4695
+ {
4696
+ "$ref": "#/definitions/firestore-timestamp"
4697
+ },
4698
+ {
4699
+ "type": "null"
4700
+ }
4701
+ ],
4702
+ "readOnly": true,
4703
+ "description": "(Read-only) Server-generated creation timestamp."
4704
+ },
4705
+ "updatedAt": {
4706
+ "anyOf": [
4707
+ {
4708
+ "$ref": "#/definitions/firestore-timestamp"
4709
+ },
4710
+ {
4711
+ "type": "null"
4712
+ }
4713
+ ],
4714
+ "readOnly": true,
4715
+ "description": "(Read-only) Server-generated update timestamp."
4716
+ },
4717
+ "createdBy": {
4158
4718
  "type": "string",
4159
- "description": "Access tier. Free string per site (ING-304 open question)."
4719
+ "x-immutable": true,
4720
+ "description": "(Immutable) FK \u2192 User/staff UID who created the site."
4160
4721
  },
4161
- "amount": {
4162
- "type": "number",
4163
- "description": "Amount paid. Generalized from amount_xof per D40 decision."
4722
+ "analyticsEnabled": {
4723
+ "type": "boolean",
4724
+ "description": "Feature flag \u2014 when false, clients should not emit analytics events for this site."
4164
4725
  },
4165
- "currency": {
4166
- "default": "XOF",
4167
- "description": "Currency code (ISO 4217). Defaults to XOF for legacy SR-Single parity.",
4168
- "type": "string"
4726
+ "lastAnalyticsSync": {
4727
+ "anyOf": [
4728
+ {
4729
+ "$ref": "#/definitions/firestore-timestamp"
4730
+ },
4731
+ {
4732
+ "type": "null"
4733
+ }
4734
+ ],
4735
+ "readOnly": true,
4736
+ "description": "(Read-only) Last time the analytics rollup pipeline refreshed `cachedMetrics`."
4169
4737
  },
4170
- "paidAt": {
4171
- "$ref": "#/definitions/firestore-timestamp",
4172
- "description": "RFC3339Nano UTC when payment was completed."
4738
+ "cachedMetrics": {
4739
+ "readOnly": true,
4740
+ "denormalized": true,
4741
+ "x-note": "Updated by the rollup pipeline (D42). Readers should treat this as a hint; authoritative counts live in analytics_daily/analytics_hourly.",
4742
+ "x-see": {
4743
+ "decisions": [
4744
+ "D41",
4745
+ "D42"
4746
+ ]
4747
+ },
4748
+ "description": "(Read-only, Denormalized) Cached metrics snapshot for quick dashboard rendering.",
4749
+ "type": [
4750
+ "object",
4751
+ "null"
4752
+ ],
4753
+ "properties": {
4754
+ "totalEvents": {
4755
+ "type": "integer",
4756
+ "minimum": -9007199254740991,
4757
+ "maximum": 9007199254740991,
4758
+ "description": "All-time event count cached on the Site."
4759
+ },
4760
+ "totalPageViews": {
4761
+ "type": "integer",
4762
+ "minimum": -9007199254740991,
4763
+ "maximum": 9007199254740991,
4764
+ "description": "All-time `page_view` + `screen_view` count."
4765
+ },
4766
+ "totalSessions": {
4767
+ "type": "integer",
4768
+ "minimum": -9007199254740991,
4769
+ "maximum": 9007199254740991,
4770
+ "description": "All-time distinct session count."
4771
+ },
4772
+ "totalOrders": {
4773
+ "type": "integer",
4774
+ "minimum": -9007199254740991,
4775
+ "maximum": 9007199254740991,
4776
+ "description": "All-time order count attributed to this site (via `Order.siteId`, D43)."
4777
+ },
4778
+ "lastEventAt": {
4779
+ "$ref": "#/definitions/firestore-timestamp",
4780
+ "description": "Timestamp of the most recent analytics event seen for this site."
4781
+ }
4782
+ },
4783
+ "required": [
4784
+ "totalEvents",
4785
+ "totalPageViews",
4786
+ "totalSessions",
4787
+ "totalOrders"
4788
+ ],
4789
+ "additionalProperties": false
4173
4790
  }
4174
4791
  },
4175
4792
  "required": [
4176
4793
  "companyId",
4177
- "siteId",
4178
- "contact",
4179
- "sessionId",
4180
- "transactionId",
4181
- "tier",
4182
- "amount",
4183
- "currency",
4184
- "paidAt"
4794
+ "name",
4795
+ "status",
4796
+ "deploymentLinks",
4797
+ "createdBy",
4798
+ "analyticsEnabled"
4185
4799
  ],
4186
4800
  "additionalProperties": false,
4187
- "description": "SitePayment model (D40 / ING-304). Collection: companies/{companyId}/sites/{siteId}/payments/{paymentId}. Immutable append-only transaction ledger. Distinct from AllowedUser (access state) and CustomerPayment (D22 \u2014 customer-level billing).",
4801
+ "description": "Site model (D41 / ING-304). Collection: companies/{companyId}/sites/{siteId}. Per-company product surface. Sub-collections (magic_link_requests, allowed_users, payments, analytics_events, analytics_daily, analytics_hourly, analytics_backfills) live under this document per D40/D42.",
4188
4802
  "example": {
4189
4803
  "id": null,
4190
4804
  "companyId": "comp_xyz789",
4191
- "siteId": "sit_ref123",
4192
- "contact": "contact",
4193
- "sessionId": "ses_ref123",
4194
- "transactionId": "tra_ref123",
4195
- "tier": "Gold",
4196
- "amount": 45000,
4805
+ "name": "Amadou Diallo",
4806
+ "description": null,
4807
+ "status": "status",
4808
+ "deploymentLinks": [
4809
+ {
4810
+ "label": "label",
4811
+ "url": "https://storage.example.com/url.jpg",
4812
+ "type": "phone"
4813
+ }
4814
+ ],
4815
+ "expiresAt": "expiresAt",
4816
+ "isExpired": null,
4817
+ "createdAt": "createdAt",
4818
+ "updatedAt": "updatedAt",
4819
+ "createdBy": "staff_k0f1",
4820
+ "analyticsEnabled": true,
4821
+ "lastAnalyticsSync": "lastAnalyticsSync",
4822
+ "cachedMetrics": null
4823
+ }
4824
+ },
4825
+ "site-payment": {
4826
+ "type": "object",
4827
+ "properties": {
4828
+ "id": {
4829
+ "readOnly": true,
4830
+ "description": "(Read-only) Firestore document ID, auto-generated.",
4831
+ "type": [
4832
+ "string",
4833
+ "null"
4834
+ ]
4835
+ },
4836
+ "companyId": {
4837
+ "type": "string",
4838
+ "x-immutable": true,
4839
+ "description": "(Immutable) FK \u2192 Company document ID."
4840
+ },
4841
+ "siteId": {
4842
+ "type": "string",
4843
+ "x-immutable": true,
4844
+ "description": "(Immutable) FK \u2192 Site document ID (D40 sub-tenant scope)."
4845
+ },
4846
+ "contact": {
4847
+ "type": "string",
4848
+ "description": "Email or E.164 phone number. Matches AllowedUser.contact."
4849
+ },
4850
+ "sessionId": {
4851
+ "type": "string",
4852
+ "description": "Payment provider checkout session ID (e.g. Wave). Usable for dedup across dual writes."
4853
+ },
4854
+ "transactionId": {
4855
+ "type": "string",
4856
+ "description": "Payment provider transaction ID."
4857
+ },
4858
+ "tier": {
4859
+ "type": "string",
4860
+ "description": "Access tier. Free string per site (ING-304 open question)."
4861
+ },
4862
+ "amount": {
4863
+ "type": "number",
4864
+ "description": "Amount paid. Generalized from amount_xof per D40 decision."
4865
+ },
4866
+ "currency": {
4867
+ "default": "XOF",
4868
+ "description": "Currency code (ISO 4217). Defaults to XOF for legacy SR-Single parity.",
4869
+ "type": "string"
4870
+ },
4871
+ "paidAt": {
4872
+ "$ref": "#/definitions/firestore-timestamp",
4873
+ "description": "RFC3339Nano UTC when payment was completed."
4874
+ }
4875
+ },
4876
+ "required": [
4877
+ "companyId",
4878
+ "siteId",
4879
+ "contact",
4880
+ "sessionId",
4881
+ "transactionId",
4882
+ "tier",
4883
+ "amount",
4884
+ "currency",
4885
+ "paidAt"
4886
+ ],
4887
+ "additionalProperties": false,
4888
+ "description": "SitePayment model (D40 / ING-304). Collection: companies/{companyId}/sites/{siteId}/payments/{paymentId}. Immutable append-only transaction ledger. Distinct from AllowedUser (access state) and CustomerPayment (D22 \u2014 customer-level billing).",
4889
+ "example": {
4890
+ "id": null,
4891
+ "companyId": "comp_xyz789",
4892
+ "siteId": "sit_ref123",
4893
+ "contact": "contact",
4894
+ "sessionId": "ses_ref123",
4895
+ "transactionId": "tra_ref123",
4896
+ "tier": "Gold",
4897
+ "amount": 45000,
4197
4898
  "currency": "XOF",
4198
4899
  "paidAt": "pai_ref123"
4199
4900
  }
4200
4901
  },
4902
+ "site-status": {
4903
+ "type": "string",
4904
+ "enum": [
4905
+ "ACTIVE",
4906
+ "INACTIVE",
4907
+ "EXPIRED",
4908
+ "ARCHIVED"
4909
+ ],
4910
+ "description": "Lifecycle status for a Site (D41). Drives whether the site is reachable and whether analytics/payments flow.",
4911
+ "x-note": "ACTIVE = live and serving traffic. INACTIVE = intentionally disabled by operators. EXPIRED = past expiresAt (derived when isExpired is true; may also be stored explicitly). ARCHIVED = soft-deleted; retained for audit but not listed by default.",
4912
+ "x-see": {
4913
+ "decisions": [
4914
+ "D41"
4915
+ ]
4916
+ },
4917
+ "x-when": "Written by operators via dashboard or by server triggers (expiration). Clients filter by ACTIVE."
4918
+ },
4919
+ "stocktake": {
4920
+ "type": "object",
4921
+ "properties": {
4922
+ "id": {
4923
+ "readOnly": true,
4924
+ "description": "(Read-only) Firestore document ID, auto-generated.",
4925
+ "type": [
4926
+ "string",
4927
+ "null"
4928
+ ]
4929
+ },
4930
+ "companyId": {
4931
+ "type": "string",
4932
+ "x-immutable": true,
4933
+ "description": "(Immutable) FK \u2192 Company document ID. Tenant scope."
4934
+ },
4935
+ "stocktakeNumber": {
4936
+ "type": "string",
4937
+ "description": "Human-readable session number, e.g. \"STK-2026-001\". Embedded as `referenceNumber` on the StockMovement rows the session emits, so the ledger remains traceable to the originating stocktake."
4938
+ },
4939
+ "stocktakeDate": {
4940
+ "$ref": "#/definitions/firestore-timestamp",
4941
+ "description": "Effective date of the count. Distinct from createdAt; can be backdated.",
4942
+ "x-note": "The date the count is FOR. Can be backdated (stocktakeDate < createdAt) \u2014 analytics buckets by stocktakeDate for \"what period was counted\" and by completedAt for \"when adjustments hit the ledger\" (GH#29 Q24)."
4943
+ },
4944
+ "warehouseId": {
4945
+ "description": "FK \u2192 Warehouse document ID. Null = whole-company stocktake (no warehouse partition).",
4946
+ "anyOf": [
4947
+ {
4948
+ "type": "string"
4949
+ },
4950
+ {
4951
+ "type": "null"
4952
+ }
4953
+ ]
4954
+ },
4955
+ "frequency": {
4956
+ "anyOf": [
4957
+ {
4958
+ "$ref": "#/definitions/stocktake-frequency"
4959
+ },
4960
+ {
4961
+ "type": "null"
4962
+ }
4963
+ ],
4964
+ "description": "Recurrence cadence. Optional \u2014 leave unset for ad-hoc sessions, or set to indicate this stocktake is part of a recurring schedule.",
4965
+ "x-note": "Optional today (dashboard does not currently track recurrence). Frequency is a property of the record, not a separate type \u2014 a daily stocktake is a Stocktake with frequency=DAILY, not a distinct DailyStocktake model. If the session was emitted from a recurring schedule, set Stocktake.scheduledStocktakeId; ad-hoc sessions leave it null.",
4966
+ "x-see": {
4967
+ "issues": [
4968
+ "gh#29"
4969
+ ]
4970
+ }
4971
+ },
4972
+ "scheduledStocktakeId": {
4973
+ "description": "FK to a recurring stocktake schedule, if this session was emitted from one. Null for ad-hoc sessions.",
4974
+ "anyOf": [
4975
+ {
4976
+ "type": "string"
4977
+ },
4978
+ {
4979
+ "type": "null"
4980
+ }
4981
+ ]
4982
+ },
4983
+ "status": {
4984
+ "$ref": "#/definitions/stocktake-status",
4985
+ "description": "Lifecycle status. Transitions PENDING \u2192 IN_PROGRESS \u2192 COMPLETED are server-owned (GH#29 \u00a73 / Q25).",
4986
+ "x-note": "PENDING = session created, no items counted yet. IN_PROGRESS = at least one item has been counted. COMPLETED = all items counted and the session was closed; on this transition the server emits one StockMovement(type=INVENTORY) per non-zero delta. CANCELLED = session abandoned without applying adjustments. Status transitions PENDING \u2192 IN_PROGRESS \u2192 COMPLETED are server-owned (Q25) \u2014 clients PATCH item status; the server transitions session status to avoid the concurrent-edit race documented in stockService.ts:1671.",
4987
+ "x-see": {
4988
+ "issues": [
4989
+ "gh#29"
4990
+ ]
4991
+ },
4992
+ "x-when": "Server-managed. Clients write item updates; the trigger transitions session status."
4993
+ },
4994
+ "adjustmentsApplied": {
4995
+ "type": "boolean",
4996
+ "x-note": "Records whether the COMPLETED transition actually mutated stock or was record-only. The dashboard's completeInventory(applyAdjustments) flag was not persisted; this field closes that gap (GH#29 audit Q22).",
4997
+ "description": "True if completing the session actually emitted StockMovement adjustments. False for record-only counts."
4998
+ },
4999
+ "notes": {
5000
+ "description": "Free-text session-level notes.",
5001
+ "anyOf": [
5002
+ {
5003
+ "type": "string"
5004
+ },
5005
+ {
5006
+ "type": "null"
5007
+ }
5008
+ ]
5009
+ },
5010
+ "totalItemsCount": {
5011
+ "type": "integer",
5012
+ "minimum": -9007199254740991,
5013
+ "maximum": 9007199254740991,
5014
+ "readOnly": true,
5015
+ "x-note": "Server-derived. Recomputed by trigger on each StocktakeItem write; not safe to write client-side under concurrent edits (GH#29 audit Q23).",
5016
+ "description": "(Read-only) Total number of StocktakeItem rows under this session."
5017
+ },
5018
+ "totalDiscrepancies": {
5019
+ "type": "integer",
5020
+ "minimum": -9007199254740991,
5021
+ "maximum": 9007199254740991,
5022
+ "readOnly": true,
5023
+ "x-note": "Server-derived = sum of |item.delta| across COUNTED items. Updated by trigger.",
5024
+ "description": "(Read-only) Sum of absolute deltas across counted items."
5025
+ },
5026
+ "totalDeltaValue": {
5027
+ "type": "number",
5028
+ "readOnly": true,
5029
+ "x-note": "Server-derived = sum of (item.delta \u00d7 item.costPerUnit) across COUNTED items, in XOF integer minor units (D18). Updated by trigger.",
5030
+ "description": "(Read-only) Net valuation impact of the count, in XOF integer minor units."
5031
+ },
5032
+ "startedAt": {
5033
+ "$ref": "#/definitions/firestore-timestamp",
5034
+ "description": "When the counting session physically opened. Typically equal to createdAt for ad-hoc sessions."
5035
+ },
5036
+ "completedAt": {
5037
+ "anyOf": [
5038
+ {
5039
+ "$ref": "#/definitions/firestore-timestamp"
5040
+ },
5041
+ {
5042
+ "type": "null"
5043
+ }
5044
+ ],
5045
+ "readOnly": true,
5046
+ "x-note": "Server-set on transition to COMPLETED. Distinct from stocktakeDate (the period counted) \u2014 analytics buckets by completedAt to answer \"when did the ledger hit?\".",
5047
+ "description": "(Read-only) When the session transitioned to COMPLETED."
5048
+ },
5049
+ "createdAt": {
5050
+ "anyOf": [
5051
+ {
5052
+ "$ref": "#/definitions/firestore-timestamp"
5053
+ },
5054
+ {
5055
+ "type": "null"
5056
+ }
5057
+ ],
5058
+ "readOnly": true,
5059
+ "description": "(Read-only) Server-generated creation timestamp."
5060
+ },
5061
+ "createdBy": {
5062
+ "type": "string",
5063
+ "x-immutable": true,
5064
+ "description": "(Immutable) FK \u2192 User UID who created the session."
5065
+ },
5066
+ "createdByName": {
5067
+ "type": "string",
5068
+ "denormalized": true,
5069
+ "x-note": "Read-time hint snapshot of the creator's display name (GH#29 \u00a73 / Q10).",
5070
+ "description": "(Denormalized) Snapshot of the creator's display name at session creation."
5071
+ },
5072
+ "completedBy": {
5073
+ "readOnly": true,
5074
+ "description": "(Read-only) FK \u2192 User UID who completed the session, set by server on COMPLETED.",
5075
+ "anyOf": [
5076
+ {
5077
+ "type": "string"
5078
+ },
5079
+ {
5080
+ "type": "null"
5081
+ }
5082
+ ]
5083
+ },
5084
+ "completedByName": {
5085
+ "denormalized": true,
5086
+ "x-note": "Read-time hint snapshot of the completer's display name (GH#29 \u00a73 / Q10).",
5087
+ "description": "(Denormalized) Snapshot of the completer's display name.",
5088
+ "anyOf": [
5089
+ {
5090
+ "type": "string"
5091
+ },
5092
+ {
5093
+ "type": "null"
5094
+ }
5095
+ ]
5096
+ },
5097
+ "updatedAt": {
5098
+ "anyOf": [
5099
+ {
5100
+ "$ref": "#/definitions/firestore-timestamp"
5101
+ },
5102
+ {
5103
+ "type": "null"
5104
+ }
5105
+ ],
5106
+ "readOnly": true,
5107
+ "description": "(Read-only) Server-generated update timestamp."
5108
+ }
5109
+ },
5110
+ "required": [
5111
+ "companyId",
5112
+ "stocktakeNumber",
5113
+ "stocktakeDate",
5114
+ "status",
5115
+ "adjustmentsApplied",
5116
+ "totalItemsCount",
5117
+ "totalDiscrepancies",
5118
+ "totalDeltaValue",
5119
+ "startedAt",
5120
+ "createdBy",
5121
+ "createdByName"
5122
+ ],
5123
+ "additionalProperties": false,
5124
+ "description": "Stocktake (GH#29 \u00a74). Collection: companies/{companyId}/stocktakes/{stocktakeId}. A recorded session of physically counting all SKUs at a point in time. Replaces the dashboard's Inventory model; items live in a sub-collection (replaces InventoryItem[] embedded array); InventoryEvolution is dropped \u2014 derivable from the sub-collection. Status transitions and aggregate fields (totalItemsCount/Discrepancies/totalDeltaValue) are server-owned per GH#29 \u00a73.",
5125
+ "example": {
5126
+ "id": null,
5127
+ "companyId": "comp_xyz789",
5128
+ "stocktakeNumber": "stocktakeNumber",
5129
+ "stocktakeDate": "stocktakeDate",
5130
+ "warehouseId": "war_ref123",
5131
+ "frequency": "frequency",
5132
+ "scheduledStocktakeId": "sch_ref123",
5133
+ "status": "status",
5134
+ "adjustmentsApplied": true,
5135
+ "notes": "VIP customer, handle with care",
5136
+ "totalItemsCount": 2,
5137
+ "totalDiscrepancies": 0,
5138
+ "totalDeltaValue": 0,
5139
+ "startedAt": "startedAt",
5140
+ "completedAt": "completedAt",
5141
+ "createdAt": "createdAt",
5142
+ "createdBy": "staff_k0f1",
5143
+ "createdByName": "Kofi Mensah",
5144
+ "completedBy": "completedBy",
5145
+ "completedByName": "completedByName",
5146
+ "updatedAt": "updatedAt"
5147
+ }
5148
+ },
5149
+ "stocktake-frequency": {
5150
+ "type": "string",
5151
+ "enum": [
5152
+ "DAILY",
5153
+ "WEEKLY",
5154
+ "MONTHLY",
5155
+ "AD_HOC"
5156
+ ],
5157
+ "description": "Recurrence cadence of a Stocktake session (GH#29 \u00a74).",
5158
+ "x-note": "Optional today (dashboard does not currently track recurrence). Frequency is a property of the record, not a separate type \u2014 a daily stocktake is a Stocktake with frequency=DAILY, not a distinct DailyStocktake model. If the session was emitted from a recurring schedule, set Stocktake.scheduledStocktakeId; ad-hoc sessions leave it null.",
5159
+ "x-see": {
5160
+ "issues": [
5161
+ "gh#29"
5162
+ ]
5163
+ }
5164
+ },
5165
+ "stocktake-item": {
5166
+ "type": "object",
5167
+ "properties": {
5168
+ "stockItemId": {
5169
+ "type": "string",
5170
+ "description": "FK \u2192 StockItem document ID. Doubles as the item document ID."
5171
+ },
5172
+ "stockItemName": {
5173
+ "type": "string",
5174
+ "denormalized": true,
5175
+ "x-note": "Read-time hint snapshot of StockItem.name. Authoritative value lives at the StockItem record; do not refresh on rename \u2014 the historical name at session time is what the auditor wrote (GH#29 \u00a73 / Q10).",
5176
+ "description": "(Denormalized) Snapshot of StockItem.name at session time."
5177
+ },
5178
+ "theoreticalQuantity": {
5179
+ "type": "number",
5180
+ "x-immutable": true,
5181
+ "description": "(Immutable) System belief at session start, snapshotted when this item row was created."
5182
+ },
5183
+ "actualQuantity": {
5184
+ "description": "Physical count entered by the counter. Null until counted; set when the line moves to status COUNTED.",
5185
+ "anyOf": [
5186
+ {
5187
+ "type": "number"
5188
+ },
5189
+ {
5190
+ "type": "null"
5191
+ }
5192
+ ]
5193
+ },
5194
+ "delta": {
5195
+ "type": "number",
5196
+ "readOnly": true,
5197
+ "x-note": "Server-derived = actualQuantity \u2212 theoreticalQuantity. Recomputed on every actualQuantity write; null/zero before counting.",
5198
+ "description": "(Read-only) Signed difference between actual and theoretical. Drives the StockMovement(type=INVENTORY) emitted on session COMPLETED."
5199
+ },
5200
+ "unit": {
5201
+ "type": "string",
5202
+ "x-immutable": true,
5203
+ "description": "(Immutable) Snapshot of StockItem.unit at session start. Locks the unit against later catalog changes."
5204
+ },
5205
+ "costPerUnit": {
5206
+ "type": "number",
5207
+ "x-immutable": true,
5208
+ "x-note": "Snapshot at session time \u2014 locks valuation against future cost changes. Lock to XOF integer minor units per D18 (GH#29 Q16).",
5209
+ "description": "(Immutable) Snapshot of StockItem.costPerUnit at session start, in XOF integer minor units."
5210
+ },
5211
+ "totalValue": {
5212
+ "type": "number",
5213
+ "readOnly": true,
5214
+ "x-note": "Server-derived = actualQuantity \u00d7 costPerUnit. Recomputed when actualQuantity is written.",
5215
+ "description": "(Read-only) Per-line valuation in XOF integer minor units."
5216
+ },
5217
+ "status": {
5218
+ "$ref": "#/definitions/stocktake-item-status",
5219
+ "description": "Per-line workflow status. Server transitions ADJUSTED on parent COMPLETED.",
5220
+ "x-note": "PENDING = line not yet counted. COUNTED = a counter recorded actualQuantity. VERIFIED = a second pair of eyes signed off (verifiedBy fields populated; Q21 \u2014 kept nullable until tenants opt into stricter QA via policy). ADJUSTED = the resulting StockMovement was emitted (server-set on Stocktake.status \u2192 COMPLETED).",
5221
+ "x-see": {
5222
+ "issues": [
5223
+ "gh#29"
5224
+ ]
5225
+ }
5226
+ },
5227
+ "notes": {
5228
+ "description": "Optional per-line note from the counter (e.g. \"damaged carton, 3 unsellable\").",
5229
+ "anyOf": [
5230
+ {
5231
+ "type": "string"
5232
+ },
5233
+ {
5234
+ "type": "null"
5235
+ }
5236
+ ]
5237
+ },
5238
+ "countedBy": {
5239
+ "description": "UID of the staff member who entered actualQuantity.",
5240
+ "anyOf": [
5241
+ {
5242
+ "type": "string"
5243
+ },
5244
+ {
5245
+ "type": "null"
5246
+ }
5247
+ ]
5248
+ },
5249
+ "countedByName": {
5250
+ "denormalized": true,
5251
+ "x-note": "Read-time hint snapshot of the counter's display name (GH#29 \u00a73 / Q10).",
5252
+ "description": "(Denormalized) Snapshot of the counter's display name at count time.",
5253
+ "anyOf": [
5254
+ {
5255
+ "type": "string"
5256
+ },
5257
+ {
5258
+ "type": "null"
5259
+ }
5260
+ ]
5261
+ },
5262
+ "countedAt": {
5263
+ "anyOf": [
5264
+ {
5265
+ "$ref": "#/definitions/firestore-timestamp"
5266
+ },
5267
+ {
5268
+ "type": "null"
5269
+ }
5270
+ ],
5271
+ "description": "When the line was counted."
5272
+ },
5273
+ "verifiedBy": {
5274
+ "description": "UID of a second staff member who verified the count. Nullable per GH#29 Q21.",
5275
+ "anyOf": [
5276
+ {
5277
+ "type": "string"
5278
+ },
5279
+ {
5280
+ "type": "null"
5281
+ }
5282
+ ]
5283
+ },
5284
+ "verifiedByName": {
5285
+ "denormalized": true,
5286
+ "x-note": "Read-time hint snapshot of the verifier's display name (GH#29 \u00a73 / Q10).",
5287
+ "description": "(Denormalized) Snapshot of the verifier's display name.",
5288
+ "anyOf": [
5289
+ {
5290
+ "type": "string"
5291
+ },
5292
+ {
5293
+ "type": "null"
5294
+ }
5295
+ ]
5296
+ },
5297
+ "verifiedAt": {
5298
+ "anyOf": [
5299
+ {
5300
+ "$ref": "#/definitions/firestore-timestamp"
5301
+ },
5302
+ {
5303
+ "type": "null"
5304
+ }
5305
+ ],
5306
+ "description": "When the line was verified, if a second-eye flow was used."
5307
+ }
5308
+ },
5309
+ "required": [
5310
+ "stockItemId",
5311
+ "stockItemName",
5312
+ "theoreticalQuantity",
5313
+ "delta",
5314
+ "unit",
5315
+ "costPerUnit",
5316
+ "totalValue",
5317
+ "status"
5318
+ ],
5319
+ "additionalProperties": false,
5320
+ "description": "StocktakeItem (GH#29 \u00a74). Sub-collection path: companies/{companyId}/stocktakes/{stocktakeId}/items/{stockItemId}. Replaces the embedded `InventoryItem[]` array on the dashboard model. Server emits one StockMovement(type=INVENTORY) per non-zero delta when the parent Stocktake transitions to COMPLETED.",
5321
+ "example": {
5322
+ "stockItemId": "sto_ref123",
5323
+ "stockItemName": "stockItemName",
5324
+ "theoreticalQuantity": 2,
5325
+ "actualQuantity": "actualQuantity",
5326
+ "delta": 0,
5327
+ "unit": "unit",
5328
+ "costPerUnit": 15000,
5329
+ "totalValue": 0,
5330
+ "status": "status",
5331
+ "notes": "VIP customer, handle with care",
5332
+ "countedBy": "countedBy",
5333
+ "countedByName": "countedByName",
5334
+ "countedAt": "countedAt",
5335
+ "verifiedBy": "verifiedBy",
5336
+ "verifiedByName": "verifiedByName",
5337
+ "verifiedAt": "verifiedAt"
5338
+ }
5339
+ },
5340
+ "stocktake-item-status": {
5341
+ "type": "string",
5342
+ "enum": [
5343
+ "PENDING",
5344
+ "COUNTED",
5345
+ "VERIFIED",
5346
+ "ADJUSTED"
5347
+ ],
5348
+ "description": "Per-line workflow status inside a Stocktake (GH#29 \u00a74, audit Q21).",
5349
+ "x-note": "PENDING = line not yet counted. COUNTED = a counter recorded actualQuantity. VERIFIED = a second pair of eyes signed off (verifiedBy fields populated; Q21 \u2014 kept nullable until tenants opt into stricter QA via policy). ADJUSTED = the resulting StockMovement was emitted (server-set on Stocktake.status \u2192 COMPLETED).",
5350
+ "x-see": {
5351
+ "issues": [
5352
+ "gh#29"
5353
+ ]
5354
+ }
5355
+ },
5356
+ "stocktake-status": {
5357
+ "type": "string",
5358
+ "enum": [
5359
+ "PENDING",
5360
+ "IN_PROGRESS",
5361
+ "COMPLETED",
5362
+ "CANCELLED"
5363
+ ],
5364
+ "description": "Lifecycle status of a Stocktake session (GH#29 \u00a73, \u00a74).",
5365
+ "x-note": "PENDING = session created, no items counted yet. IN_PROGRESS = at least one item has been counted. COMPLETED = all items counted and the session was closed; on this transition the server emits one StockMovement(type=INVENTORY) per non-zero delta. CANCELLED = session abandoned without applying adjustments. Status transitions PENDING \u2192 IN_PROGRESS \u2192 COMPLETED are server-owned (Q25) \u2014 clients PATCH item status; the server transitions session status to avoid the concurrent-edit race documented in stockService.ts:1671.",
5366
+ "x-see": {
5367
+ "issues": [
5368
+ "gh#29"
5369
+ ]
5370
+ },
5371
+ "x-when": "Server-managed. Clients write item updates; the trigger transitions session status."
5372
+ },
4201
5373
  "ticket": {
4202
5374
  "type": "object",
4203
5375
  "properties": {