@happyvertical/smrt-inventory 0.57.17 → 0.57.19

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 (45) hide show
  1. package/AGENTS.md +19 -1
  2. package/README.md +32 -2
  3. package/dist/collections/LotCollection.d.ts +22 -0
  4. package/dist/collections/LotCollection.d.ts.map +1 -0
  5. package/dist/collections/LotCorrectionCollection.d.ts +12 -0
  6. package/dist/collections/LotCorrectionCollection.d.ts.map +1 -0
  7. package/dist/collections/LotDocumentCollection.d.ts +12 -0
  8. package/dist/collections/LotDocumentCollection.d.ts.map +1 -0
  9. package/dist/collections/LotStockLevelCollection.d.ts +21 -0
  10. package/dist/collections/LotStockLevelCollection.d.ts.map +1 -0
  11. package/dist/collections/LotUsageCollection.d.ts +18 -0
  12. package/dist/collections/LotUsageCollection.d.ts.map +1 -0
  13. package/dist/collections/index.d.ts +5 -0
  14. package/dist/collections/index.d.ts.map +1 -1
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1168 -31
  18. package/dist/index.js.map +1 -1
  19. package/dist/manifest.json +2921 -288
  20. package/dist/models/Lot.d.ts +96 -0
  21. package/dist/models/Lot.d.ts.map +1 -0
  22. package/dist/models/LotCorrection.d.ts +55 -0
  23. package/dist/models/LotCorrection.d.ts.map +1 -0
  24. package/dist/models/LotDocument.d.ts +45 -0
  25. package/dist/models/LotDocument.d.ts.map +1 -0
  26. package/dist/models/LotStockLevel.d.ts +43 -0
  27. package/dist/models/LotStockLevel.d.ts.map +1 -0
  28. package/dist/models/LotUsage.d.ts +64 -0
  29. package/dist/models/LotUsage.d.ts.map +1 -0
  30. package/dist/models/StockMovement.d.ts +7 -0
  31. package/dist/models/StockMovement.d.ts.map +1 -1
  32. package/dist/models/index.d.ts +5 -0
  33. package/dist/models/index.d.ts.map +1 -1
  34. package/dist/services/LotService.d.ts +258 -0
  35. package/dist/services/LotService.d.ts.map +1 -0
  36. package/dist/services/StockService.d.ts +25 -2
  37. package/dist/services/StockService.d.ts.map +1 -1
  38. package/dist/services/index.d.ts +2 -0
  39. package/dist/services/index.d.ts.map +1 -1
  40. package/dist/services/lot-errors.d.ts +44 -0
  41. package/dist/services/lot-errors.d.ts.map +1 -0
  42. package/dist/smrt-knowledge.json +1333 -76
  43. package/dist/types.d.ts +6 -0
  44. package/dist/types.d.ts.map +1 -1
  45. package/package.json +8 -8
@@ -3,13 +3,13 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-inventory",
6
- "packageVersion": "0.57.17",
6
+ "packageVersion": "0.57.19",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "89c77cddd4a5f37b270505d1831f7c1ac37a4d516840067fcf0eb8a8c3abafd9",
11
- "packageJson": "3865e5594d72f0a47e83859abe79377a37e6f1dd418d326eb407df7121bb7ab3",
12
- "agents": "382e62dd5529521207b202de207956f67fdce6c894b8964ef4e58c85a9397d0b"
10
+ "manifest": "1343851e1c7663a114c9aca154e324b3d8216db854b0fa9e032198e946457bc5",
11
+ "packageJson": "ec35515de64d1c1079fe24af8a7227b971100db3bb17e6350626cfb3f196483c",
12
+ "agents": "4e963e1bc75c7fbf383eb7d31ef6ac37bcfb08b73bd26871c70817997a3c1f29"
13
13
  },
14
14
  "exports": [
15
15
  ".",
@@ -54,7 +54,7 @@
54
54
  "warehouse"
55
55
  ],
56
56
  "risks": [
57
- "cross-package-refs:1",
57
+ "cross-package-refs:8",
58
58
  "sensitive-fields-excluded"
59
59
  ],
60
60
  "objects": [
@@ -169,6 +169,192 @@
169
169
  "tags": [],
170
170
  "risks": []
171
171
  },
172
+ {
173
+ "name": "LotCollection",
174
+ "qualifiedName": "@happyvertical/smrt-inventory:LotCollection",
175
+ "collection": "lots",
176
+ "tableName": "inventory_lots",
177
+ "packageName": "@happyvertical/smrt-inventory",
178
+ "extends": "SmrtCollection",
179
+ "fields": [],
180
+ "relationships": [],
181
+ "methods": [
182
+ "findByLotNumber"
183
+ ],
184
+ "methodSignatures": [
185
+ {
186
+ "name": "findByLotNumber",
187
+ "async": true,
188
+ "params": [
189
+ "lotNumber: string",
190
+ "options?: object"
191
+ ],
192
+ "returns": "Promise<Lot[]>"
193
+ }
194
+ ],
195
+ "surfaces": [],
196
+ "withheldSurfaces": [
197
+ {
198
+ "kind": "api",
199
+ "operation": "findByLotNumber",
200
+ "code": "not-included",
201
+ "reason": "not listed in api.include",
202
+ "objectName": "@happyvertical/smrt-inventory:LotCollection"
203
+ }
204
+ ],
205
+ "relationshipFeatures": [
206
+ "uuidColumns"
207
+ ],
208
+ "tags": [],
209
+ "risks": []
210
+ },
211
+ {
212
+ "name": "LotCorrectionCollection",
213
+ "qualifiedName": "@happyvertical/smrt-inventory:LotCorrectionCollection",
214
+ "collection": "lotcorrections",
215
+ "tableName": "inventory_lot_corrections",
216
+ "packageName": "@happyvertical/smrt-inventory",
217
+ "extends": "SmrtCollection",
218
+ "fields": [],
219
+ "relationships": [],
220
+ "methods": [],
221
+ "surfaces": [],
222
+ "relationshipFeatures": [
223
+ "uuidColumns"
224
+ ],
225
+ "tags": [],
226
+ "risks": []
227
+ },
228
+ {
229
+ "name": "LotDocumentCollection",
230
+ "qualifiedName": "@happyvertical/smrt-inventory:LotDocumentCollection",
231
+ "collection": "lotdocuments",
232
+ "tableName": "inventory_lot_documents",
233
+ "packageName": "@happyvertical/smrt-inventory",
234
+ "extends": "SmrtCollection",
235
+ "fields": [],
236
+ "relationships": [],
237
+ "methods": [],
238
+ "surfaces": [],
239
+ "relationshipFeatures": [
240
+ "uuidColumns"
241
+ ],
242
+ "tags": [],
243
+ "risks": []
244
+ },
245
+ {
246
+ "name": "LotStockLevelCollection",
247
+ "qualifiedName": "@happyvertical/smrt-inventory:LotStockLevelCollection",
248
+ "collection": "lotstocklevels",
249
+ "tableName": "inventory_lot_stock_levels",
250
+ "packageName": "@happyvertical/smrt-inventory",
251
+ "extends": "SmrtCollection",
252
+ "fields": [],
253
+ "relationships": [],
254
+ "methods": [
255
+ "getLevel",
256
+ "onHand"
257
+ ],
258
+ "methodSignatures": [
259
+ {
260
+ "name": "getLevel",
261
+ "async": true,
262
+ "params": [
263
+ "lotId: string",
264
+ "locationId: string",
265
+ "state?: StockState"
266
+ ],
267
+ "returns": "Promise<LotStockLevel | null>"
268
+ },
269
+ {
270
+ "name": "onHand",
271
+ "async": true,
272
+ "params": [
273
+ "lotId: string",
274
+ "state?: StockState"
275
+ ],
276
+ "returns": "Promise<number>"
277
+ }
278
+ ],
279
+ "surfaces": [],
280
+ "withheldSurfaces": [
281
+ {
282
+ "kind": "api",
283
+ "operation": "getLevel",
284
+ "code": "not-included",
285
+ "reason": "not listed in api.include",
286
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevelCollection"
287
+ },
288
+ {
289
+ "kind": "api",
290
+ "operation": "onHand",
291
+ "code": "not-included",
292
+ "reason": "not listed in api.include",
293
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevelCollection"
294
+ }
295
+ ],
296
+ "relationshipFeatures": [
297
+ "uuidColumns"
298
+ ],
299
+ "tags": [],
300
+ "risks": []
301
+ },
302
+ {
303
+ "name": "LotUsageCollection",
304
+ "qualifiedName": "@happyvertical/smrt-inventory:LotUsageCollection",
305
+ "collection": "lotusages",
306
+ "tableName": "inventory_lot_usages",
307
+ "packageName": "@happyvertical/smrt-inventory",
308
+ "extends": "SmrtCollection",
309
+ "fields": [],
310
+ "relationships": [],
311
+ "methods": [
312
+ "findActiveForLot",
313
+ "findForConsumer"
314
+ ],
315
+ "methodSignatures": [
316
+ {
317
+ "name": "findActiveForLot",
318
+ "async": true,
319
+ "params": [
320
+ "lotId: string"
321
+ ],
322
+ "returns": "Promise<LotUsage[]>"
323
+ },
324
+ {
325
+ "name": "findForConsumer",
326
+ "async": true,
327
+ "params": [
328
+ "consumerType: string",
329
+ "consumerId: string",
330
+ "options?: object"
331
+ ],
332
+ "returns": "Promise<LotUsage[]>"
333
+ }
334
+ ],
335
+ "surfaces": [],
336
+ "withheldSurfaces": [
337
+ {
338
+ "kind": "api",
339
+ "operation": "findActiveForLot",
340
+ "code": "not-included",
341
+ "reason": "not listed in api.include",
342
+ "objectName": "@happyvertical/smrt-inventory:LotUsageCollection"
343
+ },
344
+ {
345
+ "kind": "api",
346
+ "operation": "findForConsumer",
347
+ "code": "not-included",
348
+ "reason": "not listed in api.include",
349
+ "objectName": "@happyvertical/smrt-inventory:LotUsageCollection"
350
+ }
351
+ ],
352
+ "relationshipFeatures": [
353
+ "uuidColumns"
354
+ ],
355
+ "tags": [],
356
+ "risks": []
357
+ },
172
358
  {
173
359
  "name": "StockLevelCollection",
174
360
  "qualifiedName": "@happyvertical/smrt-inventory:StockLevelCollection",
@@ -388,51 +574,946 @@
388
574
  "extends": "SmrtObject",
389
575
  "fields": [
390
576
  {
391
- "name": "tenantId",
577
+ "name": "tenantId",
578
+ "type": "text",
579
+ "required": false,
580
+ "columnType": "UUID"
581
+ },
582
+ {
583
+ "name": "code",
584
+ "type": "text",
585
+ "required": true,
586
+ "columnType": "TEXT",
587
+ "default": "",
588
+ "description": "A short code for the place stock is kept, for example MAIN."
589
+ },
590
+ {
591
+ "name": "name",
592
+ "type": "text",
593
+ "required": false,
594
+ "columnType": "TEXT",
595
+ "default": "",
596
+ "description": "What you call the place, for example Main warehouse."
597
+ },
598
+ {
599
+ "name": "kind",
600
+ "type": "text",
601
+ "required": false,
602
+ "columnType": "TEXT",
603
+ "default": "warehouse",
604
+ "description": "What sort of place it is, for example a warehouse or a shop floor."
605
+ },
606
+ {
607
+ "name": "placeId",
608
+ "type": "text",
609
+ "required": false,
610
+ "columnType": "TEXT",
611
+ "default": ""
612
+ },
613
+ {
614
+ "name": "active",
615
+ "type": "boolean",
616
+ "required": false,
617
+ "columnType": "BOOLEAN",
618
+ "default": true
619
+ }
620
+ ],
621
+ "relationships": [],
622
+ "methods": [],
623
+ "tenant": {
624
+ "scoped": true,
625
+ "mode": "optional",
626
+ "field": "tenantId"
627
+ },
628
+ "conflictColumns": [
629
+ "code",
630
+ "tenant_id"
631
+ ],
632
+ "surfaces": [
633
+ {
634
+ "kind": "api",
635
+ "name": "inventorylocations.list",
636
+ "operation": "list",
637
+ "objectName": "@happyvertical/smrt-inventory:InventoryLocation",
638
+ "path": "/inventorylocations",
639
+ "method": "GET"
640
+ },
641
+ {
642
+ "kind": "api",
643
+ "name": "inventorylocations.get",
644
+ "operation": "get",
645
+ "objectName": "@happyvertical/smrt-inventory:InventoryLocation",
646
+ "path": "/inventorylocations/[id]",
647
+ "method": "GET"
648
+ },
649
+ {
650
+ "kind": "api",
651
+ "name": "inventorylocations.create",
652
+ "operation": "create",
653
+ "objectName": "@happyvertical/smrt-inventory:InventoryLocation",
654
+ "path": "/inventorylocations",
655
+ "method": "POST"
656
+ },
657
+ {
658
+ "kind": "api",
659
+ "name": "inventorylocations.update",
660
+ "operation": "update",
661
+ "objectName": "@happyvertical/smrt-inventory:InventoryLocation",
662
+ "path": "/inventorylocations/[id]",
663
+ "method": "PATCH"
664
+ },
665
+ {
666
+ "kind": "cli",
667
+ "name": "inventorylocation_list",
668
+ "operation": "list",
669
+ "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
670
+ },
671
+ {
672
+ "kind": "cli",
673
+ "name": "inventorylocation_get",
674
+ "operation": "get",
675
+ "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
676
+ },
677
+ {
678
+ "kind": "cli",
679
+ "name": "inventorylocation_create",
680
+ "operation": "create",
681
+ "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
682
+ },
683
+ {
684
+ "kind": "cli",
685
+ "name": "inventorylocation_update",
686
+ "operation": "update",
687
+ "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
688
+ },
689
+ {
690
+ "kind": "cli",
691
+ "name": "inventorylocation_delete",
692
+ "operation": "delete",
693
+ "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
694
+ },
695
+ {
696
+ "kind": "mcp",
697
+ "name": "inventorylocation_list",
698
+ "operation": "list",
699
+ "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
700
+ },
701
+ {
702
+ "kind": "mcp",
703
+ "name": "inventorylocation_get",
704
+ "operation": "get",
705
+ "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
706
+ }
707
+ ],
708
+ "relationshipFeatures": [
709
+ "uuidColumns"
710
+ ],
711
+ "tags": [],
712
+ "risks": []
713
+ },
714
+ {
715
+ "name": "Lot",
716
+ "qualifiedName": "@happyvertical/smrt-inventory:Lot",
717
+ "collection": "lots",
718
+ "tableName": "inventory_lots",
719
+ "packageName": "@happyvertical/smrt-inventory",
720
+ "extends": "SmrtObject",
721
+ "fields": [
722
+ {
723
+ "name": "tenantId",
724
+ "type": "text",
725
+ "required": false,
726
+ "columnType": "UUID"
727
+ },
728
+ {
729
+ "name": "skuId",
730
+ "type": "text",
731
+ "required": false,
732
+ "columnType": "TEXT",
733
+ "description": "The item this lot is of, if catalogued."
734
+ },
735
+ {
736
+ "name": "kind",
737
+ "type": "text",
738
+ "required": false,
739
+ "columnType": "TEXT",
740
+ "default": "lot",
741
+ "description": "Whether this is a batch (lot) or a single unit (serial)."
742
+ },
743
+ {
744
+ "name": "lotNumber",
745
+ "type": "text",
746
+ "required": true,
747
+ "columnType": "TEXT",
748
+ "default": "",
749
+ "constraints": {
750
+ "maxLength": 128
751
+ },
752
+ "description": "The batch code, supplier lot or serial number."
753
+ },
754
+ {
755
+ "name": "searchKey",
756
+ "type": "text",
757
+ "required": false,
758
+ "columnType": "TEXT",
759
+ "default": "",
760
+ "constraints": {
761
+ "maxLength": 160
762
+ },
763
+ "description": "The number in normal form, used for lookup."
764
+ },
765
+ {
766
+ "name": "identityKey",
767
+ "type": "text",
768
+ "required": true,
769
+ "columnType": "TEXT",
770
+ "default": "",
771
+ "constraints": {
772
+ "maxLength": 160
773
+ },
774
+ "description": "Internal identity that keeps lots from being created twice."
775
+ },
776
+ {
777
+ "name": "uniqueNumber",
778
+ "type": "boolean",
779
+ "required": false,
780
+ "columnType": "BOOLEAN",
781
+ "default": false,
782
+ "description": "Whether the number must be unique for this item."
783
+ },
784
+ {
785
+ "name": "receivedQty",
786
+ "type": "decimal",
787
+ "required": false,
788
+ "columnType": "REAL",
789
+ "description": "How many came in on the receipt."
790
+ },
791
+ {
792
+ "name": "unit",
793
+ "type": "text",
794
+ "required": false,
795
+ "columnType": "TEXT",
796
+ "default": "",
797
+ "description": "The unit of measure."
798
+ },
799
+ {
800
+ "name": "sourceType",
801
+ "type": "text",
802
+ "required": false,
803
+ "columnType": "TEXT",
804
+ "default": "",
805
+ "description": "What kind of record received this lot."
806
+ },
807
+ {
808
+ "name": "sourceId",
809
+ "type": "text",
810
+ "required": false,
811
+ "columnType": "TEXT",
812
+ "default": "",
813
+ "description": "The record that received this lot."
814
+ },
815
+ {
816
+ "name": "attributes",
817
+ "type": "text",
818
+ "required": false,
819
+ "columnType": "TEXT",
820
+ "description": "Anything else worth knowing about this lot, stored as JSON."
821
+ },
822
+ {
823
+ "name": "status",
824
+ "type": "text",
825
+ "required": false,
826
+ "columnType": "TEXT",
827
+ "default": "active",
828
+ "description": "Where the lot stands: active, quarantined, consumed or void."
829
+ },
830
+ {
831
+ "name": "receivedAt",
832
+ "type": "datetime",
833
+ "required": false,
834
+ "columnType": "TIMESTAMP",
835
+ "description": "When the lot was received."
836
+ },
837
+ {
838
+ "name": "receivedByProfileId",
839
+ "type": "crossPackageRef",
840
+ "required": false,
841
+ "related": "@happyvertical/smrt-profiles:Profile",
842
+ "columnType": "UUID"
843
+ },
844
+ {
845
+ "name": "lastUsedAt",
846
+ "type": "datetime",
847
+ "required": false,
848
+ "columnType": "TIMESTAMP"
849
+ }
850
+ ],
851
+ "relationships": [
852
+ {
853
+ "name": "receivedByProfileId",
854
+ "type": "crossPackageRef",
855
+ "required": false,
856
+ "related": "@happyvertical/smrt-profiles:Profile",
857
+ "columnType": "UUID"
858
+ }
859
+ ],
860
+ "methods": [
861
+ "getAttributes",
862
+ "setAttributes"
863
+ ],
864
+ "methodSignatures": [
865
+ {
866
+ "name": "getAttributes",
867
+ "returns": "Record<string, unknown>"
868
+ },
869
+ {
870
+ "name": "setAttributes",
871
+ "params": [
872
+ "value: Record<string, unknown> | string"
873
+ ],
874
+ "returns": "void"
875
+ }
876
+ ],
877
+ "tenant": {
878
+ "scoped": true,
879
+ "mode": "optional",
880
+ "field": "tenantId"
881
+ },
882
+ "conflictColumns": [
883
+ "identity_key",
884
+ "tenant_id"
885
+ ],
886
+ "surfaces": [
887
+ {
888
+ "kind": "api",
889
+ "name": "lots.list",
890
+ "operation": "list",
891
+ "objectName": "@happyvertical/smrt-inventory:Lot",
892
+ "path": "/lots",
893
+ "method": "GET"
894
+ },
895
+ {
896
+ "kind": "api",
897
+ "name": "lots.get",
898
+ "operation": "get",
899
+ "objectName": "@happyvertical/smrt-inventory:Lot",
900
+ "path": "/lots/[id]",
901
+ "method": "GET"
902
+ },
903
+ {
904
+ "kind": "cli",
905
+ "name": "lot_list",
906
+ "operation": "list",
907
+ "objectName": "@happyvertical/smrt-inventory:Lot"
908
+ },
909
+ {
910
+ "kind": "cli",
911
+ "name": "lot_get",
912
+ "operation": "get",
913
+ "objectName": "@happyvertical/smrt-inventory:Lot"
914
+ },
915
+ {
916
+ "kind": "mcp",
917
+ "name": "lot_list",
918
+ "operation": "list",
919
+ "objectName": "@happyvertical/smrt-inventory:Lot"
920
+ },
921
+ {
922
+ "kind": "mcp",
923
+ "name": "lot_get",
924
+ "operation": "get",
925
+ "objectName": "@happyvertical/smrt-inventory:Lot"
926
+ }
927
+ ],
928
+ "withheldSurfaces": [
929
+ {
930
+ "kind": "api",
931
+ "operation": "getAttributes",
932
+ "code": "not-included",
933
+ "reason": "not listed in api.include",
934
+ "objectName": "@happyvertical/smrt-inventory:Lot"
935
+ },
936
+ {
937
+ "kind": "api",
938
+ "operation": "setAttributes",
939
+ "code": "not-included",
940
+ "reason": "not listed in api.include",
941
+ "objectName": "@happyvertical/smrt-inventory:Lot"
942
+ }
943
+ ],
944
+ "relationshipFeatures": [
945
+ "crossPackageRef",
946
+ "uuidColumns"
947
+ ],
948
+ "tags": [],
949
+ "risks": []
950
+ },
951
+ {
952
+ "name": "LotCorrection",
953
+ "qualifiedName": "@happyvertical/smrt-inventory:LotCorrection",
954
+ "collection": "lotcorrections",
955
+ "tableName": "inventory_lot_corrections",
956
+ "packageName": "@happyvertical/smrt-inventory",
957
+ "extends": "SmrtObject",
958
+ "fields": [
959
+ {
960
+ "name": "tenantId",
961
+ "type": "text",
962
+ "required": false,
963
+ "columnType": "UUID"
964
+ },
965
+ {
966
+ "name": "lotId",
967
+ "type": "foreignKey",
968
+ "required": true,
969
+ "related": "Lot",
970
+ "columnType": "UUID"
971
+ },
972
+ {
973
+ "name": "kind",
974
+ "type": "text",
975
+ "required": true,
976
+ "columnType": "TEXT",
977
+ "default": "attribute",
978
+ "description": "What was corrected."
979
+ },
980
+ {
981
+ "name": "field",
982
+ "type": "text",
983
+ "required": false,
984
+ "columnType": "TEXT",
985
+ "default": "",
986
+ "description": "Which attribute changed, if one did."
987
+ },
988
+ {
989
+ "name": "oldValue",
990
+ "type": "text",
991
+ "required": false,
992
+ "columnType": "TEXT",
993
+ "description": "What it was."
994
+ },
995
+ {
996
+ "name": "newValue",
997
+ "type": "text",
998
+ "required": false,
999
+ "columnType": "TEXT",
1000
+ "description": "What it is now."
1001
+ },
1002
+ {
1003
+ "name": "reason",
1004
+ "type": "text",
1005
+ "required": true,
1006
+ "columnType": "TEXT",
1007
+ "constraints": {
1008
+ "maxLength": 1000
1009
+ },
1010
+ "description": "Why it was corrected."
1011
+ },
1012
+ {
1013
+ "name": "usageId",
1014
+ "type": "text",
1015
+ "required": false,
1016
+ "columnType": "TEXT"
1017
+ },
1018
+ {
1019
+ "name": "documentId",
1020
+ "type": "text",
1021
+ "required": false,
1022
+ "columnType": "TEXT"
1023
+ },
1024
+ {
1025
+ "name": "actorProfileId",
1026
+ "type": "crossPackageRef",
1027
+ "required": false,
1028
+ "related": "@happyvertical/smrt-profiles:Profile",
1029
+ "columnType": "UUID"
1030
+ },
1031
+ {
1032
+ "name": "correctedAt",
1033
+ "type": "datetime",
1034
+ "required": false,
1035
+ "columnType": "TIMESTAMP",
1036
+ "description": "When it was corrected."
1037
+ }
1038
+ ],
1039
+ "relationships": [
1040
+ {
1041
+ "name": "lotId",
1042
+ "type": "foreignKey",
1043
+ "required": true,
1044
+ "related": "Lot",
1045
+ "columnType": "UUID"
1046
+ },
1047
+ {
1048
+ "name": "actorProfileId",
1049
+ "type": "crossPackageRef",
1050
+ "required": false,
1051
+ "related": "@happyvertical/smrt-profiles:Profile",
1052
+ "columnType": "UUID"
1053
+ }
1054
+ ],
1055
+ "methods": [],
1056
+ "tenant": {
1057
+ "scoped": true,
1058
+ "mode": "optional",
1059
+ "field": "tenantId"
1060
+ },
1061
+ "conflictColumns": [
1062
+ "id"
1063
+ ],
1064
+ "surfaces": [
1065
+ {
1066
+ "kind": "api",
1067
+ "name": "lotcorrections.list",
1068
+ "operation": "list",
1069
+ "objectName": "@happyvertical/smrt-inventory:LotCorrection",
1070
+ "path": "/lotcorrections",
1071
+ "method": "GET"
1072
+ },
1073
+ {
1074
+ "kind": "api",
1075
+ "name": "lotcorrections.get",
1076
+ "operation": "get",
1077
+ "objectName": "@happyvertical/smrt-inventory:LotCorrection",
1078
+ "path": "/lotcorrections/[id]",
1079
+ "method": "GET"
1080
+ },
1081
+ {
1082
+ "kind": "cli",
1083
+ "name": "lotcorrection_list",
1084
+ "operation": "list",
1085
+ "objectName": "@happyvertical/smrt-inventory:LotCorrection"
1086
+ },
1087
+ {
1088
+ "kind": "cli",
1089
+ "name": "lotcorrection_get",
1090
+ "operation": "get",
1091
+ "objectName": "@happyvertical/smrt-inventory:LotCorrection"
1092
+ },
1093
+ {
1094
+ "kind": "mcp",
1095
+ "name": "lotcorrection_list",
1096
+ "operation": "list",
1097
+ "objectName": "@happyvertical/smrt-inventory:LotCorrection"
1098
+ },
1099
+ {
1100
+ "kind": "mcp",
1101
+ "name": "lotcorrection_get",
1102
+ "operation": "get",
1103
+ "objectName": "@happyvertical/smrt-inventory:LotCorrection"
1104
+ }
1105
+ ],
1106
+ "relationshipFeatures": [
1107
+ "crossPackageRef",
1108
+ "foreignKey",
1109
+ "uuidColumns"
1110
+ ],
1111
+ "tags": [],
1112
+ "risks": []
1113
+ },
1114
+ {
1115
+ "name": "LotDocument",
1116
+ "qualifiedName": "@happyvertical/smrt-inventory:LotDocument",
1117
+ "collection": "lotdocuments",
1118
+ "tableName": "inventory_lot_documents",
1119
+ "packageName": "@happyvertical/smrt-inventory",
1120
+ "extends": "SmrtObject",
1121
+ "fields": [
1122
+ {
1123
+ "name": "tenantId",
1124
+ "type": "text",
1125
+ "required": false,
1126
+ "columnType": "UUID"
1127
+ },
1128
+ {
1129
+ "name": "lotId",
1130
+ "type": "foreignKey",
1131
+ "required": true,
1132
+ "related": "Lot",
1133
+ "columnType": "UUID"
1134
+ },
1135
+ {
1136
+ "name": "assetId",
1137
+ "type": "crossPackageRef",
1138
+ "required": false,
1139
+ "related": "@happyvertical/smrt-assets:Asset",
1140
+ "columnType": "UUID"
1141
+ },
1142
+ {
1143
+ "name": "role",
1144
+ "type": "text",
1145
+ "required": false,
1146
+ "columnType": "TEXT",
1147
+ "default": "certificate",
1148
+ "description": "What the document is for, for example certificate."
1149
+ },
1150
+ {
1151
+ "name": "attachedAt",
1152
+ "type": "datetime",
1153
+ "required": false,
1154
+ "columnType": "TIMESTAMP",
1155
+ "description": "When it was attached."
1156
+ },
1157
+ {
1158
+ "name": "attachedByProfileId",
1159
+ "type": "crossPackageRef",
1160
+ "required": false,
1161
+ "related": "@happyvertical/smrt-profiles:Profile",
1162
+ "columnType": "UUID"
1163
+ },
1164
+ {
1165
+ "name": "detachedAt",
1166
+ "type": "datetime",
1167
+ "required": false,
1168
+ "columnType": "TIMESTAMP"
1169
+ },
1170
+ {
1171
+ "name": "detachedByProfileId",
1172
+ "type": "crossPackageRef",
1173
+ "required": false,
1174
+ "related": "@happyvertical/smrt-profiles:Profile",
1175
+ "columnType": "UUID"
1176
+ }
1177
+ ],
1178
+ "relationships": [
1179
+ {
1180
+ "name": "lotId",
1181
+ "type": "foreignKey",
1182
+ "required": true,
1183
+ "related": "Lot",
1184
+ "columnType": "UUID"
1185
+ },
1186
+ {
1187
+ "name": "assetId",
1188
+ "type": "crossPackageRef",
1189
+ "required": false,
1190
+ "related": "@happyvertical/smrt-assets:Asset",
1191
+ "columnType": "UUID"
1192
+ },
1193
+ {
1194
+ "name": "attachedByProfileId",
1195
+ "type": "crossPackageRef",
1196
+ "required": false,
1197
+ "related": "@happyvertical/smrt-profiles:Profile",
1198
+ "columnType": "UUID"
1199
+ },
1200
+ {
1201
+ "name": "detachedByProfileId",
1202
+ "type": "crossPackageRef",
1203
+ "required": false,
1204
+ "related": "@happyvertical/smrt-profiles:Profile",
1205
+ "columnType": "UUID"
1206
+ }
1207
+ ],
1208
+ "methods": [],
1209
+ "tenant": {
1210
+ "scoped": true,
1211
+ "mode": "optional",
1212
+ "field": "tenantId"
1213
+ },
1214
+ "conflictColumns": [
1215
+ "tenant_id",
1216
+ "slug",
1217
+ "context"
1218
+ ],
1219
+ "surfaces": [
1220
+ {
1221
+ "kind": "api",
1222
+ "name": "lotdocuments.list",
1223
+ "operation": "list",
1224
+ "objectName": "@happyvertical/smrt-inventory:LotDocument",
1225
+ "path": "/lotdocuments",
1226
+ "method": "GET"
1227
+ },
1228
+ {
1229
+ "kind": "api",
1230
+ "name": "lotdocuments.get",
1231
+ "operation": "get",
1232
+ "objectName": "@happyvertical/smrt-inventory:LotDocument",
1233
+ "path": "/lotdocuments/[id]",
1234
+ "method": "GET"
1235
+ },
1236
+ {
1237
+ "kind": "cli",
1238
+ "name": "lotdocument_list",
1239
+ "operation": "list",
1240
+ "objectName": "@happyvertical/smrt-inventory:LotDocument"
1241
+ },
1242
+ {
1243
+ "kind": "cli",
1244
+ "name": "lotdocument_get",
1245
+ "operation": "get",
1246
+ "objectName": "@happyvertical/smrt-inventory:LotDocument"
1247
+ },
1248
+ {
1249
+ "kind": "mcp",
1250
+ "name": "lotdocument_list",
1251
+ "operation": "list",
1252
+ "objectName": "@happyvertical/smrt-inventory:LotDocument"
1253
+ },
1254
+ {
1255
+ "kind": "mcp",
1256
+ "name": "lotdocument_get",
1257
+ "operation": "get",
1258
+ "objectName": "@happyvertical/smrt-inventory:LotDocument"
1259
+ }
1260
+ ],
1261
+ "relationshipFeatures": [
1262
+ "crossPackageRef",
1263
+ "foreignKey",
1264
+ "uuidColumns"
1265
+ ],
1266
+ "tags": [],
1267
+ "risks": []
1268
+ },
1269
+ {
1270
+ "name": "LotStockLevel",
1271
+ "qualifiedName": "@happyvertical/smrt-inventory:LotStockLevel",
1272
+ "collection": "lotstocklevels",
1273
+ "tableName": "inventory_lot_stock_levels",
1274
+ "packageName": "@happyvertical/smrt-inventory",
1275
+ "extends": "SmrtObject",
1276
+ "fields": [
1277
+ {
1278
+ "name": "tenantId",
1279
+ "type": "text",
1280
+ "required": false,
1281
+ "columnType": "UUID"
1282
+ },
1283
+ {
1284
+ "name": "lotId",
1285
+ "type": "foreignKey",
1286
+ "required": true,
1287
+ "related": "Lot",
1288
+ "columnType": "UUID"
1289
+ },
1290
+ {
1291
+ "name": "skuId",
1292
+ "type": "text",
1293
+ "required": false,
1294
+ "columnType": "TEXT",
1295
+ "default": "",
1296
+ "description": "The item the lot is of."
1297
+ },
1298
+ {
1299
+ "name": "locationId",
1300
+ "type": "text",
1301
+ "required": true,
1302
+ "columnType": "TEXT",
1303
+ "default": "",
1304
+ "description": "Where the lot is kept."
1305
+ },
1306
+ {
1307
+ "name": "state",
1308
+ "type": "text",
1309
+ "required": true,
1310
+ "columnType": "TEXT",
1311
+ "default": "available",
1312
+ "description": "The state of this stock: available, allocated, wip, qc_hold or damaged."
1313
+ },
1314
+ {
1315
+ "name": "qty",
1316
+ "type": "decimal",
1317
+ "required": false,
1318
+ "columnType": "REAL",
1319
+ "description": "How many are on hand."
1320
+ }
1321
+ ],
1322
+ "relationships": [
1323
+ {
1324
+ "name": "lotId",
1325
+ "type": "foreignKey",
1326
+ "required": true,
1327
+ "related": "Lot",
1328
+ "columnType": "UUID"
1329
+ }
1330
+ ],
1331
+ "methods": [],
1332
+ "tenant": {
1333
+ "scoped": true,
1334
+ "mode": "optional",
1335
+ "field": "tenantId"
1336
+ },
1337
+ "conflictColumns": [
1338
+ "lot_id",
1339
+ "location_id",
1340
+ "state",
1341
+ "tenant_id"
1342
+ ],
1343
+ "surfaces": [
1344
+ {
1345
+ "kind": "api",
1346
+ "name": "lotstocklevels.list",
1347
+ "operation": "list",
1348
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevel",
1349
+ "path": "/lotstocklevels",
1350
+ "method": "GET"
1351
+ },
1352
+ {
1353
+ "kind": "api",
1354
+ "name": "lotstocklevels.get",
1355
+ "operation": "get",
1356
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevel",
1357
+ "path": "/lotstocklevels/[id]",
1358
+ "method": "GET"
1359
+ },
1360
+ {
1361
+ "kind": "cli",
1362
+ "name": "lotstocklevel_list",
1363
+ "operation": "list",
1364
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevel"
1365
+ },
1366
+ {
1367
+ "kind": "cli",
1368
+ "name": "lotstocklevel_get",
1369
+ "operation": "get",
1370
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevel"
1371
+ },
1372
+ {
1373
+ "kind": "mcp",
1374
+ "name": "lotstocklevel_list",
1375
+ "operation": "list",
1376
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevel"
1377
+ },
1378
+ {
1379
+ "kind": "mcp",
1380
+ "name": "lotstocklevel_get",
1381
+ "operation": "get",
1382
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevel"
1383
+ }
1384
+ ],
1385
+ "relationshipFeatures": [
1386
+ "foreignKey",
1387
+ "uuidColumns"
1388
+ ],
1389
+ "tags": [],
1390
+ "risks": []
1391
+ },
1392
+ {
1393
+ "name": "LotUsage",
1394
+ "qualifiedName": "@happyvertical/smrt-inventory:LotUsage",
1395
+ "collection": "lotusages",
1396
+ "tableName": "inventory_lot_usages",
1397
+ "packageName": "@happyvertical/smrt-inventory",
1398
+ "extends": "SmrtObject",
1399
+ "fields": [
1400
+ {
1401
+ "name": "tenantId",
1402
+ "type": "text",
1403
+ "required": false,
1404
+ "columnType": "UUID"
1405
+ },
1406
+ {
1407
+ "name": "lotId",
1408
+ "type": "foreignKey",
1409
+ "required": true,
1410
+ "related": "Lot",
1411
+ "columnType": "UUID"
1412
+ },
1413
+ {
1414
+ "name": "consumerType",
392
1415
  "type": "text",
393
- "required": false,
394
- "columnType": "UUID"
1416
+ "required": true,
1417
+ "columnType": "TEXT",
1418
+ "default": "",
1419
+ "description": "What kind of record used the lot."
395
1420
  },
396
1421
  {
397
- "name": "code",
1422
+ "name": "consumerId",
398
1423
  "type": "text",
399
1424
  "required": true,
400
1425
  "columnType": "TEXT",
401
1426
  "default": "",
402
- "description": "A short code for the place stock is kept, for example MAIN."
1427
+ "description": "The record that used the lot."
403
1428
  },
404
1429
  {
405
- "name": "name",
1430
+ "name": "qty",
1431
+ "type": "decimal",
1432
+ "required": false,
1433
+ "columnType": "REAL",
1434
+ "description": "How much was used."
1435
+ },
1436
+ {
1437
+ "name": "unit",
406
1438
  "type": "text",
407
1439
  "required": false,
408
1440
  "columnType": "TEXT",
409
1441
  "default": "",
410
- "description": "What you call the place, for example Main warehouse."
1442
+ "description": "The unit of measure."
411
1443
  },
412
1444
  {
413
- "name": "kind",
1445
+ "name": "status",
414
1446
  "type": "text",
415
1447
  "required": false,
416
1448
  "columnType": "TEXT",
417
- "default": "warehouse",
418
- "description": "What sort of place it is, for example a warehouse or a shop floor."
1449
+ "default": "active",
1450
+ "description": "Whether the link still stands: active or unlinked."
419
1451
  },
420
1452
  {
421
- "name": "placeId",
1453
+ "name": "linkedAt",
1454
+ "type": "datetime",
1455
+ "required": false,
1456
+ "columnType": "TIMESTAMP",
1457
+ "description": "When the lot was linked."
1458
+ },
1459
+ {
1460
+ "name": "linkedByProfileId",
1461
+ "type": "crossPackageRef",
1462
+ "required": false,
1463
+ "related": "@happyvertical/smrt-profiles:Profile",
1464
+ "columnType": "UUID"
1465
+ },
1466
+ {
1467
+ "name": "unlinkedAt",
1468
+ "type": "datetime",
1469
+ "required": false,
1470
+ "columnType": "TIMESTAMP"
1471
+ },
1472
+ {
1473
+ "name": "unlinkedByProfileId",
1474
+ "type": "crossPackageRef",
1475
+ "required": false,
1476
+ "related": "@happyvertical/smrt-profiles:Profile",
1477
+ "columnType": "UUID"
1478
+ },
1479
+ {
1480
+ "name": "unlinkReason",
422
1481
  "type": "text",
423
1482
  "required": false,
424
1483
  "columnType": "TEXT",
425
- "default": ""
1484
+ "description": "Why the link was removed."
426
1485
  },
427
1486
  {
428
- "name": "active",
429
- "type": "boolean",
1487
+ "name": "movementId",
1488
+ "type": "text",
430
1489
  "required": false,
431
- "columnType": "BOOLEAN",
432
- "default": true
1490
+ "columnType": "TEXT",
1491
+ "description": "The stock movement for this use, if any."
1492
+ }
1493
+ ],
1494
+ "relationships": [
1495
+ {
1496
+ "name": "lotId",
1497
+ "type": "foreignKey",
1498
+ "required": true,
1499
+ "related": "Lot",
1500
+ "columnType": "UUID"
1501
+ },
1502
+ {
1503
+ "name": "linkedByProfileId",
1504
+ "type": "crossPackageRef",
1505
+ "required": false,
1506
+ "related": "@happyvertical/smrt-profiles:Profile",
1507
+ "columnType": "UUID"
1508
+ },
1509
+ {
1510
+ "name": "unlinkedByProfileId",
1511
+ "type": "crossPackageRef",
1512
+ "required": false,
1513
+ "related": "@happyvertical/smrt-profiles:Profile",
1514
+ "columnType": "UUID"
433
1515
  }
434
1516
  ],
435
- "relationships": [],
436
1517
  "methods": [],
437
1518
  "tenant": {
438
1519
  "scoped": true,
@@ -440,86 +1521,55 @@
440
1521
  "field": "tenantId"
441
1522
  },
442
1523
  "conflictColumns": [
443
- "code",
444
- "tenant_id"
1524
+ "tenant_id",
1525
+ "slug",
1526
+ "context"
445
1527
  ],
446
1528
  "surfaces": [
447
1529
  {
448
1530
  "kind": "api",
449
- "name": "inventorylocations.list",
1531
+ "name": "lotusages.list",
450
1532
  "operation": "list",
451
- "objectName": "@happyvertical/smrt-inventory:InventoryLocation",
452
- "path": "/inventorylocations",
1533
+ "objectName": "@happyvertical/smrt-inventory:LotUsage",
1534
+ "path": "/lotusages",
453
1535
  "method": "GET"
454
1536
  },
455
1537
  {
456
1538
  "kind": "api",
457
- "name": "inventorylocations.get",
1539
+ "name": "lotusages.get",
458
1540
  "operation": "get",
459
- "objectName": "@happyvertical/smrt-inventory:InventoryLocation",
460
- "path": "/inventorylocations/[id]",
1541
+ "objectName": "@happyvertical/smrt-inventory:LotUsage",
1542
+ "path": "/lotusages/[id]",
461
1543
  "method": "GET"
462
1544
  },
463
- {
464
- "kind": "api",
465
- "name": "inventorylocations.create",
466
- "operation": "create",
467
- "objectName": "@happyvertical/smrt-inventory:InventoryLocation",
468
- "path": "/inventorylocations",
469
- "method": "POST"
470
- },
471
- {
472
- "kind": "api",
473
- "name": "inventorylocations.update",
474
- "operation": "update",
475
- "objectName": "@happyvertical/smrt-inventory:InventoryLocation",
476
- "path": "/inventorylocations/[id]",
477
- "method": "PATCH"
478
- },
479
1545
  {
480
1546
  "kind": "cli",
481
- "name": "inventorylocation_list",
1547
+ "name": "lotusage_list",
482
1548
  "operation": "list",
483
- "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
1549
+ "objectName": "@happyvertical/smrt-inventory:LotUsage"
484
1550
  },
485
1551
  {
486
1552
  "kind": "cli",
487
- "name": "inventorylocation_get",
1553
+ "name": "lotusage_get",
488
1554
  "operation": "get",
489
- "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
490
- },
491
- {
492
- "kind": "cli",
493
- "name": "inventorylocation_create",
494
- "operation": "create",
495
- "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
496
- },
497
- {
498
- "kind": "cli",
499
- "name": "inventorylocation_update",
500
- "operation": "update",
501
- "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
502
- },
503
- {
504
- "kind": "cli",
505
- "name": "inventorylocation_delete",
506
- "operation": "delete",
507
- "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
1555
+ "objectName": "@happyvertical/smrt-inventory:LotUsage"
508
1556
  },
509
1557
  {
510
1558
  "kind": "mcp",
511
- "name": "inventorylocation_list",
1559
+ "name": "lotusage_list",
512
1560
  "operation": "list",
513
- "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
1561
+ "objectName": "@happyvertical/smrt-inventory:LotUsage"
514
1562
  },
515
1563
  {
516
1564
  "kind": "mcp",
517
- "name": "inventorylocation_get",
1565
+ "name": "lotusage_get",
518
1566
  "operation": "get",
519
- "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
1567
+ "objectName": "@happyvertical/smrt-inventory:LotUsage"
520
1568
  }
521
1569
  ],
522
1570
  "relationshipFeatures": [
1571
+ "crossPackageRef",
1572
+ "foreignKey",
523
1573
  "uuidColumns"
524
1574
  ],
525
1575
  "tags": [],
@@ -674,6 +1724,13 @@
674
1724
  "default": "",
675
1725
  "description": "The place it moved at."
676
1726
  },
1727
+ {
1728
+ "name": "lotId",
1729
+ "type": "text",
1730
+ "required": false,
1731
+ "columnType": "TEXT",
1732
+ "description": "The lot that moved, when tracked by lot."
1733
+ },
677
1734
  {
678
1735
  "name": "fromState",
679
1736
  "type": "text",
@@ -907,6 +1964,206 @@
907
1964
  "operation": "get",
908
1965
  "objectName": "@happyvertical/smrt-inventory:InventoryLocation"
909
1966
  },
1967
+ {
1968
+ "kind": "api",
1969
+ "name": "lots.list",
1970
+ "operation": "list",
1971
+ "objectName": "@happyvertical/smrt-inventory:Lot",
1972
+ "path": "/lots",
1973
+ "method": "GET"
1974
+ },
1975
+ {
1976
+ "kind": "api",
1977
+ "name": "lots.get",
1978
+ "operation": "get",
1979
+ "objectName": "@happyvertical/smrt-inventory:Lot",
1980
+ "path": "/lots/[id]",
1981
+ "method": "GET"
1982
+ },
1983
+ {
1984
+ "kind": "cli",
1985
+ "name": "lot_list",
1986
+ "operation": "list",
1987
+ "objectName": "@happyvertical/smrt-inventory:Lot"
1988
+ },
1989
+ {
1990
+ "kind": "cli",
1991
+ "name": "lot_get",
1992
+ "operation": "get",
1993
+ "objectName": "@happyvertical/smrt-inventory:Lot"
1994
+ },
1995
+ {
1996
+ "kind": "mcp",
1997
+ "name": "lot_list",
1998
+ "operation": "list",
1999
+ "objectName": "@happyvertical/smrt-inventory:Lot"
2000
+ },
2001
+ {
2002
+ "kind": "mcp",
2003
+ "name": "lot_get",
2004
+ "operation": "get",
2005
+ "objectName": "@happyvertical/smrt-inventory:Lot"
2006
+ },
2007
+ {
2008
+ "kind": "api",
2009
+ "name": "lotcorrections.list",
2010
+ "operation": "list",
2011
+ "objectName": "@happyvertical/smrt-inventory:LotCorrection",
2012
+ "path": "/lotcorrections",
2013
+ "method": "GET"
2014
+ },
2015
+ {
2016
+ "kind": "api",
2017
+ "name": "lotcorrections.get",
2018
+ "operation": "get",
2019
+ "objectName": "@happyvertical/smrt-inventory:LotCorrection",
2020
+ "path": "/lotcorrections/[id]",
2021
+ "method": "GET"
2022
+ },
2023
+ {
2024
+ "kind": "cli",
2025
+ "name": "lotcorrection_list",
2026
+ "operation": "list",
2027
+ "objectName": "@happyvertical/smrt-inventory:LotCorrection"
2028
+ },
2029
+ {
2030
+ "kind": "cli",
2031
+ "name": "lotcorrection_get",
2032
+ "operation": "get",
2033
+ "objectName": "@happyvertical/smrt-inventory:LotCorrection"
2034
+ },
2035
+ {
2036
+ "kind": "mcp",
2037
+ "name": "lotcorrection_list",
2038
+ "operation": "list",
2039
+ "objectName": "@happyvertical/smrt-inventory:LotCorrection"
2040
+ },
2041
+ {
2042
+ "kind": "mcp",
2043
+ "name": "lotcorrection_get",
2044
+ "operation": "get",
2045
+ "objectName": "@happyvertical/smrt-inventory:LotCorrection"
2046
+ },
2047
+ {
2048
+ "kind": "api",
2049
+ "name": "lotdocuments.list",
2050
+ "operation": "list",
2051
+ "objectName": "@happyvertical/smrt-inventory:LotDocument",
2052
+ "path": "/lotdocuments",
2053
+ "method": "GET"
2054
+ },
2055
+ {
2056
+ "kind": "api",
2057
+ "name": "lotdocuments.get",
2058
+ "operation": "get",
2059
+ "objectName": "@happyvertical/smrt-inventory:LotDocument",
2060
+ "path": "/lotdocuments/[id]",
2061
+ "method": "GET"
2062
+ },
2063
+ {
2064
+ "kind": "cli",
2065
+ "name": "lotdocument_list",
2066
+ "operation": "list",
2067
+ "objectName": "@happyvertical/smrt-inventory:LotDocument"
2068
+ },
2069
+ {
2070
+ "kind": "cli",
2071
+ "name": "lotdocument_get",
2072
+ "operation": "get",
2073
+ "objectName": "@happyvertical/smrt-inventory:LotDocument"
2074
+ },
2075
+ {
2076
+ "kind": "mcp",
2077
+ "name": "lotdocument_list",
2078
+ "operation": "list",
2079
+ "objectName": "@happyvertical/smrt-inventory:LotDocument"
2080
+ },
2081
+ {
2082
+ "kind": "mcp",
2083
+ "name": "lotdocument_get",
2084
+ "operation": "get",
2085
+ "objectName": "@happyvertical/smrt-inventory:LotDocument"
2086
+ },
2087
+ {
2088
+ "kind": "api",
2089
+ "name": "lotstocklevels.list",
2090
+ "operation": "list",
2091
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevel",
2092
+ "path": "/lotstocklevels",
2093
+ "method": "GET"
2094
+ },
2095
+ {
2096
+ "kind": "api",
2097
+ "name": "lotstocklevels.get",
2098
+ "operation": "get",
2099
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevel",
2100
+ "path": "/lotstocklevels/[id]",
2101
+ "method": "GET"
2102
+ },
2103
+ {
2104
+ "kind": "cli",
2105
+ "name": "lotstocklevel_list",
2106
+ "operation": "list",
2107
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevel"
2108
+ },
2109
+ {
2110
+ "kind": "cli",
2111
+ "name": "lotstocklevel_get",
2112
+ "operation": "get",
2113
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevel"
2114
+ },
2115
+ {
2116
+ "kind": "mcp",
2117
+ "name": "lotstocklevel_list",
2118
+ "operation": "list",
2119
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevel"
2120
+ },
2121
+ {
2122
+ "kind": "mcp",
2123
+ "name": "lotstocklevel_get",
2124
+ "operation": "get",
2125
+ "objectName": "@happyvertical/smrt-inventory:LotStockLevel"
2126
+ },
2127
+ {
2128
+ "kind": "api",
2129
+ "name": "lotusages.list",
2130
+ "operation": "list",
2131
+ "objectName": "@happyvertical/smrt-inventory:LotUsage",
2132
+ "path": "/lotusages",
2133
+ "method": "GET"
2134
+ },
2135
+ {
2136
+ "kind": "api",
2137
+ "name": "lotusages.get",
2138
+ "operation": "get",
2139
+ "objectName": "@happyvertical/smrt-inventory:LotUsage",
2140
+ "path": "/lotusages/[id]",
2141
+ "method": "GET"
2142
+ },
2143
+ {
2144
+ "kind": "cli",
2145
+ "name": "lotusage_list",
2146
+ "operation": "list",
2147
+ "objectName": "@happyvertical/smrt-inventory:LotUsage"
2148
+ },
2149
+ {
2150
+ "kind": "cli",
2151
+ "name": "lotusage_get",
2152
+ "operation": "get",
2153
+ "objectName": "@happyvertical/smrt-inventory:LotUsage"
2154
+ },
2155
+ {
2156
+ "kind": "mcp",
2157
+ "name": "lotusage_list",
2158
+ "operation": "list",
2159
+ "objectName": "@happyvertical/smrt-inventory:LotUsage"
2160
+ },
2161
+ {
2162
+ "kind": "mcp",
2163
+ "name": "lotusage_get",
2164
+ "operation": "get",
2165
+ "objectName": "@happyvertical/smrt-inventory:LotUsage"
2166
+ },
910
2167
  {
911
2168
  "kind": "api",
912
2169
  "name": "stocklevels.list",
@@ -990,14 +2247,14 @@
990
2247
  ],
991
2248
  "prompts": [],
992
2249
  "relationshipsV2": {
993
- "foreignKeyFields": 0,
994
- "crossPackageRefFields": 1,
2250
+ "foreignKeyFields": 4,
2251
+ "crossPackageRefFields": 8,
995
2252
  "junctionCollections": 0,
996
2253
  "hierarchicalObjects": 0,
997
2254
  "polymorphicAssociations": 0,
998
- "uuidColumns": 14
2255
+ "uuidColumns": 56
999
2256
  },
1000
- "agentDoc": "# @happyvertical/smrt-inventory\n\nMulti-location stock tracking. Strictly industry-neutral — the same primitives serve apparel, furniture, automotive, CPG, electronics, and any other vertical that counts discrete units across locations.\n\n## Models\n\n| Model | Purpose |\n|---|---|\n| _(catalog shapes — `Product`, `Material`, `ProductVariant`, `Sku` — live in `@happyvertical/smrt-products`)_ | Sku is the smallest sellable / countable unit; its `attributes` JSON carries axis-value pins. ProductVariant is the per-product axis declaration. Import either from `@happyvertical/smrt-products` directly. |\n| `InventoryLocation` | Warehouse / factory / retail / in-transit / virtual. Open-ended `kind` string. Optional `placeId` references `@happyvertical/smrt-places`. `conflictColumns: ['code', 'tenant_id']`. |\n| `StockLevel` | Materialized `qty` for a `(skuId, locationId, state)` tuple. Upserted in place. **Mutated only by `StockService`.** States: `available`, `allocated`, `wip`, `qc_hold`, `damaged`. |\n| `StockMovement` | Append-only audit log. Every `StockService` mutation writes one (or two for `transfer`). `sourceType` + `sourceId` carry cross-package attribution. |\n\nAll three inventory models (`InventoryLocation`, `StockLevel`, `StockMovement`) use `@TenantScoped({ mode: 'optional' })` + nullable `tenantId` so they can be used either tenant-scoped or globally. The catalog shapes (`Product`, `Material`, `ProductVariant`, `Sku`) live in `@happyvertical/smrt-products` and carry their own tenant decoration there.\n\n## StockService — the only sanctioned way to mutate stock\n\n```typescript\nimport { createStockService } from '@happyvertical/smrt-inventory';\n\nconst service = await createStockService({ db });\nawait service.receive(skuId, locationId, qty, { sourceType, sourceId });\nawait service.reserve(skuId, locationId, qty, { sourceType, sourceId });\nawait service.release(skuId, locationId, qty, { sourceType, sourceId });\nawait service.fulfill(skuId, locationId, qty, { sourceType, sourceId });\nawait service.transfer(skuId, fromLocId, toLocId, qty, { sourceType, sourceId });\nawait service.adjust(skuId, locationId, delta, { sourceType, sourceId });\n```\n\n| Method | Behavior | Movement reason |\n|---|---|---|\n| `receive` | +qty available — purchase receipt, return, production produce | `receipt` |\n| `reserve` | available → allocated. Throws `InsufficientStockError` on overdraw | `reservation` |\n| `release` | allocated → available (order cancel) | `release` |\n| `fulfill` | -qty allocated. Stock leaves the building | `fulfillment` |\n| `transfer` | move stock between locations (writes two movements) | `transfer_out`, `transfer_in` |\n| `adjust` | signed delta — cycle counts and one-off corrections | `adjustment` |\n\nAll methods reject zero / negative / non-finite quantities except `adjust`, which accepts non-zero signed deltas. Negative deltas that would drive a state below zero throw `InsufficientStockError`.\n\n## Opt-in DispatchBus hooks\n\nOff by default. Wire them up explicitly in the application's `smrt.ts`:\n\n```typescript\nimport { installInventoryDispatchHandlers } from '@happyvertical/smrt-inventory';\n\nconst handlers = await installInventoryDispatchHandlers({\n dispatchBus: bus,\n db, // or stockService: existingService\n});\n```\n\nThis subscribes to:\n- `contract:created` → calls `service.reserve()` for every line inside one `stockService.withTransaction(...)`, attributed to `('Contract', payload.contractId)`\n- `fulfillment:shipped` → calls `service.fulfill()` for every line inside one `stockService.withTransaction(...)`, attributed to `('Fulfillment', payload.fulfillmentId)`\n\nBoth handlers are atomic across lines: a shortfall on line N rolls back lines 1..N-1 so the event is all-or-nothing. Without this guarantee, `DispatchBus` would only log the async handler error and a partially-reserved (or partially-fulfilled) contract would have no compensating event to fix it.\n\nPer-handler toggles: `installContractReserved`, `installFulfillmentShipped`. The `production_order:posted` hook is deliberately not installed here — it depends on the BOM model and ships in `@happyvertical/smrt-manufacturing` (issue #1250).\n\n## Schema migration (Phase 1 release)\n\nThe `Sku` model moved out of this package and into `@happyvertical/smrt-products`. Previously it lived in this package's `inventory_skus` table; it now lives in `product_skus` over there.\n\n**Upgrade procedure** for deployed consumers:\n\n1. **Let the framework create the destination table first.** Boot the new version once with `@happyvertical/smrt-products` registered in your `SmrtClassOptions`; the lazy `syncSchema` path will create `product_skus` with the right primary key, NOT NULL, UNIQUE (`code`, `tenant_id`), and indexes — they're derived from the `Sku` model decorators and would be stripped by a raw `CREATE TABLE AS SELECT`.\n\n2. **Idempotently copy rows across.** SQLite + Postgres:\n\n ```sql\n BEGIN;\n INSERT INTO product_skus (\n id, slug, context, created_at, updated_at,\n tenant_id, product_id, code, barcode, name,\n attributes, weight_grams, parent_sku_id, active\n )\n SELECT\n id, slug, context, created_at, updated_at,\n tenant_id, product_id, code, barcode, name,\n attributes, weight_grams, parent_sku_id, active\n FROM inventory_skus\n WHERE NOT EXISTS (\n SELECT 1 FROM product_skus p WHERE p.id = inventory_skus.id\n );\n COMMIT;\n ```\n\n3. **Drop the legacy table** once you've verified row counts match:\n\n ```sql\n DROP TABLE IF EXISTS inventory_skus;\n ```\n\n`StockLevel.skuId` and `StockMovement.skuId` carry plain string ids that still resolve to the same rows after the rename, so no inventory data has to move.\n\n## Gotchas\n\n- **Movements are append-only.** The materialized `StockLevel` row is derived state; the `StockMovement` ledger is the source of truth. Never let CRUD UIs expose update/delete on the movement table. If you find yourself wanting to \"fix\" a movement row, write a compensating `adjust()` call instead.\n- **Never call `StockLevel.create()` or `save()` directly.** Going around `StockService` silently desyncs the audit log and breaks cycle counts.\n- **Cross-industry constraint.** This package's vocabulary stays generic: `InventoryLocation`, `StockLevel`, `StockMovement`. Apparel-specific concepts (`Style`, `Makeup`, `Colorway`, fashion `Season`, tech-pack metadata) and their analogues in furniture / automotive / CPG live in the relevant template package, never here. PRs that introduce industry vocabulary should be rejected.\n- **Cross-package references are plain strings.** `skuId`, `placeId`, `sourceId` — all plain string ids, never `@foreignKey()`. Inventory's stock-motion logic only ever reads StockLevel/StockMovement; it doesn't follow `skuId` back to the catalog's Sku table, so the layering stays loose.\n- **`conflictColumns` include `tenant_id`** on `InventoryLocation` and `StockLevel`. NULL-matching semantics (`(code, NULL) IS NOT DISTINCT FROM (code, NULL)`) are handled by `@happyvertical/sql >= 0.74.0` natively, so two saves with the same `(code, NULL)` tuple correctly merge in place. Pass `nullsDistinct: true` at the sql layer to opt back into NULL-distinct semantics.\n- **Atomic per-method, transactional across composition.** Each `StockService` mutation (`receive` / `reserve` / `release` / `fulfill` / `transfer` / `adjust`) wraps every write in a single `db.transaction(...)` (via `@happyvertical/sql >= 0.74.0`). Partial failure rolls the whole call back — level writes and the matching `StockMovement` audit row commit together or not at all. `transfer` writes both legs (source decrement, destination increment, and both audit rows) inside one tx, so a failure mid-`transfer` is fully reverted.\n Callers composing multiple `StockService` calls into one logical unit (e.g. consuming materials line-by-line for a production order) should wrap them in `await stockService.withTransaction(async (tx) => { ... })` — `tx` is a tx-bound `StockService` with the same public API; mutation calls inside the callback share one transaction and either all commit or all roll back.\n If the underlying SQL adapter does not expose `transaction()` (extremely rare — all four built-in `@happyvertical/sql >= 0.74.0` adapters implement it), the service degrades to non-atomic serial writes and emits a one-time `console.warn`.\n- **Concurrent reservations: tightened, not bulletproof.** `@happyvertical/sql >= 0.74.0` serialises null-aware *upserts* via a per-key in-process lock (SQLite) or advisory lock (Postgres). That lock fixed the row-creation race (issue #1246) — two concurrent `create()` calls on the same conflict-column tuple no longer race the underlying `INSERT ... ON CONFLICT`. The first `StockLevel` row for a key is now created insert-only (#3204): a concurrent first write fails on the unique key and its transaction rolls back (retry it) instead of upserting over the winner's qty. It does NOT serialise the read-modify-write compound that `reserve` / `fulfill` / `transfer` / `adjust` perform on an existing row: each method reads the current level (`SELECT`), computes the new value in JS, then writes it back (`UPDATE` via `save()` on the loaded row). The whole compound is inside one `db.transaction(...)` from round-3, so the level write and audit row commit together — but two concurrent transactions on the same `(skuId, locationId)` can each see the same starting balance, compute the same decrement, and double-spend. Awaited (serial) calls always behave correctly. Use a job queue (e.g. `@happyvertical/smrt-jobs`) or your own mutex when you need hard atomicity across concurrent callers. The full fix is `SELECT ... FOR UPDATE` inside the same transaction (Postgres) or upgrading the journal mode (SQLite); deferred until we see a real high-contention workload that warrants it.\n\n## Source attribution\n\nEvery `StockMovement` carries `sourceType` + `sourceId` so downstream queries can reconstruct \"what caused this movement\". Conventions used by the built-in dispatch handlers:\n\n| sourceType | Emitter | Note |\n|---|---|---|\n| `Contract` | `smrt-commerce` `contract:created` | All reservation/release/fulfilment legs caused by a contract |\n| `Fulfillment` | `smrt-commerce` `fulfillment:shipped` | Outbound shipment |\n| `PurchaseOrder` | (your code) | Inbound receipt |\n| `CycleCount` | (your code) | Adjustments from physical counts |\n| `TransferOrder` | (your code) | Both legs of a transfer |\n| `ProductionOrder` | `smrt-manufacturing` (issue #1250) | Materials consumed + finished goods produced |\n\n## Replenishment and browser components\n\n`StockService.setReorderPolicy()` is the sanctioned metadata writer for nullable\n`StockLevel.reorderPoint` / `reorderQuantity` on the available SKU/location row.\nIt does not change quantity or emit a movement. `findBelowReorderPoint()` uses\npublic tenant-filtered collection reads and strict less-than semantics.\n`StockMutationOptions.actorProfileId` propagates to every movement, including\nboth transfer legs; null retains compatibility for automation. It is a qualified\ncross-package profile reference, not an authorization decision.\n\n`src/svelte/` exports data-driven components through `./svelte`. Server callers\nown authentication and persistence callbacks; never import backend models into\nthis entry or expose ledger mutations as CRUD. Package `typecheck` includes\nSvelte checks and `test` includes DOM component tests. See README for the public\nprops, service contracts, and additive schema upgrade requirements.\n",
2257
+ "agentDoc": "# @happyvertical/smrt-inventory\n\nMulti-location stock tracking. Strictly industry-neutral — the same primitives serve apparel, furniture, automotive, CPG, electronics, and any other vertical that counts discrete units across locations.\n\n## Models\n\n| Model | Purpose |\n|---|---|\n| _(catalog shapes — `Product`, `Material`, `ProductVariant`, `Sku` — live in `@happyvertical/smrt-products`)_ | Sku is the smallest sellable / countable unit; its `attributes` JSON carries axis-value pins. ProductVariant is the per-product axis declaration. Import either from `@happyvertical/smrt-products` directly. |\n| `InventoryLocation` | Warehouse / factory / retail / in-transit / virtual. Open-ended `kind` string. Optional `placeId` references `@happyvertical/smrt-places`. `conflictColumns: ['code', 'tenant_id']`. |\n| `StockLevel` | Materialized `qty` for a `(skuId, locationId, state)` tuple. Upserted in place. **Mutated only by `StockService`.** States: `available`, `allocated`, `wip`, `qc_hold`, `damaged`. |\n| `StockMovement` | Append-only audit log. Every `StockService` mutation writes one (or two for `transfer`). `sourceType` + `sourceId` carry cross-package attribution. |\n\n| `Lot` | A traceable batch or serial (`inventory_lots`). `conflictColumns: ['identity_key','tenant_id']`, every create insert-only. **Written only by `LotService`.** |\n| `LotStockLevel` | Per-lot balance for `(lotId, locationId, state)` (`inventory_lot_stock_levels`). **Mutated only by `StockService`** when `lotId` is passed. |\n| `LotUsage` | A lot used by a consumer: the trace edge. Never deleted; `status: 'unlinked'` after a correction. `consumerType: 'Lot'` marks assembly lineage. |\n| `LotCorrection` | Append-only correction trail (`conflictColumns: ['id']`); reason mandatory. |\n| `LotDocument` | Asset attached to a lot (`assetId` is a cross-package ref); detached, never deleted. |\n\nAll inventory models (`InventoryLocation`, `StockLevel`, `StockMovement`, and the lot models) use `@TenantScoped({ mode: 'optional' })` + nullable `tenantId` so they can be used either tenant-scoped or globally. The catalog shapes (`Product`, `Material`, `ProductVariant`, `Sku`) live in `@happyvertical/smrt-products` and carry their own tenant decoration there.\n\n## StockService — the only sanctioned way to mutate stock\n\n```typescript\nimport { createStockService } from '@happyvertical/smrt-inventory';\n\nconst service = await createStockService({ db });\nawait service.receive(skuId, locationId, qty, { sourceType, sourceId });\nawait service.reserve(skuId, locationId, qty, { sourceType, sourceId });\nawait service.release(skuId, locationId, qty, { sourceType, sourceId });\nawait service.fulfill(skuId, locationId, qty, { sourceType, sourceId });\nawait service.transfer(skuId, fromLocId, toLocId, qty, { sourceType, sourceId });\nawait service.adjust(skuId, locationId, delta, { sourceType, sourceId });\n```\n\n| Method | Behavior | Movement reason |\n|---|---|---|\n| `receive` | +qty available — purchase receipt, return, production produce | `receipt` |\n| `reserve` | available → allocated. Throws `InsufficientStockError` on overdraw | `reservation` |\n| `release` | allocated → available (order cancel) | `release` |\n| `fulfill` | -qty allocated. Stock leaves the building | `fulfillment` |\n| `transfer` | move stock between locations (writes two movements) | `transfer_out`, `transfer_in` |\n| `adjust` | signed delta — cycle counts and one-off corrections | `adjustment` |\n\nAll methods reject zero / negative / non-finite quantities except `adjust`, which accepts non-zero signed deltas. Negative deltas that would drive a state below zero throw `InsufficientStockError`.\n\n## LotService — lots and serials (additive, #3459)\n\n`createLotService({ db })` (with `withTransaction`, same pattern as `StockService`) is the only writer of the lot tables. `receiveLot` records identity only and moves no stock; compose with `StockService` in one transaction via `createLotService({ db: tx.db })`. Passing `lotId` to a `StockService` mutation applies the same deltas to `LotStockLevel`, enforces non-negative at lot level (`InsufficientStockError.lotId`), validates the lot (`LotNotFoundError`, `LotMismatchError` when `lot.skuId` differs), and stamps `StockMovement.lotId`.\n\n- `Lot.identityKey` is the only DB-unique key: `u:<kind>:<sku>:<SEARCHKEY>` for unique lots and all serials (hashed if over 160 chars), `k:<idempotencyKey>` for idempotent receipts, else a random `r:` key. So a lot number repeats by default (one supplier lot, two receipts); `DuplicateLotError` only for unique ones.\n- Corrections (`correctLotNumber`, `correctAttributes`, `setStatus`, `unlinkUsage`, `detachDocument`) require a reason, are refused when nothing changes, and write a `LotCorrection`. They `save()` the lot so concurrent changes conflict on its revision; retry the loser.\n- `recordUsage` is idempotent per consumer; `exclusive` (default for serials) throws `LotInUseError` for a second consumer.\n- `traceForward(lotId)` / `traceBack(consumerType, consumerId)` follow `consumerType: 'Lot'` usages with a path-based cycle guard and `depth` (default 10).\n- `StockService` does not consult `lot.status`: callers decide what a quarantined or void lot may do. A repeated `idempotencyKey` returns the first lot even if the other fields differ; a repeated `recordUsage` for the same consumer returns the existing usage unchanged (its `qty` is ignored).\n- No authorization inside the package; callers authorize. Nothing assumes an id format, so a consumer's data migration may insert rows with its own ids.\n- Schema upgrade is additive: five new tables and one nullable column, `inventory_stock_movements.lot_id`. `StockLevel`'s unique key is unchanged. Runtime never creates schema: generate and apply the normal migration first (`smrt doctor --db`); no data copy is needed.\n\n## Opt-in DispatchBus hooks\n\nOff by default. Wire them up explicitly in the application's `smrt.ts`:\n\n```typescript\nimport { installInventoryDispatchHandlers } from '@happyvertical/smrt-inventory';\n\nconst handlers = await installInventoryDispatchHandlers({\n dispatchBus: bus,\n db, // or stockService: existingService\n});\n```\n\nThis subscribes to:\n- `contract:created` → calls `service.reserve()` for every line inside one `stockService.withTransaction(...)`, attributed to `('Contract', payload.contractId)`\n- `fulfillment:shipped` → calls `service.fulfill()` for every line inside one `stockService.withTransaction(...)`, attributed to `('Fulfillment', payload.fulfillmentId)`\n\nBoth handlers are atomic across lines: a shortfall on line N rolls back lines 1..N-1 so the event is all-or-nothing. Without this guarantee, `DispatchBus` would only log the async handler error and a partially-reserved (or partially-fulfilled) contract would have no compensating event to fix it.\n\nPer-handler toggles: `installContractReserved`, `installFulfillmentShipped`. The `production_order:posted` hook is deliberately not installed here — it depends on the BOM model and ships in `@happyvertical/smrt-manufacturing` (issue #1250).\n\n## Schema migration (Phase 1 release)\n\nThe `Sku` model moved out of this package and into `@happyvertical/smrt-products`. Previously it lived in this package's `inventory_skus` table; it now lives in `product_skus` over there.\n\n**Upgrade procedure** for deployed consumers:\n\n1. **Let the framework create the destination table first.** Boot the new version once with `@happyvertical/smrt-products` registered in your `SmrtClassOptions`; the lazy `syncSchema` path will create `product_skus` with the right primary key, NOT NULL, UNIQUE (`code`, `tenant_id`), and indexes — they're derived from the `Sku` model decorators and would be stripped by a raw `CREATE TABLE AS SELECT`.\n\n2. **Idempotently copy rows across.** SQLite + Postgres:\n\n ```sql\n BEGIN;\n INSERT INTO product_skus (\n id, slug, context, created_at, updated_at,\n tenant_id, product_id, code, barcode, name,\n attributes, weight_grams, parent_sku_id, active\n )\n SELECT\n id, slug, context, created_at, updated_at,\n tenant_id, product_id, code, barcode, name,\n attributes, weight_grams, parent_sku_id, active\n FROM inventory_skus\n WHERE NOT EXISTS (\n SELECT 1 FROM product_skus p WHERE p.id = inventory_skus.id\n );\n COMMIT;\n ```\n\n3. **Drop the legacy table** once you've verified row counts match:\n\n ```sql\n DROP TABLE IF EXISTS inventory_skus;\n ```\n\n`StockLevel.skuId` and `StockMovement.skuId` carry plain string ids that still resolve to the same rows after the rename, so no inventory data has to move.\n\n## Gotchas\n\n- **Movements are append-only.** The materialized `StockLevel` row is derived state; the `StockMovement` ledger is the source of truth. Never let CRUD UIs expose update/delete on the movement table. If you find yourself wanting to \"fix\" a movement row, write a compensating `adjust()` call instead.\n- **Never call `StockLevel.create()` or `save()` directly.** Going around `StockService` silently desyncs the audit log and breaks cycle counts.\n- **Cross-industry constraint.** This package's vocabulary stays generic: `InventoryLocation`, `StockLevel`, `StockMovement`. Apparel-specific concepts (`Style`, `Makeup`, `Colorway`, fashion `Season`, tech-pack metadata) and their analogues in furniture / automotive / CPG live in the relevant template package, never here. PRs that introduce industry vocabulary should be rejected.\n- **Cross-package references are plain strings.** `skuId`, `placeId`, `sourceId` — all plain string ids, never `@foreignKey()`. Inventory's stock-motion logic only ever reads StockLevel/StockMovement; it doesn't follow `skuId` back to the catalog's Sku table, so the layering stays loose.\n- **`conflictColumns` include `tenant_id`** on `InventoryLocation` and `StockLevel`. NULL-matching semantics (`(code, NULL) IS NOT DISTINCT FROM (code, NULL)`) are handled by `@happyvertical/sql >= 0.74.0` natively, so two saves with the same `(code, NULL)` tuple correctly merge in place. Pass `nullsDistinct: true` at the sql layer to opt back into NULL-distinct semantics.\n- **Atomic per-method, transactional across composition.** Each `StockService` mutation (`receive` / `reserve` / `release` / `fulfill` / `transfer` / `adjust`) wraps every write in a single `db.transaction(...)` (via `@happyvertical/sql >= 0.74.0`). Partial failure rolls the whole call back — level writes and the matching `StockMovement` audit row commit together or not at all. `transfer` writes both legs (source decrement, destination increment, and both audit rows) inside one tx, so a failure mid-`transfer` is fully reverted.\n Callers composing multiple `StockService` calls into one logical unit (e.g. consuming materials line-by-line for a production order) should wrap them in `await stockService.withTransaction(async (tx) => { ... })` — `tx` is a tx-bound `StockService` with the same public API; mutation calls inside the callback share one transaction and either all commit or all roll back.\n If the underlying SQL adapter does not expose `transaction()` (extremely rare — all four built-in `@happyvertical/sql >= 0.74.0` adapters implement it), the service degrades to non-atomic serial writes and emits a one-time `console.warn`.\n- **Concurrent reservations: tightened, not bulletproof.** `@happyvertical/sql >= 0.74.0` serialises null-aware *upserts* via a per-key in-process lock (SQLite) or advisory lock (Postgres). That lock fixed the row-creation race (issue #1246) — two concurrent `create()` calls on the same conflict-column tuple no longer race the underlying `INSERT ... ON CONFLICT`. The first `StockLevel` row for a key is now created insert-only (#3204): a concurrent first write fails on the unique key and its transaction rolls back (retry it) instead of upserting over the winner's qty. It does NOT serialise the read-modify-write compound that `reserve` / `fulfill` / `transfer` / `adjust` perform on an existing row: each method reads the current level (`SELECT`), computes the new value in JS, then writes it back (`UPDATE` via `save()` on the loaded row). The whole compound is inside one `db.transaction(...)` from round-3, so the level write and audit row commit together — but two concurrent transactions on the same `(skuId, locationId)` can each see the same starting balance, compute the same decrement, and double-spend. Awaited (serial) calls always behave correctly. Use a job queue (e.g. `@happyvertical/smrt-jobs`) or your own mutex when you need hard atomicity across concurrent callers. The full fix is `SELECT ... FOR UPDATE` inside the same transaction (Postgres) or upgrading the journal mode (SQLite); deferred until we see a real high-contention workload that warrants it.\n\n## Source attribution\n\nEvery `StockMovement` carries `sourceType` + `sourceId` so downstream queries can reconstruct \"what caused this movement\". Conventions used by the built-in dispatch handlers:\n\n| sourceType | Emitter | Note |\n|---|---|---|\n| `Contract` | `smrt-commerce` `contract:created` | All reservation/release/fulfilment legs caused by a contract |\n| `Fulfillment` | `smrt-commerce` `fulfillment:shipped` | Outbound shipment |\n| `PurchaseOrder` | (your code) | Inbound receipt |\n| `CycleCount` | (your code) | Adjustments from physical counts |\n| `TransferOrder` | (your code) | Both legs of a transfer |\n| `ProductionOrder` | `smrt-manufacturing` (issue #1250) | Materials consumed + finished goods produced |\n\n## Replenishment and browser components\n\n`StockService.setReorderPolicy()` is the sanctioned metadata writer for nullable\n`StockLevel.reorderPoint` / `reorderQuantity` on the available SKU/location row.\nIt does not change quantity or emit a movement. `findBelowReorderPoint()` uses\npublic tenant-filtered collection reads and strict less-than semantics.\n`StockMutationOptions.actorProfileId` propagates to every movement, including\nboth transfer legs; null retains compatibility for automation. It is a qualified\ncross-package profile reference, not an authorization decision.\n\n`src/svelte/` exports data-driven components through `./svelte`. Server callers\nown authentication and persistence callbacks; never import backend models into\nthis entry or expose ledger mutations as CRUD. Package `typecheck` includes\nSvelte checks and `test` includes DOM component tests. See README for the public\nprops, service contracts, and additive schema upgrade requirements.\n",
1001
2258
  "recipes": [
1002
2259
  {
1003
2260
  "id": "inventory.stock",