@happyvertical/smrt-commerce 0.55.7 → 0.55.8

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 (47) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +34 -0
  3. package/agents/customer-vendor-ui.md +31 -0
  4. package/dist/index.d.ts +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +642 -240
  7. package/dist/index.js.map +1 -1
  8. package/dist/manifest.json +1449 -109
  9. package/dist/models/Contract.d.ts.map +1 -1
  10. package/dist/models/ContractLineItem.d.ts.map +1 -1
  11. package/dist/models/Customer.d.ts.map +1 -1
  12. package/dist/models/Fulfillment.d.ts.map +1 -1
  13. package/dist/models/FulfillmentLineItem.d.ts.map +1 -1
  14. package/dist/models/Invoice.d.ts.map +1 -1
  15. package/dist/models/InvoiceLineItem.d.ts.map +1 -1
  16. package/dist/models/Payment.d.ts.map +1 -1
  17. package/dist/models/PaymentAllocation.d.ts.map +1 -1
  18. package/dist/models/Vendor.d.ts.map +1 -1
  19. package/dist/playground.js +19 -0
  20. package/dist/playground.js.map +1 -1
  21. package/dist/recipes.d.ts +733 -19
  22. package/dist/recipes.d.ts.map +1 -1
  23. package/dist/smrt-knowledge.json +1147 -123
  24. package/dist/svelte/components/CustomerSelect.svelte +53 -0
  25. package/dist/svelte/components/CustomerSelect.svelte.d.ts +47 -0
  26. package/dist/svelte/components/CustomerSelect.svelte.d.ts.map +1 -0
  27. package/dist/svelte/components/PartySelect.svelte +138 -0
  28. package/dist/svelte/components/PartySelect.svelte.d.ts +70 -0
  29. package/dist/svelte/components/PartySelect.svelte.d.ts.map +1 -0
  30. package/dist/svelte/components/VendorSelect.svelte +53 -0
  31. package/dist/svelte/components/VendorSelect.svelte.d.ts +47 -0
  32. package/dist/svelte/components/VendorSelect.svelte.d.ts.map +1 -0
  33. package/dist/svelte/i18n.d.ts +8 -0
  34. package/dist/svelte/i18n.d.ts.map +1 -1
  35. package/dist/svelte/i18n.js +8 -0
  36. package/dist/svelte/index.d.ts +6 -0
  37. package/dist/svelte/index.d.ts.map +1 -1
  38. package/dist/svelte/index.js +6 -0
  39. package/dist/svelte/playground/PartySelectPreview.svelte +108 -0
  40. package/dist/svelte/playground/PartySelectPreview.svelte.d.ts +8 -0
  41. package/dist/svelte/playground/PartySelectPreview.svelte.d.ts.map +1 -0
  42. package/dist/svelte/playground.d.ts.map +1 -1
  43. package/dist/svelte/playground.js +19 -0
  44. package/dist/ui.d.ts.map +1 -1
  45. package/dist/ui.js +20 -0
  46. package/dist/ui.js.map +1 -1
  47. package/package.json +16 -12
@@ -3,14 +3,15 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-commerce",
6
- "packageVersion": "0.55.7",
6
+ "packageVersion": "0.55.8",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "4df0d8742511a1d86fb3ba87a46ae8021fc87b10796985af4ed6af1103fd0247",
11
- "packageJson": "6cda8f067b483bcf6e2760f207f92e039f7a35340d29f677abd12e4b1e4cf7ec",
12
- "agents": "67b16f7ce35487a5fecaca37b52edbdb66c166b2d99672ce56f08ea3f4a6f3f3",
10
+ "manifest": "fab00930e2edfdf36fb8808888b8211e960b7f986d0622ee83106e0d94eafcb0",
11
+ "packageJson": "f4998bf8791b5098e3a81439230337d8a40eb39d831d3cdd24e3b9e8578845e3",
12
+ "agents": "d7214590c1d2b6a7e47ee181833f9411eb9c8c5c6a0f325e0666e1723694fe22",
13
13
  "moduleDoc:agents/invoice-quantity.md": "bc3b617d4fb8cc87511763b281283107557ba64795e64522193ef967a3715d32",
14
+ "moduleDoc:agents/customer-vendor-ui.md": "1187b4d14e6bab661a2bb3312315239952dc955f90eceb177719f48dd0979150",
14
15
  "moduleDoc:agents/commercial-ui.md": "308729a481c82903d06837e7052c04509a92cf17d17345f71dce88d8d42d0431",
15
16
  "moduleDoc:agents/quote-ui.md": "514568e4e7815c47113627ac044a1f31a6933377b3f6c70f5d54b2508d94200c",
16
17
  "moduleDoc:agents/purchasing-ui.md": "51ab713d89949c9779973191dfa3529e5120d087321236fc48ad1948c88f9866"
@@ -36,7 +37,11 @@
36
37
  "@happyvertical/smrt-vitest": "workspace:*",
37
38
  "@sveltejs/package": "^2.5.8",
38
39
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
40
+ "@testing-library/jest-dom": "^7.0.1",
41
+ "@testing-library/svelte": "^5.4.2",
42
+ "@testing-library/user-event": "^14.6.7",
39
43
  "@types/node": "24.13.6",
44
+ "axe-core": "^4.13.0",
40
45
  "fast-glob": "^3.3.3",
41
46
  "svelte": "^5.57.0",
42
47
  "svelte-check": "^4.7.6",
@@ -4028,7 +4033,8 @@
4028
4033
  "name": "contractType",
4029
4034
  "type": "text",
4030
4035
  "required": false,
4031
- "columnType": "TEXT"
4036
+ "columnType": "TEXT",
4037
+ "description": "The kind of contract: order, estimate, lease, agreement and so on."
4032
4038
  },
4033
4039
  {
4034
4040
  "name": "status",
@@ -4422,7 +4428,8 @@
4422
4428
  "name": "contractType",
4423
4429
  "type": "text",
4424
4430
  "required": false,
4425
- "columnType": "TEXT"
4431
+ "columnType": "TEXT",
4432
+ "description": "The kind of contract: order, estimate, lease, agreement and so on."
4426
4433
  },
4427
4434
  {
4428
4435
  "name": "status",
@@ -4878,7 +4885,8 @@
4878
4885
  "name": "contractType",
4879
4886
  "type": "text",
4880
4887
  "required": false,
4881
- "columnType": "TEXT"
4888
+ "columnType": "TEXT",
4889
+ "description": "The kind of contract: order, estimate, lease, agreement and so on."
4882
4890
  },
4883
4891
  {
4884
4892
  "name": "status",
@@ -5334,7 +5342,8 @@
5334
5342
  "name": "contractType",
5335
5343
  "type": "text",
5336
5344
  "required": false,
5337
- "columnType": "TEXT"
5345
+ "columnType": "TEXT",
5346
+ "description": "The kind of contract: order, estimate, lease, agreement and so on."
5338
5347
  },
5339
5348
  {
5340
5349
  "name": "status",
@@ -5790,7 +5799,8 @@
5790
5799
  "name": "contractType",
5791
5800
  "type": "text",
5792
5801
  "required": false,
5793
- "columnType": "TEXT"
5802
+ "columnType": "TEXT",
5803
+ "description": "The kind of contract: order, estimate, lease, agreement and so on."
5794
5804
  },
5795
5805
  {
5796
5806
  "name": "status",
@@ -6246,7 +6256,8 @@
6246
6256
  "name": "contractType",
6247
6257
  "type": "text",
6248
6258
  "required": false,
6249
- "columnType": "TEXT"
6259
+ "columnType": "TEXT",
6260
+ "description": "The kind of contract: order, estimate, lease, agreement and so on."
6250
6261
  },
6251
6262
  {
6252
6263
  "name": "status",
@@ -6702,7 +6713,8 @@
6702
6713
  "name": "contractType",
6703
6714
  "type": "text",
6704
6715
  "required": false,
6705
- "columnType": "TEXT"
6716
+ "columnType": "TEXT",
6717
+ "description": "The kind of contract: order, estimate, lease, agreement and so on."
6706
6718
  },
6707
6719
  {
6708
6720
  "name": "status",
@@ -7158,7 +7170,8 @@
7158
7170
  "name": "contractType",
7159
7171
  "type": "text",
7160
7172
  "required": false,
7161
- "columnType": "TEXT"
7173
+ "columnType": "TEXT",
7174
+ "description": "The kind of contract: order, estimate, lease, agreement and so on."
7162
7175
  },
7163
7176
  {
7164
7177
  "name": "status",
@@ -7626,7 +7639,8 @@
7626
7639
  "name": "contractType",
7627
7640
  "type": "text",
7628
7641
  "required": false,
7629
- "columnType": "TEXT"
7642
+ "columnType": "TEXT",
7643
+ "description": "The kind of contract: order, estimate, lease, agreement and so on."
7630
7644
  },
7631
7645
  {
7632
7646
  "name": "status",
@@ -8082,7 +8096,8 @@
8082
8096
  "name": "contractType",
8083
8097
  "type": "text",
8084
8098
  "required": false,
8085
- "columnType": "TEXT"
8099
+ "columnType": "TEXT",
8100
+ "description": "The kind of contract: order, estimate, lease, agreement and so on."
8086
8101
  },
8087
8102
  {
8088
8103
  "name": "status",
@@ -8679,28 +8694,32 @@
8679
8694
  "type": "foreignKey",
8680
8695
  "required": false,
8681
8696
  "related": "Contract",
8682
- "columnType": "UUID"
8697
+ "columnType": "UUID",
8698
+ "description": "The order, quote or agreement this line belongs to."
8683
8699
  },
8684
8700
  {
8685
8701
  "name": "description",
8686
8702
  "type": "text",
8687
8703
  "required": false,
8688
8704
  "columnType": "TEXT",
8689
- "default": ""
8705
+ "default": "",
8706
+ "description": "What is being sold or bought on this line."
8690
8707
  },
8691
8708
  {
8692
8709
  "name": "quantity",
8693
8710
  "type": "decimal",
8694
8711
  "required": false,
8695
8712
  "columnType": "REAL",
8696
- "default": 1
8713
+ "default": 1,
8714
+ "description": "How many."
8697
8715
  },
8698
8716
  {
8699
8717
  "name": "unitPrice",
8700
8718
  "type": "integer",
8701
8719
  "required": false,
8702
8720
  "columnType": "INTEGER",
8703
- "default": 0
8721
+ "default": 0,
8722
+ "description": "The price of one, before tax."
8704
8723
  },
8705
8724
  {
8706
8725
  "name": "discount",
@@ -8714,21 +8733,24 @@
8714
8733
  "type": "decimal",
8715
8734
  "required": false,
8716
8735
  "columnType": "REAL",
8717
- "default": 0
8736
+ "default": 0,
8737
+ "description": "The tax rate for this line, as a percentage (5 means 5%)."
8718
8738
  },
8719
8739
  {
8720
8740
  "name": "amount",
8721
8741
  "type": "integer",
8722
8742
  "required": false,
8723
8743
  "columnType": "INTEGER",
8724
- "default": 0
8744
+ "default": 0,
8745
+ "description": "The line total."
8725
8746
  },
8726
8747
  {
8727
8748
  "name": "productId",
8728
8749
  "type": "crossPackageRef",
8729
8750
  "required": false,
8730
8751
  "related": "@happyvertical/smrt-products:Product",
8731
- "columnType": "UUID"
8752
+ "columnType": "UUID",
8753
+ "description": "The product this line is for, if it is one you list."
8732
8754
  },
8733
8755
  {
8734
8756
  "name": "sku",
@@ -9231,33 +9253,38 @@
9231
9253
  "type": "foreignKey",
9232
9254
  "required": false,
9233
9255
  "related": "Contract",
9234
- "columnType": "UUID"
9256
+ "columnType": "UUID",
9257
+ "description": "The order being fulfilled."
9235
9258
  },
9236
9259
  {
9237
9260
  "name": "fulfillmentType",
9238
9261
  "type": "text",
9239
9262
  "required": false,
9240
- "columnType": "TEXT"
9263
+ "columnType": "TEXT",
9264
+ "description": "How it gets to the customer: shipment, delivery, pickup, digital or service."
9241
9265
  },
9242
9266
  {
9243
9267
  "name": "status",
9244
9268
  "type": "text",
9245
9269
  "required": false,
9246
- "columnType": "TEXT"
9270
+ "columnType": "TEXT",
9271
+ "description": "Where it stands: pending, processing, shipped, delivered or cancelled."
9247
9272
  },
9248
9273
  {
9249
9274
  "name": "trackingNumber",
9250
9275
  "type": "text",
9251
9276
  "required": false,
9252
9277
  "columnType": "TEXT",
9253
- "default": ""
9278
+ "default": "",
9279
+ "description": "The carrier's tracking number."
9254
9280
  },
9255
9281
  {
9256
9282
  "name": "carrier",
9257
9283
  "type": "text",
9258
9284
  "required": false,
9259
9285
  "columnType": "TEXT",
9260
- "default": ""
9286
+ "default": "",
9287
+ "description": "The company carrying it."
9261
9288
  },
9262
9289
  {
9263
9290
  "name": "shippingAddress",
@@ -9269,13 +9296,15 @@
9269
9296
  "name": "shippedAt",
9270
9297
  "type": "datetime",
9271
9298
  "required": false,
9272
- "columnType": "TIMESTAMP"
9299
+ "columnType": "TIMESTAMP",
9300
+ "description": "When it was dispatched."
9273
9301
  },
9274
9302
  {
9275
9303
  "name": "deliveredAt",
9276
9304
  "type": "datetime",
9277
9305
  "required": false,
9278
- "columnType": "TIMESTAMP"
9306
+ "columnType": "TIMESTAMP",
9307
+ "description": "When it arrived."
9279
9308
  },
9280
9309
  {
9281
9310
  "name": "estimatedDelivery",
@@ -9288,7 +9317,8 @@
9288
9317
  "type": "text",
9289
9318
  "required": false,
9290
9319
  "columnType": "TEXT",
9291
- "default": ""
9320
+ "default": "",
9321
+ "description": "Anything worth remembering about this shipment."
9292
9322
  }
9293
9323
  ],
9294
9324
  "relationships": [
@@ -9557,21 +9587,24 @@
9557
9587
  "type": "foreignKey",
9558
9588
  "required": false,
9559
9589
  "related": "Fulfillment",
9560
- "columnType": "UUID"
9590
+ "columnType": "UUID",
9591
+ "description": "The shipment this line belongs to."
9561
9592
  },
9562
9593
  {
9563
9594
  "name": "contractLineItemId",
9564
9595
  "type": "foreignKey",
9565
9596
  "required": false,
9566
9597
  "related": "ContractLineItem",
9567
- "columnType": "UUID"
9598
+ "columnType": "UUID",
9599
+ "description": "The order line being sent."
9568
9600
  },
9569
9601
  {
9570
9602
  "name": "quantityFulfilled",
9571
9603
  "type": "decimal",
9572
9604
  "required": false,
9573
9605
  "columnType": "REAL",
9574
- "default": 1
9606
+ "default": 1,
9607
+ "description": "How many of that line are in this shipment."
9575
9608
  },
9576
9609
  {
9577
9610
  "name": "notes",
@@ -9737,21 +9770,24 @@
9737
9770
  "type": "foreignKey",
9738
9771
  "required": false,
9739
9772
  "related": "Customer",
9740
- "columnType": "UUID"
9773
+ "columnType": "UUID",
9774
+ "description": "The customer being billed."
9741
9775
  },
9742
9776
  {
9743
9777
  "name": "contractId",
9744
9778
  "type": "foreignKey",
9745
9779
  "required": false,
9746
9780
  "related": "Contract",
9747
- "columnType": "UUID"
9781
+ "columnType": "UUID",
9782
+ "description": "The order or agreement this invoice bills for, if any."
9748
9783
  },
9749
9784
  {
9750
9785
  "name": "invoiceNumber",
9751
9786
  "type": "text",
9752
9787
  "required": false,
9753
9788
  "columnType": "TEXT",
9754
- "default": ""
9789
+ "default": "",
9790
+ "description": "The number printed on the invoice."
9755
9791
  },
9756
9792
  {
9757
9793
  "name": "reference",
@@ -9764,33 +9800,38 @@
9764
9800
  "name": "issueDate",
9765
9801
  "type": "datetime",
9766
9802
  "required": false,
9767
- "columnType": "TIMESTAMP"
9803
+ "columnType": "TIMESTAMP",
9804
+ "description": "The date the invoice was issued."
9768
9805
  },
9769
9806
  {
9770
9807
  "name": "dueDate",
9771
9808
  "type": "datetime",
9772
9809
  "required": false,
9773
- "columnType": "TIMESTAMP"
9810
+ "columnType": "TIMESTAMP",
9811
+ "description": "The date payment is expected."
9774
9812
  },
9775
9813
  {
9776
9814
  "name": "paidDate",
9777
9815
  "type": "datetime",
9778
9816
  "required": false,
9779
- "columnType": "TIMESTAMP"
9817
+ "columnType": "TIMESTAMP",
9818
+ "description": "The date it was paid in full."
9780
9819
  },
9781
9820
  {
9782
9821
  "name": "subtotal",
9783
9822
  "type": "integer",
9784
9823
  "required": false,
9785
9824
  "columnType": "INTEGER",
9786
- "default": 0
9825
+ "default": 0,
9826
+ "description": "The amount before tax."
9787
9827
  },
9788
9828
  {
9789
9829
  "name": "taxAmount",
9790
9830
  "type": "integer",
9791
9831
  "required": false,
9792
9832
  "columnType": "INTEGER",
9793
- "default": 0
9833
+ "default": 0,
9834
+ "description": "The tax charged."
9794
9835
  },
9795
9836
  {
9796
9837
  "name": "providerTaxAmount",
@@ -9804,27 +9845,31 @@
9804
9845
  "type": "integer",
9805
9846
  "required": false,
9806
9847
  "columnType": "INTEGER",
9807
- "default": 0
9848
+ "default": 0,
9849
+ "description": "What the customer owes in all, tax included."
9808
9850
  },
9809
9851
  {
9810
9852
  "name": "amountPaid",
9811
9853
  "type": "integer",
9812
9854
  "required": false,
9813
9855
  "columnType": "INTEGER",
9814
- "default": 0
9856
+ "default": 0,
9857
+ "description": "How much of the total has been paid so far."
9815
9858
  },
9816
9859
  {
9817
9860
  "name": "currency",
9818
9861
  "type": "text",
9819
9862
  "required": false,
9820
9863
  "columnType": "TEXT",
9821
- "default": "CAD"
9864
+ "default": "CAD",
9865
+ "description": "The currency of the invoice."
9822
9866
  },
9823
9867
  {
9824
9868
  "name": "status",
9825
9869
  "type": "text",
9826
9870
  "required": false,
9827
- "columnType": "TEXT"
9871
+ "columnType": "TEXT",
9872
+ "description": "Where the invoice stands: draft, sent, viewed, paid, overdue or cancelled."
9828
9873
  },
9829
9874
  {
9830
9875
  "name": "arJournalId",
@@ -9904,21 +9949,24 @@
9904
9949
  "type": "text",
9905
9950
  "required": false,
9906
9951
  "columnType": "TEXT",
9907
- "default": ""
9952
+ "default": "",
9953
+ "description": "Private notes, not shown to the customer."
9908
9954
  },
9909
9955
  {
9910
9956
  "name": "customerNotes",
9911
9957
  "type": "text",
9912
9958
  "required": false,
9913
9959
  "columnType": "TEXT",
9914
- "default": ""
9960
+ "default": "",
9961
+ "description": "A message shown to the customer on the invoice."
9915
9962
  },
9916
9963
  {
9917
9964
  "name": "terms",
9918
9965
  "type": "text",
9919
9966
  "required": false,
9920
9967
  "columnType": "TEXT",
9921
- "default": ""
9968
+ "default": "",
9969
+ "description": "The payment conditions, such as Net 30."
9922
9970
  }
9923
9971
  ],
9924
9972
  "relationships": [
@@ -10358,14 +10406,16 @@
10358
10406
  "type": "foreignKey",
10359
10407
  "required": false,
10360
10408
  "related": "Invoice",
10361
- "columnType": "UUID"
10409
+ "columnType": "UUID",
10410
+ "description": "The invoice this line belongs to."
10362
10411
  },
10363
10412
  {
10364
10413
  "name": "description",
10365
10414
  "type": "text",
10366
10415
  "required": false,
10367
10416
  "columnType": "TEXT",
10368
- "default": ""
10417
+ "default": "",
10418
+ "description": "What is being billed on this line."
10369
10419
  },
10370
10420
  {
10371
10421
  "name": "sku",
@@ -10379,7 +10429,8 @@
10379
10429
  "type": "decimal",
10380
10430
  "required": false,
10381
10431
  "columnType": "REAL",
10382
- "default": 1
10432
+ "default": 1,
10433
+ "description": "How many."
10383
10434
  },
10384
10435
  {
10385
10436
  "name": "invoiceEditorStateJson",
@@ -10393,7 +10444,8 @@
10393
10444
  "type": "integer",
10394
10445
  "required": false,
10395
10446
  "columnType": "INTEGER",
10396
- "default": 0
10447
+ "default": 0,
10448
+ "description": "The price of one, before tax."
10397
10449
  },
10398
10450
  {
10399
10451
  "name": "discount",
@@ -10407,14 +10459,16 @@
10407
10459
  "type": "decimal",
10408
10460
  "required": false,
10409
10461
  "columnType": "REAL",
10410
- "default": 0
10462
+ "default": 0,
10463
+ "description": "The tax rate for this line, as a percentage (5 means 5%)."
10411
10464
  },
10412
10465
  {
10413
10466
  "name": "amount",
10414
10467
  "type": "integer",
10415
10468
  "required": false,
10416
10469
  "columnType": "INTEGER",
10417
- "default": 0
10470
+ "default": 0,
10471
+ "description": "The line total."
10418
10472
  },
10419
10473
  {
10420
10474
  "name": "sourceType",
@@ -10768,40 +10822,46 @@
10768
10822
  "type": "foreignKey",
10769
10823
  "required": false,
10770
10824
  "related": "Contract",
10771
- "columnType": "UUID"
10825
+ "columnType": "UUID",
10826
+ "description": "The order or agreement this payment is for, if any."
10772
10827
  },
10773
10828
  {
10774
10829
  "name": "customerId",
10775
10830
  "type": "foreignKey",
10776
10831
  "required": false,
10777
10832
  "related": "Customer",
10778
- "columnType": "UUID"
10833
+ "columnType": "UUID",
10834
+ "description": "The customer who paid."
10779
10835
  },
10780
10836
  {
10781
10837
  "name": "amount",
10782
10838
  "type": "integer",
10783
10839
  "required": false,
10784
10840
  "columnType": "INTEGER",
10785
- "default": 0
10841
+ "default": 0,
10842
+ "description": "How much was paid."
10786
10843
  },
10787
10844
  {
10788
10845
  "name": "currency",
10789
10846
  "type": "text",
10790
10847
  "required": false,
10791
10848
  "columnType": "TEXT",
10792
- "default": "USD"
10849
+ "default": "USD",
10850
+ "description": "The currency of the payment."
10793
10851
  },
10794
10852
  {
10795
10853
  "name": "method",
10796
10854
  "type": "text",
10797
10855
  "required": false,
10798
- "columnType": "TEXT"
10856
+ "columnType": "TEXT",
10857
+ "description": "How it was paid: cash, check, card, bank transfer or other."
10799
10858
  },
10800
10859
  {
10801
10860
  "name": "status",
10802
10861
  "type": "text",
10803
10862
  "required": false,
10804
- "columnType": "TEXT"
10863
+ "columnType": "TEXT",
10864
+ "description": "Whether the payment is pending, completed, failed or refunded."
10805
10865
  },
10806
10866
  {
10807
10867
  "name": "transactionId",
@@ -10815,7 +10875,8 @@
10815
10875
  "type": "text",
10816
10876
  "required": false,
10817
10877
  "columnType": "TEXT",
10818
- "default": ""
10878
+ "default": "",
10879
+ "description": "A reference note, such as a check number."
10819
10880
  },
10820
10881
  {
10821
10882
  "name": "journalId",
@@ -10828,14 +10889,16 @@
10828
10889
  "name": "paidAt",
10829
10890
  "type": "datetime",
10830
10891
  "required": false,
10831
- "columnType": "TIMESTAMP"
10892
+ "columnType": "TIMESTAMP",
10893
+ "description": "When the money was received."
10832
10894
  },
10833
10895
  {
10834
10896
  "name": "notes",
10835
10897
  "type": "text",
10836
10898
  "required": false,
10837
10899
  "columnType": "TEXT",
10838
- "default": ""
10900
+ "default": "",
10901
+ "description": "Anything worth remembering about this payment."
10839
10902
  },
10840
10903
  {
10841
10904
  "name": "externalId",
@@ -11221,27 +11284,31 @@
11221
11284
  "type": "foreignKey",
11222
11285
  "required": false,
11223
11286
  "related": "Payment",
11224
- "columnType": "UUID"
11287
+ "columnType": "UUID",
11288
+ "description": "The payment being applied."
11225
11289
  },
11226
11290
  {
11227
11291
  "name": "invoiceId",
11228
11292
  "type": "foreignKey",
11229
11293
  "required": false,
11230
11294
  "related": "Invoice",
11231
- "columnType": "UUID"
11295
+ "columnType": "UUID",
11296
+ "description": "The invoice it settles."
11232
11297
  },
11233
11298
  {
11234
11299
  "name": "amount",
11235
11300
  "type": "integer",
11236
11301
  "required": false,
11237
11302
  "columnType": "INTEGER",
11238
- "default": 0
11303
+ "default": 0,
11304
+ "description": "How much of the payment goes to this invoice."
11239
11305
  },
11240
11306
  {
11241
11307
  "name": "allocatedAt",
11242
11308
  "type": "datetime",
11243
11309
  "required": false,
11244
- "columnType": "TIMESTAMP"
11310
+ "columnType": "TIMESTAMP",
11311
+ "description": "When it was applied."
11245
11312
  },
11246
11313
  {
11247
11314
  "name": "allocatedBy",
@@ -11255,7 +11322,8 @@
11255
11322
  "type": "text",
11256
11323
  "required": false,
11257
11324
  "columnType": "TEXT",
11258
- "default": ""
11325
+ "default": "",
11326
+ "description": "Anything worth remembering about this allocation."
11259
11327
  }
11260
11328
  ],
11261
11329
  "relationships": [
@@ -19699,13 +19767,18 @@
19699
19767
  "polymorphicAssociations": 0,
19700
19768
  "uuidColumns": 162
19701
19769
  },
19702
- "agentDoc": "# @happyvertical/smrt-commerce\n\nE-commerce with Contract STI hierarchy, invoice lifecycle, payment tracking, payout remittance, and optional ledger integration.\n\n## Models\n\n- **Customer** / **Vendor**: linked to Profile via string ID (not FK). Customer has creditLimit, paymentTerms, customerType (DTC / WHOLESALE / RETAIL). Vendor has leadTimeDays, minimumOrder, and `payoutAddresses: Record<string, string>` — a flat map from payout-rail-qualified currency code (`USDC-base`, `BTC`, `USD-stripe`, ...) to destination string (EVM address, BTC address, Stripe Connect account id, IBAN). Use `getPayoutAddress(currency)` for a `Map.get`-style lookup; missing entries return `undefined` (caller decides skip-vs-error).\n- **Contract** (STI base → Estimate, Order, Lease, Agreement, PurchaseOrder, WholesaleOrder, ProductionOrder, Cart, LicenseSale): 9 contract types sharing one table. Carries `channelId` (open-ended string — `dtc-web`, `wholesale-b2b`, `pos-store-N`, etc.) so the same model serves DTC checkout, B2B portals, and POS.\n- **WholesaleOrder**: B2B order. Conventional pairing — customer has `customerType: 'wholesale'`, NET-30/60 terms, delivered via wholesale-portal channel.\n- **ProductionOrder**: manufacturing equivalent of a PurchaseOrder — commission your factory to make finished goods. Consumes raw materials per BOM (`@happyvertical/smrt-manufacturing`) and produces SKU stock (`@happyvertical/smrt-inventory`).\n- **Cart**: transient order-in-progress. Same shape as Order; the application promotes the row from `_meta_type: Cart` to `_meta_type: Order` at checkout instead of copying data between tables.\n- **LicenseSale**: industry-neutral licensing primitive. Carries an *immutable* rights snapshot (`rightsMedium`, `rightsDistributionScope`, `rightsExclusivity`, `rightsDuration`, `rightsTerritory`, `rightsSublicensing`, `rightsDerivatives` — typed `Meta<T>` fields), licensee identity (`licenseeEmail`, optional `licenseeLegalEntity` / `licenseeJurisdiction`), and a signed-PDF reference (`pdfUrl`, `pdfHash`, optional `onChainHashRegistryRef`). Once saved at `ContractStatus.ACCEPTED`, the rights snapshot is frozen — mutating any of the seven rights fields and re-saving throws. The only legal transition out of ACCEPTED is `revoke()` (moves to CANCELLED without touching rights). Useful for stock media, music licensing, code-asset marketplaces, license keys, anywhere rights are sold for a fee.\n- **ContractLineItem**: items on contracts.\n- **Invoice**: status machine `DRAFT → SENT → VIEWED → PARTIAL → PAID` (also OVERDUE, CANCELLED, WRITTEN_OFF). `recognizeRevenue()` creates balanced AR journal entry (DR: Accounts Receivable, CR: Revenue, CR: Tax Payable).\n- **InvoiceLineItem**: fractional-quantity line items with safe minor-unit validation/recalculation on save; editor-mode recovery and existing-column migration are documented in [invoice quantity persistence](agents/invoice-quantity.md).\n- **Payment** / **PaymentAllocation**: tracks payments against invoices. Status controlled by `Invoice.updatePaymentStatus()`, not Payment model. Carries optional backend-adapter fields for PaymentBackend-routed flows: `backendId` (rail adapter id — `base-usdc`, `btc`, `stripe`, distinct from `externalProvider` which names an accounting sync destination), `backendTxRef` (chain tx hash or gateway settlement id), `nativeAmount` / `nativeCurrency` (what actually arrived), `usdAtQuote` / `usdAtConfirmation` (drift accounting for volatile-currency rails). `Payment.usdDrift()` returns the confirmation - quote delta, or `0` when either side is unset.\n- **PaymentInstrument**: reusable saved payment method (\"card on file\"). Stores only provider references and non-sensitive display metadata (`providerCustomerId`, `providerPaymentMethodId`, brand / last4 / expiry), never raw card data. Use `PaymentInstrumentCollection.setDefaultForCustomer()` to change the default so the single-default-per-customer invariant is enforced; generated create/update routes cannot write `isDefault` directly.\n- **PaymentIntent**: short-lived pre-payment commitment with multi-option semantics. Locks a USD price for a fixed window (default 15 minutes) and lists one or more `PaymentOption`s describing different rails (`backendId`, `currency`, `payTo`, `nativeAmount`, optional `chain` / `memo` / `x402Capable` / `expiresAt`). First option to receive payment wins; the others are implicitly retired. State machine `awaiting_payment → paid → (issued | retired)` plus `expired` and `cancelled`. Mutate via dedicated `markPaid` / `markIssued` / `expire` / `cancel` / `retire` helpers — direct status assignment bypasses the invariant checks. Idempotency via natural key `(tenant_id, offering_ref, licensee_email, idempotency_key)` and `PaymentIntentCollection.getOrCreateByIdempotencyKey()`.\n- **Payout**: operator-to-supplier remittance. Distinct from Payment because direction, status machine, and chain semantics differ. Status machine `pending → sent → confirmed → failed` (failed is terminal but resettable via `resetFromFailed()` after fixing the underlying problem). References source `paymentId` (plain string) and destination `vendorId` (foreign key). Amount invariant `supplierNet === grossAmount - operatorFee` enforced exactly on save (integer minor units, no tolerance — #2401). `PayoutCollection.createFromPayment()` is the typical entry point; it pulls native amount / currency from the source Payment.\n- **Fulfillment** / **FulfillmentLineItem**: shipment/delivery tracking.\n\n## Recipes (#3590)\n\n`src/recipes.ts` declares four minimal `SmrtRecipe`s, emitted into the built\n`manifest.json` and `smrt-knowledge.json` `recipes`: `commerce.customers`\n(Customer), `commerce.vendors` (Vendor), `commerce.sales` (Order, requires\ncustomers), `commerce.purchases` (PurchaseOrder, requires vendors). Headers\nonly; line items, fulfilment, invoices, and payments are later recipes. Each\nhas user-facing help in `src/<name>.recipe.md` (#3591), and the fields those\nrecipes show carry `@field({ description })` text; keep both in plain\nlanguage for end users and update them when a recipe's fields change. See\n[core recipes](../core/agents/recipes.md).\n\n## Ledger Integration\n\n`@happyvertical/smrt-ledgers` is a regular dependency, loaded lazily via dynamic `import()` so the coupling stays runtime-only (no hard static import; the package graph stays a DAG — see #1582). Invoice stores `arJournalId` and `revenueJournalId` as string references. `recognizeRevenue()` creates a balanced AR journal entry; `getArJournal()` returns null when no journal has been recognized yet.\n\n## Billing-period close (#3060)\n\n`src/billing/` closes periods from smrt-subscriptions charges; see the README\nfor usage.\n\n- **Port, not provider.** Period close, events, and credit checkout call only\n the `BillingProvider` port (integer minor units). `stripe.ts` adapts\n `@happyvertical/accounting`, which takes currency *major* units; convert with\n `units.ts` and never hand-roll a Stripe call here. `units.ts` exponents are\n an explicit ISO 4217 table matching the SDK's, never `Intl` display digits\n (CLDR differs for IQD, MGA, … and drifts by ICU release). The SDK's\n `*Minor` fields are already ISO minor units. Capabilities a rail may lack\n (`chargeSavedPaymentMethod`, `createSetupCheckout`, `getCheckout`,\n `setDefaultPaymentMethod`, `markInvoiceUncollectible`) are optional port\n methods: detect them by presence, and keep additions optional (#3139).\n `sendAndReconcile` refuses a provider invoice whose subtotal or total\n disagrees with the local invoice.\n- **Replay safety is identity.** Close, invoice, line, payment, allocation,\n and claim ids are `deterministicId()`s; `BillingLineSource` (conflict key\n `source_type, source_id`) is the only double-billing guard, so never bill a\n source without claiming it, and never widen that key. Each close step is\n persisted before the next; `acquireCloseLease()` serializes workers. A\n close that nets to a credit keeps its claims and ends `carried_forward`;\n its subtotal is a `credit_carry_forward` source for the payer's next billed\n close, which marks the carried close `completed`.\n- **Cycles and flat-plan coverage (#3116).** `cycles.ts` is the schedule\n (pure; `BillingAccount.billingAnchorAt`, null = calendar months). A close\n with no period closes each payer's `lastEndedBillingPeriod()`. Flat-plan\n claims are the exception to \"id from the source\": their `sourceId` is\n `<subscriptionId>:seq:<chainSequence>` and their `periodStart/End` is the\n exact time they bill. `claimFlatWindow()` reads the last number, then the\n coverage (claims ending after the window start), claims one gap as number\n n + 1, and retries on conflict; reading in that order is what makes it\n safe. That numbering, not the period, stops overlaps when schedules or\n payers change — never claim a flat plan outside it, and keep both reads\n bounded (no whole-history scans). Each numbered claim is its own invoice\n line keyed `lineKey|periodStart` (a function of the claim alone, so a\n resumed close rebuilds the same line ids). Proration is\n `prorateMinorUnits()` (half up, BigInt) against the period's full cycle.\n- **System tables.** `BillingAccount`, `BillingPeriodClose`, and\n `BillingLineSource` are not tenant-scoped and have no generated surface.\n Collections stay unexported; models are root exports (#3082). Invoices,\n payments, and journals are written in the seller's tenant; payer-owned\n subscriptions and credit grants in a system context, only after\n verification.\n- **Events** use smrt-jobs' delivery inbox under provider\n `<name>-billing:<sellerTenantId>` and a provider-filtered\n `ForgeProjectionRuntime`, so a runtime never claims another seller's\n events; any other runtime sharing the inbox must pass `providers` too. Only\n events this package acts on are stored: foreign checkouts are `ignored`,\n and only `smrt_*` checkout metadata is kept; it is\n HMAC-signed with the webhook secret at creation (empty values are omitted:\n Stripe drops them), so another integration on the same account cannot forge\n a credit purchase or a card setup; credit is granted only when the\n collected total equals it plus provider tax. Checkout amounts come from the\n re-read session (`getCheckout()`), never the Stripe event, whose amounts are\n in Stripe's unit. Rotating the webhook secret strands in-flight checkouts\n (up to 24 h). `observe()` re-reads provider state, so ordering never depends\n on delivery order; the only provider writes in `observe()` are idempotent\n (making a saved card the default). The standing hook runs inside the event\n transaction and must be idempotent.\n- **Card on file and top-ups (#3139).** A saved card is the payer customer's\n default `PaymentInstrument` (seller tenant, id from the provider method).\n `autoChargeInvoices` bills such payers `charge_automatically`, decided once\n and saved as `Invoice.collectionMethod` before the first push, which every\n retry reuses (never recompute it: a lost push response would record a\n method the provider was not asked for, #3190); activation follows `paid`, never send. An auto top-up attempt\n is a `PENDING` `Payment` (`reference` `auto-top-up:<policyId>`, id from the\n policy and `count()` of its attempts) inserted before the charge, so racing\n evaluations derive one id and one charge; the charge key is derived from\n that id. `applyAutoTopUpOutcome()` must run in a transaction and starts with\n a conditional `UPDATE … WHERE status = 'pending'` row lock, so the hook and\n the `payment` webhook settle once; the grant is keyed by the charge key.\n Never settle an attempt outside that function. Taxed accounts are charged\n with provider tax by default (#3194): the attempt row holds the credit\n (the charge subtotal), the tax is journaled to tax payable at settlement,\n and the attempt's tax decision is recorded in its `notes` so a re-drive\n repeats the same charge request (the provider refuses a key reused with\n different tax). `claimAttempt()` is the only place that decides a\n skip and reports it (`onAutoTopUpSkipped`); `cardOnFile()` and period close\n use `defaultCardInstrument()` — never copy that predicate. A provider write-off moves the local invoice to\n `WRITTEN_OFF` (a later payment, by card or on a rail (#3188), is recorded\n and reinstates the payer; the status stays, so \"paid in full\" for a\n written-off invoice is read from its allocations).\n- **`Invoice.providerTaxAmount`** is added to line-item tax; it is\n server-managed (not API-writable). `toAccountingInput()` emits major units.\n- **Payment rails (#3138).** A runtime has one issuing `provider` and any\n number of `paymentProviders` (unique names, one inbox namespace each; a\n provider with `capabilities.issuesInvoices === false` can never issue).\n `observe()` resolves the provider from the delivery namespace, never the\n payload. Crypto rails implement the port over `@happyvertical/payments`'\n `CryptoCheckoutGateway`; never call BTCPay (or any gateway) from here.\n Settlement is decided by the pure `decideAttempt()` from gateway state and\n `paymentPolicy` — the gateway's `settled` is authoritative and nothing here\n counts confirmations. A `BillingPaymentAttempt` (`(provider, checkout_id)`\n deterministic id) records every state and settles once; grants and payments\n are keyed by checkout id. Invoice payments close the issuer's invoice out of\n band in `observe()` (idempotent) before `project()` records the payment, and\n `settlePaidInvoice` skips (records nothing for) a `paidOutOfBand` invoice\n the rail latched with `outOfBandRequestedAt`; other out-of-band closes are\n recorded as `OTHER` payments. Rails never save cards or charge tax: a taxed\n credit purchase on a rail needs an explicit `automaticTax: false`. Exceptions are flags for operators;\n nothing reverses or refunds automatically.\n- **Known limits**: write-offs post no bad-debt journal (AR stays until paid\n or journaled by an operator); discounts still reach Stripe as negative lines (the SDK's\n line `discount` would change the lines a replayed push reconciles, so it\n needs its own migration); `RetailCharge` has no adjustment ledger. Flat\n plans are monthly, in arrears; plan changes are not prorated (#3119).\n\n## Cross-Package References\n\n- `customerId` → `@foreignKey('Customer')` (hard reference within package)\n- `profileId` → plain string to smrt-profiles\n- `arJournalId`, `revenueJournalId` → plain string to smrt-ledgers\n- `skuId` (on `PaymentIntent`, `LicenseSale`) → plain string to smrt-products\n- `paymentId` (on `PaymentIntent`, `Payout`, `LicenseSale`) → plain string to `Payment` (same package, but kept as plain string for cross-model consistency)\n- `vendorId` on `Payout` → `@foreignKey(Vendor)` (hard reference within package)\n- Billing records reference tenants with `@crossPackageRef('@happyvertical/smrt-users:Tenant')` and charges, plan periods, and adjustments by plain `sourceId`\n\n## Gotchas\n\n- **Optional tenancy**: all models `@TenantScoped({ mode: 'optional' })` + nullable tenantId\n- **Currency is integer minor units** (cents, satoshis) — `$19.99` is `1999`, like affiliates. Money is exact, so money fields initialize `= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER columns (BIGINT on fresh PostgreSQL/DuckDB databases). Writing a fractional major-unit value is the bug — the model rejects it via `assertIntegerMinorUnits` on `Invoice`/`Payment`/`PaymentAllocation`/`Payout`/`Contract` before the database sees it, because PostgreSQL would reject it with `22P02` while SQLite's affinity silently stores it. `pnpm --filter @happyvertical/smrt-commerce test:postgres` is the lane that holds the line (#2361, #2401, #2373).\n- **Rates are the opposite**: `InvoiceLineItem.taxRate` and `ContractLineItem.taxRate` are inherently fractional and must initialize `= 0.0`. INTEGER would truncate every rate to 0. `ContractLineItem.quantity` is decimal for the same reason (contracts price fractional hours/weight/bandwidth); `InvoiceLineItem.quantity` is also decimal (six fractional places); existing integer columns require the explicit [quantity migration](agents/invoice-quantity.md).\n- **Rounding happens where a rate meets money, and nowhere else**: `InvoiceLineItem` uses exact shared invoice arithmetic to round fractional quantity × price and then tax to minor units; `ContractLineItem.calculateAmount()` uses `Math.round()` for tax. Everything downstream is exact integer arithmetic, which is why **there are no `EPSILON` tolerances left on any money path** (#2401). `Invoice`, `PaymentAllocation`, `Payout` and `PaymentIntent` compare with `===` / `>` / `>=`. Do not reintroduce a tolerance: against integers it forgives a real one-cent discrepancy rather than float fuzz. (`FulfillmentLineItem`'s quantity epsilon is unrelated — quantities are genuinely decimal there.)\n- **Migrating an existing database to minor units**: `preflightCommerceMoneyMinorUnits(db)` reports, per column, whether it still holds major units and which rows would be rounded or exceed JavaScript's safe-integer range; `migrateCommerceMoneyToMinorUnits(db)` converts them. Both are exported from the package root. The rescale is idempotent via a `_smrt_backfills` marker, changes the column type in place on PostgreSQL/DuckDB (`ALTER … TYPE BIGINT USING round(col * 100)`), and on SQLite rescales values only — SQLite cannot alter a declared type in place, so those columns come back in `declaredTypeChangePending` and need the table-rebuild path (#2370).\n- **`Payment.nativeAmount` is NOT in that migration, and must not be.** Its minor unit is a property of the *row's asset*: satoshis on a BTC rail (×1e8), cents on a fiat or stablecoin rail (×1e2), in one column discriminated only by `nativeCurrency`. A blanket ×100 turns 0.00713 BTC into `1` instead of `713000` sats, and a round 0.01 BTC even passes the preflight's integrality check while being wrong by six orders of magnitude. Convert it in two deliberate steps — normalise each row to its own asset's minor units while the column is still floating-point, then `rescaleMoneyColumnsToMinorUnits(db, COMMERCE_NATIVE_UNIT_COLUMNS, { scale: 1, … })` for the type change. `COMMERCE_NATIVE_UNIT_COLUMNS` is exported with a worked example; step one is deliberately the deployment's to write, because only it knows which `nativeCurrency` values it has used.\n- **`PaymentIntent.paymentOptions[].nativeAmount` has the same per-asset problem inside a JSON column and is likewise not migrated.** An intent's price lock defaults to 15 minutes, so the supported answer is to let open intents expire and be re-quoted: an unconverted option fails the exact `!==` reconciliation against the payment that arrives, which is a loud failure rather than a silently mis-scaled quote.\n- **Range and existing deployments**: fresh PostgreSQL/DuckDB INTEGER columns are BIGINT, and the runtime rejects values outside JavaScript's safe-integer range rather than rounding them. Existing PostgreSQL `int4` columns do not self-repair; widen them through the explicit migration tracked in #2424.\n- **Journals posted from commerce carry minor units**: `Payment.recordPayment()` and `Invoice.recognizeRevenue()` write `amount` / `totalAmount` straight into smrt-ledgers entries. smrt-ledgers is unit-agnostic (its `BALANCE_EPSILON` only checks debits === credits), and integer entries balance exactly, so this is strictly tighter than the major-unit journals commerce used to post.\n- **Invoice controls payment status**: not the Payment model — use `Invoice.updatePaymentStatus()`\n- **Tax rate is external**: no tax rate field on Invoice — rate must be calculated externally\n- **Profile linking**: separate `ProfileCollection.create()` needed to fetch actual Profile object\n- **PaymentIntent natural key**: `conflictColumns: ['tenant_id', 'offering_ref', 'licensee_email', 'idempotency_key']` — a retried `create` with the same tuple upserts the existing row. Use `getOrCreateByIdempotencyKey()` to branch on `{ intent, created }`. Empty natural-key inputs (e.g. blank `idempotencyKey`) disable dedup by design.\n- **Payout state-machine guards**: `markSent` requires a non-empty `backendTxRef`; `markConfirmed` only valid from `SENT`; `markFailed` only valid from `PENDING` / `SENT` (a confirmed payout can't fail — that path is a refund). `resetFromFailed()` is the dedicated escape hatch from `FAILED` back to `PENDING` after an operator fixes the underlying problem; it clears `backendTxRef` so the next attempt picks up a fresh one.\n- **PaymentInstrument defaults**: `isDefault` is domain-managed, not API-writable. Call `setDefaultForCustomer(customerId, instrumentId)` so the target is validated and the customer's other instruments are cleared.\n- **LicenseSale immutability**: rights snapshot freezes on save-with-status-ACCEPTED. The captured snapshot lives in a module-scoped `WeakMap<LicenseSale, string>` so it doesn't interact with the schema or round-trip through `_meta_data`. Drafts (status != ACCEPTED) remain mutable. To \"change\" an issued license: `revoke()` it, then issue a new `LicenseSale` row.\n- **Vendor.payoutAddresses normalization**: the constructor accepts either a `Record<string, string>` or a pre-serialized JSON string. `initialize()` re-normalizes after the framework's option-override pass; `save()` re-normalizes defensively against direct field assignment. Non-string values inside the input map are silently dropped to preserve the typed invariant downstream.\n\n## Commercial UI\n\nSee [commercial UI composition](agents/commercial-ui.md) for native form recovery, amount compatibility and backend ownership. Package-owned guides under `agents/` ship with the package.\n\n## Quote and estimate UI\n\n`src/svelte/quote-types.ts` separates unparsed draft strings from explicit\nminor-unit revision DTOs. Native row submitters, caller-owned retry identity and\nextension conventions are documented in [agents/quote-ui.md](agents/quote-ui.md).\n\n## Purchasing UI\n\n`PurchaseOrderEditor` preserves allocation IDs and explicit zeroes, including\nretained reduction rows; the server validates review fingerprints before recording.\nSee [agents/purchasing-ui.md](agents/purchasing-ui.md) for caller responsibilities.\n",
19770
+ "agentDoc": "# @happyvertical/smrt-commerce\n\nE-commerce with Contract STI hierarchy, invoice lifecycle, payment tracking, payout remittance, and optional ledger integration.\n\n## Models\n\n- **Customer** / **Vendor**: linked to Profile via string ID (not FK). Customer has creditLimit, paymentTerms, customerType (DTC / WHOLESALE / RETAIL). Vendor has leadTimeDays, minimumOrder, and `payoutAddresses: Record<string, string>` — a flat map from payout-rail-qualified currency code (`USDC-base`, `BTC`, `USD-stripe`, ...) to destination string (EVM address, BTC address, Stripe Connect account id, IBAN). Use `getPayoutAddress(currency)` for a `Map.get`-style lookup; missing entries return `undefined` (caller decides skip-vs-error).\n- **Contract** (STI base → Estimate, Order, Lease, Agreement, PurchaseOrder, WholesaleOrder, ProductionOrder, Cart, LicenseSale): 9 contract types sharing one table. Carries `channelId` (open-ended string — `dtc-web`, `wholesale-b2b`, `pos-store-N`, etc.) so the same model serves DTC checkout, B2B portals, and POS.\n- **WholesaleOrder**: B2B order. Conventional pairing — customer has `customerType: 'wholesale'`, NET-30/60 terms, delivered via wholesale-portal channel.\n- **ProductionOrder**: manufacturing equivalent of a PurchaseOrder — commission your factory to make finished goods. Consumes raw materials per BOM (`@happyvertical/smrt-manufacturing`) and produces SKU stock (`@happyvertical/smrt-inventory`).\n- **Cart**: transient order-in-progress. Same shape as Order; the application promotes the row from `_meta_type: Cart` to `_meta_type: Order` at checkout instead of copying data between tables.\n- **LicenseSale**: industry-neutral licensing primitive. Carries an *immutable* rights snapshot (`rightsMedium`, `rightsDistributionScope`, `rightsExclusivity`, `rightsDuration`, `rightsTerritory`, `rightsSublicensing`, `rightsDerivatives` — typed `Meta<T>` fields), licensee identity (`licenseeEmail`, optional `licenseeLegalEntity` / `licenseeJurisdiction`), and a signed-PDF reference (`pdfUrl`, `pdfHash`, optional `onChainHashRegistryRef`). Once saved at `ContractStatus.ACCEPTED`, the rights snapshot is frozen — mutating any of the seven rights fields and re-saving throws. The only legal transition out of ACCEPTED is `revoke()` (moves to CANCELLED without touching rights). Useful for stock media, music licensing, code-asset marketplaces, license keys, anywhere rights are sold for a fee.\n- **ContractLineItem**: items on contracts.\n- **Invoice**: status machine `DRAFT → SENT → VIEWED → PARTIAL → PAID` (also OVERDUE, CANCELLED, WRITTEN_OFF). `recognizeRevenue()` creates balanced AR journal entry (DR: Accounts Receivable, CR: Revenue, CR: Tax Payable).\n- **InvoiceLineItem**: fractional-quantity line items with safe minor-unit validation/recalculation on save; editor-mode recovery and existing-column migration are documented in [invoice quantity persistence](agents/invoice-quantity.md).\n- **Payment** / **PaymentAllocation**: tracks payments against invoices. Status controlled by `Invoice.updatePaymentStatus()`, not Payment model. Carries optional backend-adapter fields for PaymentBackend-routed flows: `backendId` (rail adapter id — `base-usdc`, `btc`, `stripe`, distinct from `externalProvider` which names an accounting sync destination), `backendTxRef` (chain tx hash or gateway settlement id), `nativeAmount` / `nativeCurrency` (what actually arrived), `usdAtQuote` / `usdAtConfirmation` (drift accounting for volatile-currency rails). `Payment.usdDrift()` returns the confirmation - quote delta, or `0` when either side is unset.\n- **PaymentInstrument**: reusable saved payment method (\"card on file\"). Stores only provider references and non-sensitive display metadata (`providerCustomerId`, `providerPaymentMethodId`, brand / last4 / expiry), never raw card data. Use `PaymentInstrumentCollection.setDefaultForCustomer()` to change the default so the single-default-per-customer invariant is enforced; generated create/update routes cannot write `isDefault` directly.\n- **PaymentIntent**: short-lived pre-payment commitment with multi-option semantics. Locks a USD price for a fixed window (default 15 minutes) and lists one or more `PaymentOption`s describing different rails (`backendId`, `currency`, `payTo`, `nativeAmount`, optional `chain` / `memo` / `x402Capable` / `expiresAt`). First option to receive payment wins; the others are implicitly retired. State machine `awaiting_payment → paid → (issued | retired)` plus `expired` and `cancelled`. Mutate via dedicated `markPaid` / `markIssued` / `expire` / `cancel` / `retire` helpers — direct status assignment bypasses the invariant checks. Idempotency via natural key `(tenant_id, offering_ref, licensee_email, idempotency_key)` and `PaymentIntentCollection.getOrCreateByIdempotencyKey()`.\n- **Payout**: operator-to-supplier remittance. Distinct from Payment because direction, status machine, and chain semantics differ. Status machine `pending → sent → confirmed → failed` (failed is terminal but resettable via `resetFromFailed()` after fixing the underlying problem). References source `paymentId` (plain string) and destination `vendorId` (foreign key). Amount invariant `supplierNet === grossAmount - operatorFee` enforced exactly on save (integer minor units, no tolerance — #2401). `PayoutCollection.createFromPayment()` is the typical entry point; it pulls native amount / currency from the source Payment.\n- **Fulfillment** / **FulfillmentLineItem**: shipment/delivery tracking.\n\n## Recipes (#3590)\n\n`src/recipes.ts` declares four minimal `SmrtRecipe`s, emitted into the built\n`manifest.json` and `smrt-knowledge.json` `recipes`: `commerce.customers`\n(Customer), `commerce.vendors` (Vendor), `commerce.sales` (Order, requires\ncustomers), `commerce.purchases` (PurchaseOrder, requires vendors). Headers\nonly; line items, fulfilment, invoices, and payments are later recipes. Each\nhas user-facing help in `src/<name>.recipe.md` (#3591), and the fields those\nrecipes show carry `@field({ description })` text; keep both in plain\nlanguage for end users and update them when a recipe's fields change. See\n[core recipes](../core/agents/recipes.md).\n\n## Ledger Integration\n\n`@happyvertical/smrt-ledgers` is a regular dependency, loaded lazily via dynamic `import()` so the coupling stays runtime-only (no hard static import; the package graph stays a DAG — see #1582). Invoice stores `arJournalId` and `revenueJournalId` as string references. `recognizeRevenue()` creates a balanced AR journal entry; `getArJournal()` returns null when no journal has been recognized yet.\n\n## Billing-period close (#3060)\n\n`src/billing/` closes periods from smrt-subscriptions charges; see the README\nfor usage.\n\n- **Port, not provider.** Period close, events, and credit checkout call only\n the `BillingProvider` port (integer minor units). `stripe.ts` adapts\n `@happyvertical/accounting`, which takes currency *major* units; convert with\n `units.ts` and never hand-roll a Stripe call here. `units.ts` exponents are\n an explicit ISO 4217 table matching the SDK's, never `Intl` display digits\n (CLDR differs for IQD, MGA, … and drifts by ICU release). The SDK's\n `*Minor` fields are already ISO minor units. Capabilities a rail may lack\n (`chargeSavedPaymentMethod`, `createSetupCheckout`, `getCheckout`,\n `setDefaultPaymentMethod`, `markInvoiceUncollectible`) are optional port\n methods: detect them by presence, and keep additions optional (#3139).\n `sendAndReconcile` refuses a provider invoice whose subtotal or total\n disagrees with the local invoice.\n- **Replay safety is identity.** Close, invoice, line, payment, allocation,\n and claim ids are `deterministicId()`s; `BillingLineSource` (conflict key\n `source_type, source_id`) is the only double-billing guard, so never bill a\n source without claiming it, and never widen that key. Each close step is\n persisted before the next; `acquireCloseLease()` serializes workers. A\n close that nets to a credit keeps its claims and ends `carried_forward`;\n its subtotal is a `credit_carry_forward` source for the payer's next billed\n close, which marks the carried close `completed`.\n- **Cycles and flat-plan coverage (#3116).** `cycles.ts` is the schedule\n (pure; `BillingAccount.billingAnchorAt`, null = calendar months). A close\n with no period closes each payer's `lastEndedBillingPeriod()`. Flat-plan\n claims are the exception to \"id from the source\": their `sourceId` is\n `<subscriptionId>:seq:<chainSequence>` and their `periodStart/End` is the\n exact time they bill. `claimFlatWindow()` reads the last number, then the\n coverage (claims ending after the window start), claims one gap as number\n n + 1, and retries on conflict; reading in that order is what makes it\n safe. That numbering, not the period, stops overlaps when schedules or\n payers change — never claim a flat plan outside it, and keep both reads\n bounded (no whole-history scans). Each numbered claim is its own invoice\n line keyed `lineKey|periodStart` (a function of the claim alone, so a\n resumed close rebuilds the same line ids). Proration is\n `prorateMinorUnits()` (half up, BigInt) against the period's full cycle.\n- **System tables.** `BillingAccount`, `BillingPeriodClose`, and\n `BillingLineSource` are not tenant-scoped and have no generated surface.\n Collections stay unexported; models are root exports (#3082). Invoices,\n payments, and journals are written in the seller's tenant; payer-owned\n subscriptions and credit grants in a system context, only after\n verification.\n- **Events** use smrt-jobs' delivery inbox under provider\n `<name>-billing:<sellerTenantId>` and a provider-filtered\n `ForgeProjectionRuntime`, so a runtime never claims another seller's\n events; any other runtime sharing the inbox must pass `providers` too. Only\n events this package acts on are stored: foreign checkouts are `ignored`,\n and only `smrt_*` checkout metadata is kept; it is\n HMAC-signed with the webhook secret at creation (empty values are omitted:\n Stripe drops them), so another integration on the same account cannot forge\n a credit purchase or a card setup; credit is granted only when the\n collected total equals it plus provider tax. Checkout amounts come from the\n re-read session (`getCheckout()`), never the Stripe event, whose amounts are\n in Stripe's unit. Rotating the webhook secret strands in-flight checkouts\n (up to 24 h). `observe()` re-reads provider state, so ordering never depends\n on delivery order; the only provider writes in `observe()` are idempotent\n (making a saved card the default). The standing hook runs inside the event\n transaction and must be idempotent.\n- **Card on file and top-ups (#3139).** A saved card is the payer customer's\n default `PaymentInstrument` (seller tenant, id from the provider method).\n `autoChargeInvoices` bills such payers `charge_automatically`, decided once\n and saved as `Invoice.collectionMethod` before the first push, which every\n retry reuses (never recompute it: a lost push response would record a\n method the provider was not asked for, #3190); activation follows `paid`, never send. An auto top-up attempt\n is a `PENDING` `Payment` (`reference` `auto-top-up:<policyId>`, id from the\n policy and `count()` of its attempts) inserted before the charge, so racing\n evaluations derive one id and one charge; the charge key is derived from\n that id. `applyAutoTopUpOutcome()` must run in a transaction and starts with\n a conditional `UPDATE … WHERE status = 'pending'` row lock, so the hook and\n the `payment` webhook settle once; the grant is keyed by the charge key.\n Never settle an attempt outside that function. Taxed accounts are charged\n with provider tax by default (#3194): the attempt row holds the credit\n (the charge subtotal), the tax is journaled to tax payable at settlement,\n and the attempt's tax decision is recorded in its `notes` so a re-drive\n repeats the same charge request (the provider refuses a key reused with\n different tax). `claimAttempt()` is the only place that decides a\n skip and reports it (`onAutoTopUpSkipped`); `cardOnFile()` and period close\n use `defaultCardInstrument()` — never copy that predicate. A provider write-off moves the local invoice to\n `WRITTEN_OFF` (a later payment, by card or on a rail (#3188), is recorded\n and reinstates the payer; the status stays, so \"paid in full\" for a\n written-off invoice is read from its allocations).\n- **`Invoice.providerTaxAmount`** is added to line-item tax; it is\n server-managed (not API-writable). `toAccountingInput()` emits major units.\n- **Payment rails (#3138).** A runtime has one issuing `provider` and any\n number of `paymentProviders` (unique names, one inbox namespace each; a\n provider with `capabilities.issuesInvoices === false` can never issue).\n `observe()` resolves the provider from the delivery namespace, never the\n payload. Crypto rails implement the port over `@happyvertical/payments`'\n `CryptoCheckoutGateway`; never call BTCPay (or any gateway) from here.\n Settlement is decided by the pure `decideAttempt()` from gateway state and\n `paymentPolicy` — the gateway's `settled` is authoritative and nothing here\n counts confirmations. A `BillingPaymentAttempt` (`(provider, checkout_id)`\n deterministic id) records every state and settles once; grants and payments\n are keyed by checkout id. Invoice payments close the issuer's invoice out of\n band in `observe()` (idempotent) before `project()` records the payment, and\n `settlePaidInvoice` skips (records nothing for) a `paidOutOfBand` invoice\n the rail latched with `outOfBandRequestedAt`; other out-of-band closes are\n recorded as `OTHER` payments. Rails never save cards or charge tax: a taxed\n credit purchase on a rail needs an explicit `automaticTax: false`. Exceptions are flags for operators;\n nothing reverses or refunds automatically.\n- **Known limits**: write-offs post no bad-debt journal (AR stays until paid\n or journaled by an operator); discounts still reach Stripe as negative lines (the SDK's\n line `discount` would change the lines a replayed push reconciles, so it\n needs its own migration); `RetailCharge` has no adjustment ledger. Flat\n plans are monthly, in arrears; plan changes are not prorated (#3119).\n\n## Cross-Package References\n\n- `customerId` → `@foreignKey('Customer')` (hard reference within package)\n- `profileId` → plain string to smrt-profiles\n- `arJournalId`, `revenueJournalId` → plain string to smrt-ledgers\n- `skuId` (on `PaymentIntent`, `LicenseSale`) → plain string to smrt-products\n- `paymentId` (on `PaymentIntent`, `Payout`, `LicenseSale`) → plain string to `Payment` (same package, but kept as plain string for cross-model consistency)\n- `vendorId` on `Payout` → `@foreignKey(Vendor)` (hard reference within package)\n- Billing records reference tenants with `@crossPackageRef('@happyvertical/smrt-users:Tenant')` and charges, plan periods, and adjustments by plain `sourceId`\n\n## Gotchas\n\n- **Optional tenancy**: all models `@TenantScoped({ mode: 'optional' })` + nullable tenantId\n- **Currency is integer minor units** (cents, satoshis) — `$19.99` is `1999`, like affiliates. Money is exact, so money fields initialize `= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER columns (BIGINT on fresh PostgreSQL/DuckDB databases). Writing a fractional major-unit value is the bug — the model rejects it via `assertIntegerMinorUnits` on `Invoice`/`Payment`/`PaymentAllocation`/`Payout`/`Contract` before the database sees it, because PostgreSQL would reject it with `22P02` while SQLite's affinity silently stores it. `pnpm --filter @happyvertical/smrt-commerce test:postgres` is the lane that holds the line (#2361, #2401, #2373).\n- **Rates are the opposite**: `InvoiceLineItem.taxRate` and `ContractLineItem.taxRate` are inherently fractional and must initialize `= 0.0`. INTEGER would truncate every rate to 0. `ContractLineItem.quantity` is decimal for the same reason (contracts price fractional hours/weight/bandwidth); `InvoiceLineItem.quantity` is also decimal (six fractional places); existing integer columns require the explicit [quantity migration](agents/invoice-quantity.md).\n- **Rounding happens where a rate meets money, and nowhere else**: `InvoiceLineItem` uses exact shared invoice arithmetic to round fractional quantity × price and then tax to minor units; `ContractLineItem.calculateAmount()` uses `Math.round()` for tax. Everything downstream is exact integer arithmetic, which is why **there are no `EPSILON` tolerances left on any money path** (#2401). `Invoice`, `PaymentAllocation`, `Payout` and `PaymentIntent` compare with `===` / `>` / `>=`. Do not reintroduce a tolerance: against integers it forgives a real one-cent discrepancy rather than float fuzz. (`FulfillmentLineItem`'s quantity epsilon is unrelated — quantities are genuinely decimal there.)\n- **Migrating an existing database to minor units**: `preflightCommerceMoneyMinorUnits(db)` reports, per column, whether it still holds major units and which rows would be rounded or exceed JavaScript's safe-integer range; `migrateCommerceMoneyToMinorUnits(db)` converts them. Both are exported from the package root. The rescale is idempotent via a `_smrt_backfills` marker, changes the column type in place on PostgreSQL/DuckDB (`ALTER … TYPE BIGINT USING round(col * 100)`), and on SQLite rescales values only — SQLite cannot alter a declared type in place, so those columns come back in `declaredTypeChangePending` and need the table-rebuild path (#2370).\n- **`Payment.nativeAmount` is NOT in that migration, and must not be.** Its minor unit is a property of the *row's asset*: satoshis on a BTC rail (×1e8), cents on a fiat or stablecoin rail (×1e2), in one column discriminated only by `nativeCurrency`. A blanket ×100 turns 0.00713 BTC into `1` instead of `713000` sats, and a round 0.01 BTC even passes the preflight's integrality check while being wrong by six orders of magnitude. Convert it in two deliberate steps — normalise each row to its own asset's minor units while the column is still floating-point, then `rescaleMoneyColumnsToMinorUnits(db, COMMERCE_NATIVE_UNIT_COLUMNS, { scale: 1, … })` for the type change. `COMMERCE_NATIVE_UNIT_COLUMNS` is exported with a worked example; step one is deliberately the deployment's to write, because only it knows which `nativeCurrency` values it has used.\n- **`PaymentIntent.paymentOptions[].nativeAmount` has the same per-asset problem inside a JSON column and is likewise not migrated.** An intent's price lock defaults to 15 minutes, so the supported answer is to let open intents expire and be re-quoted: an unconverted option fails the exact `!==` reconciliation against the payment that arrives, which is a loud failure rather than a silently mis-scaled quote.\n- **Range and existing deployments**: fresh PostgreSQL/DuckDB INTEGER columns are BIGINT, and the runtime rejects values outside JavaScript's safe-integer range rather than rounding them. Existing PostgreSQL `int4` columns do not self-repair; widen them through the explicit migration tracked in #2424.\n- **Journals posted from commerce carry minor units**: `Payment.recordPayment()` and `Invoice.recognizeRevenue()` write `amount` / `totalAmount` straight into smrt-ledgers entries. smrt-ledgers is unit-agnostic (its `BALANCE_EPSILON` only checks debits === credits), and integer entries balance exactly, so this is strictly tighter than the major-unit journals commerce used to post.\n- **Invoice controls payment status**: not the Payment model — use `Invoice.updatePaymentStatus()`\n- **Tax rate is external**: no tax rate field on Invoice — rate must be calculated externally\n- **Profile linking**: separate `ProfileCollection.create()` needed to fetch actual Profile object\n- **PaymentIntent natural key**: `conflictColumns: ['tenant_id', 'offering_ref', 'licensee_email', 'idempotency_key']` — a retried `create` with the same tuple upserts the existing row. Use `getOrCreateByIdempotencyKey()` to branch on `{ intent, created }`. Empty natural-key inputs (e.g. blank `idempotencyKey`) disable dedup by design.\n- **Payout state-machine guards**: `markSent` requires a non-empty `backendTxRef`; `markConfirmed` only valid from `SENT`; `markFailed` only valid from `PENDING` / `SENT` (a confirmed payout can't fail — that path is a refund). `resetFromFailed()` is the dedicated escape hatch from `FAILED` back to `PENDING` after an operator fixes the underlying problem; it clears `backendTxRef` so the next attempt picks up a fresh one.\n- **PaymentInstrument defaults**: `isDefault` is domain-managed, not API-writable. Call `setDefaultForCustomer(customerId, instrumentId)` so the target is validated and the customer's other instruments are cleared.\n- **LicenseSale immutability**: rights snapshot freezes on save-with-status-ACCEPTED. The captured snapshot lives in a module-scoped `WeakMap<LicenseSale, string>` so it doesn't interact with the schema or round-trip through `_meta_data`. Drafts (status != ACCEPTED) remain mutable. To \"change\" an issued license: `revoke()` it, then issue a new `LicenseSale` row.\n- **Vendor.payoutAddresses normalization**: the constructor accepts either a `Record<string, string>` or a pre-serialized JSON string. `initialize()` re-normalizes after the framework's option-override pass; `save()` re-normalizes defensively against direct field assignment. Non-string values inside the input map are silently dropped to preserve the typed invariant downstream.\n\n## Commercial UI\n\n`CustomerSelect`/`VendorSelect` are the registered selectors for Customer and\nVendor (`selects` in `src/ui.ts`); models carry `ui.widget` hints. See\n[customer/vendor UI](agents/customer-vendor-ui.md#selectors-3602).\n\nSee [commercial UI composition](agents/commercial-ui.md) for native form recovery, amount compatibility and backend ownership. Package-owned guides under `agents/` ship with the package.\n\n## Quote and estimate UI\n\n`src/svelte/quote-types.ts` separates unparsed draft strings from explicit\nminor-unit revision DTOs. Native row submitters, caller-owned retry identity and\nextension conventions are documented in [agents/quote-ui.md](agents/quote-ui.md).\n\n## Purchasing UI\n\n`PurchaseOrderEditor` preserves allocation IDs and explicit zeroes, including\nretained reduction rows; the server validates review fingerprints before recording.\nSee [agents/purchasing-ui.md](agents/purchasing-ui.md) for caller responsibilities.\n",
19703
19771
  "moduleDocs": [
19704
19772
  {
19705
19773
  "path": "agents/invoice-quantity.md",
19706
19774
  "module": "invoice-quantity",
19707
19775
  "content": "# Invoice quantity persistence and editor recovery\n\nInvoiceLineItem accepts nonnegative decimal quantities with at most six decimal\nplaces. The `1.0` default declares a floating-point quantity column. Unit price,\nflat discount, and calculated amount remain safe integer minor units. Save always\nvalidates resolved fields and recalculates amount; a supplied amount is never\ntrusted. Gross quantity × price and then tax on discounted gross round to minor\nunits, with ties toward positive infinity (including existing negative-price\ncredit lines). Tax is a resolved fraction in [0, 1], with eight decimal places.\nAccounting export retains an explicit zero rate instead of omitting it.\n\n`applyEditorDraft(draft, authorizedContext)` resolves the shared invoice draft\nhelper into description/SKU/quantity/price/flat discount/tax and stores cloned\n`invoiceEditorStateJson` TEXT, accessed through a guarded typed `invoiceEditorState` getter/setter. It does not save.\n`getEditorDraft(authorizedContext)` returns a clone only when currency, inherited\ntax authority, and all source fields still agree. Otherwise it returns null so the\ncaller can reconstruct a flat-discount/explicit-tax editor from resolved fields.\nNormal save clears stale editor state and never derives money from metadata.\nAll other object metadata, authorization, and transaction ownership are unchanged.\n\n## Existing integer columns\n\nRun a backup and pause invoice writers for this explicit migration. With a root\ndatabase handle and its known engine (`sqlite`, `postgres`, or `duckdb`), call\n`preflightInvoiceEditorStorage(db, engine)`, resolve every reported invalid row,\nthen `migrateInvoiceEditorStorage(db, engine)`. The preflight rejects negative,\nnonfinite, unsafe, excess-precision, and numerically non-roundtripping quantities.\nThe migration uses a transaction (and a PostgreSQL writer lock) and never rescales\nor rounds stored values. PostgreSQL widens INTEGER/BIGINT to DOUBLE PRECISION;\nDuckDB widens to DOUBLE. Repeating it leaves an already widened declaration alone.\nAn absent table is reported as absent and is never marked migrated.\n\nSQLite uses the public core table-rebuild planner to change the declaration to\nREAL while preserving existing columns, constraints, indexes, and triggers. If\nincoming foreign keys make that rebuild unsafe, the migration throws with the\nplanner reason and rolls back; it does not disable constraints or lose children.\nResolve that maintenance condition before deploying fractional writers. The same\ntransaction adds the nullable TEXT `invoice_editor_state_json` column with an empty\nstring default on every engine. This column is required for updated model saves.\nAn existing floating quantity column still receives the missing state column.\nNo schema mutation runs implicitly during runtime initialization or model save.\n\nThe actual DuckDB quantity/storage migration is tested. Complete Commerce model\npersistence on DuckDB is currently unsupported because its hard foreign keys use\nON UPDATE CASCADE, which DuckDB cannot preserve; the schema builder refuses that\ncontract. SQLite and PostgreSQL exercise complete save/reload model behavior.\n"
19708
19776
  },
19777
+ {
19778
+ "path": "agents/customer-vendor-ui.md",
19779
+ "module": "customer-vendor-ui",
19780
+ "content": "# Customer and vendor UI\n\nImport the provider-free surfaces from `@happyvertical/smrt-commerce/svelte`:\n\n```svelte\n<script lang=\"ts\">\n import {\n CustomerDirectory,\n CustomerDetail,\n CustomerForm,\n VendorDirectory,\n VendorDetail,\n VendorForm,\n type CustomerDisplayData,\n } from '@happyvertical/smrt-commerce/svelte';\n\n let { data, form } = $props();\n</script>\n\n<CustomerForm\n mode=\"edit\"\n values={form?.values ?? data.values}\n errors={form?.errors}\n transport={{\n action: '?/update',\n method: 'post',\n hiddenFields: {\n requestId: form?.values.requestId ?? data.requestId,\n expectedTenantId: data.tenantId,\n },\n }}\n labels={{ singular: 'Client', plural: 'Clients' }}\n cancelHref={`/clients/${data.customer.id}`}\n/>\n```\n\nThe caller owns action URLs, server authorization, tenancy checks, request tokens, persistence, and retry behavior. Capability props only decide whether an affordance is rendered. The components never create a Profile, Customer, Vendor, or contact relationship and never generate request tokens.\n\n`CustomerDisplayData.creditLimitMinor` and `VendorDisplayData.minimumOrderMinor` are integer currency minor units. Detail components convert them only for display and accept an explicit `minorUnitExponent`. Form values deliberately use retained strings such as `1250.00`; the server validates and converts those major-unit strings to the model's integer minor units.\n\n`VendorFormValues.leadTimeDays` is also retained text. The control supplies a numeric keyboard hint without browser number-input sanitization, and the server must parse and validate the submitted non-negative integer.\n\nForms use the provider-free `Form`, `Input`, `Select`, `Textarea`, and `Checkbox` controls from `@happyvertical/smrt-ui/forms`. They default to native `POST` submission through `preventDefault={false}`. `transport.hiddenFields`, `transport.intentName`, and the field-name maps let an application preserve its existing payload contract. Repeatable contact rows submit repeated field names in DOM order. Add and remove controls are native submitters, so they work without JavaScript; the server returns the revised rows and retained values.\n\nDirectory search forms accept `hiddenFields` as a string record for caller-owned GET state such as sort direction and page size. Fields whose names equal the resolved `queryName` or `statusName` are omitted so the visible search controls remain authoritative.\n\nCustomer shipping and billing addresses use the shared `CountrySelect` and\n`ProvinceSelect`. The native names remain `shippingCountry`, `shippingState`,\n`billingCountry`, and `billingState` unless `fieldNames` overrides them. Changing\ncountry changes the region control without clearing its current string; CA and US\nrender localized subdivision choices, while unsupported countries render an\neditable text field. Unknown values remain selectable/postable after a failed\nsubmission. Use `countryOptions` or `provinceOptions` only when the caller must\nrestrict the choices. `VendorForm` has no structured address fields in its public\nDTO and does not invent them.\n\nVendor currency uses the shared `CurrencySelect`. The raw retained string remains\nthe native `currency` payload, including unknown values returned after validation.\nUse `currencyOptions` for caller-authorized restrictions; omitted options use the\nshared currency catalog. Selector labels follow the active smrt-ui i18n locale.\n\nThe forms place a non-focusable default save submitter before contact row actions so Enter in a text field cannot accidentally add or remove a contact. This remains in place when the `actions` snippet customizes the visible action bar; set `transport.saveIntent` to the custom save intent.\n\nUse the `extension` snippets for application fields such as construction roles and trades. Keep those fields and their policy in the application:\n\n```svelte\n<VendorForm values={data.values} transport={{ action: '?/save' }}>\n {#snippet extension(values)}\n <ConstructionVendorFields roles={values.roles} trades={values.trades} />\n {/snippet}\n</VendorForm>\n```\n\nDirectory entries carry caller-owned detail links and pagination links. Detail edit links and form submit buttons appear only when their capability props allow them; the server must still authorize every request.\n\nDirectory empty and filter behavior follows this contract:\n\n| State | Empty guidance | Native form behavior |\n| --- | --- | --- |\n| No query, status, or caller filter is active | Explain that no records have been added; when `canCreate` and `addHref` are present, point to creating the first record | Search and status controls retain their normal names |\n| Query or status is active | Suggest changing search or filters | Visible query/status controls remain authoritative over same-named `hiddenFields` |\n| A caller-owned filter is active | Suggest changing search or filters | Render the `filters` snippet inside the directory form and list its names in `filterNames` so same-named hidden fields are omitted |\n| Caller supplies `emptyDescription` | Render the supplied description | Submission behavior is unchanged |\n\nThe `filters` snippet is for domain-adjacent GET controls that belong beside\nsearch and status, such as a trade classification. It does not move filtering\nor authorization into the component. `hasAdditionalFilters` describes whether\nthose controls currently narrow the result, while `filterNames` prevents a\nhidden/visible duplicate name from changing native form serialization.\n\nThe registry keys are `customer-directory`, `customer-detail`, `customer-form`, `vendor-directory`, `vendor-detail`, `vendor-form`, and `party-contact-fields`. Interactive examples are published by `@happyvertical/smrt-commerce/playground` and ship in the next generated Commerce package release. DomaCraft can replace its local routes incrementally by adapting its existing server DTOs and payload names; no identity or database migration is required.\n\nThe form playground previews handle save, add-contact, and remove-contact intents in memory. They deliberately return a simulated validation error on save so retained values and retry behavior can be inspected without a backend.\n\n## Selectors (#3602)\n\n`CustomerSelect` and `VendorSelect` are field-sized pickers built on the shared\nsmrt-ui `RelationInput` (`@happyvertical/smrt-ui/forms`, ARIA 1.2 combobox),\nthrough one internal `PartySelect` adapter that maps party DTOs to relation\noptions and localizes the field's text. Search, resolve, debounce, stale-result\nand create handling live in `RelationInput`, not here.\n\n| Prop | Meaning |\n| --- | --- |\n| `name`, `value` (bindable), `label`, `required`, `disabled`, `error`, `placeholder`, `description` | Field basics. `label` defaults to the localized \"Customer\"/\"Vendor\"; `error` renders a linked `role=\"alert\"`. |\n| `search(query)` | Caller-owned lookup returning party DTOs; called with `''` when the list opens and, debounced (`debounceMs`, default 250), as the person types. |\n| `resolve(id)` | Caller-owned lookup of the current value's DTO when it has not been listed; without it a value stays blank until searched. |\n| `onCreate(query)` | Optional. Renders \"+ New customer/vendor\" (`createLabel` overrides); a returned DTO is selected. |\n| `onchange(id)`, `interaction` | Change callback (`''` when cleared) and agent-interaction options. |\n\nRows show `profile.name` with localized status and, for customers, type\n(`Active · Wholesale`). Data never enters the component: no stores, no fetches,\nno tenant logic. A failed `search` shows an inline status, not an exception;\na failed `onCreate` leaves the field as it was. Text comes from the\n`commerce.select.*` messages. The selectors are registered as the\n`customer-select` and `vendor-select` slots, whose `selects` qualified names\nare emitted into the manifest's `uiSelectors`; keep `selects` a string literal\nso the scanner can read it.\n\nWidget hints (`@field({ ui: { widget } })`, #3599) are presentation-only and\nthe build rejects them on non-text fields; `currency` is a currency-code\npicker for ISO 4217 strings and is never applied to minor-unit amounts.\nBehaviour tests run under jsdom: `pnpm exec vitest run --config\nvitest.selects.config.ts` (also part of `pnpm test:ui`).\n"
19781
+ },
19709
19782
  {
19710
19783
  "path": "agents/commercial-ui.md",
19711
19784
  "module": "commercial-ui",
@@ -19723,11 +19796,131 @@
19723
19796
  }
19724
19797
  ],
19725
19798
  "recipes": [
19799
+ {
19800
+ "id": "commerce.agreements",
19801
+ "className": "AgreementsRecipe",
19802
+ "label": "General agreements",
19803
+ "summary": "Keep signed agreements with customers, and see every contract in one list.",
19804
+ "synonyms": [
19805
+ "contracts",
19806
+ "terms",
19807
+ "deals"
19808
+ ],
19809
+ "models": [
19810
+ "@happyvertical/smrt-commerce:Agreement",
19811
+ "@happyvertical/smrt-commerce:Contract",
19812
+ "@happyvertical/smrt-commerce:ContractLineItem"
19813
+ ],
19814
+ "nav": [
19815
+ {
19816
+ "label": "Agreements",
19817
+ "model": "@happyvertical/smrt-commerce:Agreement",
19818
+ "icon": "fileText",
19819
+ "description": "Signed deals and terms with your customers, in one place."
19820
+ },
19821
+ {
19822
+ "label": "All contracts",
19823
+ "model": "@happyvertical/smrt-commerce:Contract",
19824
+ "icon": "fileText",
19825
+ "description": "Every contract you hold, whoever it is with."
19826
+ }
19827
+ ],
19828
+ "requires": [
19829
+ "commerce.customers"
19830
+ ],
19831
+ "group": {
19832
+ "id": "agreements",
19833
+ "label": "Agreements",
19834
+ "summary": "Contracts with customers: general agreements, leases and licence sales."
19835
+ },
19836
+ "section": {
19837
+ "id": "agreements",
19838
+ "label": "Agreements",
19839
+ "icon": "fileText",
19840
+ "description": "Contracts, leases and licenses with customers."
19841
+ },
19842
+ "options": {
19843
+ "@happyvertical/smrt-commerce:Agreement": {
19844
+ "fields": {
19845
+ "contractType": {
19846
+ "default": "agreement",
19847
+ "visibility": "hidden",
19848
+ "locked": true
19849
+ },
19850
+ "customerId": {
19851
+ "label": "Customer",
19852
+ "order": 1,
19853
+ "required": true
19854
+ },
19855
+ "vendorId": {
19856
+ "visibility": "hidden"
19857
+ },
19858
+ "subtotal": {
19859
+ "visibility": "hidden"
19860
+ },
19861
+ "taxAmount": {
19862
+ "visibility": "hidden"
19863
+ },
19864
+ "channelId": {
19865
+ "visibility": "hidden"
19866
+ }
19867
+ }
19868
+ },
19869
+ "@happyvertical/smrt-commerce:ContractLineItem": {
19870
+ "fields": {
19871
+ "sku": {
19872
+ "visibility": "hidden"
19873
+ },
19874
+ "startDate": {
19875
+ "visibility": "hidden"
19876
+ },
19877
+ "endDate": {
19878
+ "visibility": "hidden"
19879
+ },
19880
+ "billingPeriod": {
19881
+ "visibility": "hidden"
19882
+ },
19883
+ "sortOrder": {
19884
+ "visibility": "hidden"
19885
+ },
19886
+ "discount": {
19887
+ "visibility": "hidden"
19888
+ },
19889
+ "taxRate": {
19890
+ "help": "Type a percentage: 5 means 5%."
19891
+ }
19892
+ }
19893
+ },
19894
+ "@happyvertical/smrt-commerce:Contract": {
19895
+ "fields": {
19896
+ "channelId": {
19897
+ "visibility": "hidden"
19898
+ }
19899
+ }
19900
+ }
19901
+ },
19902
+ "help": {
19903
+ "markdown": "## Overview\n\nAgreements are contracts you keep with customers. General agreements live here;\nleases and licence sales are separate recipes in the same group.\n**All contracts** lists every kind together, including orders and quotes.\n\n## Tasks\n\n### Record an agreement\n\n1. Create a new agreement.\n2. Pick the **{field:customerId}**.\n3. Set the **{field:issueDate}** and **{field:expiryDate}**.\n4. Put the conditions in **{field:terms}** and save.\n\n### Follow it\n\n1. Update **{field:status}** from draft to accepted to completed.\n2. Check **{field:expiryDate}** to spot agreements about to lapse.\n",
19904
+ "fieldRefs": [
19905
+ "customerId",
19906
+ "expiryDate",
19907
+ "issueDate",
19908
+ "status",
19909
+ "terms"
19910
+ ]
19911
+ },
19912
+ "demo": {
19913
+ "mode": "server",
19914
+ "reasons": [
19915
+ "Needs a server because its package does not build for a browser: @happyvertical/payments btcpay (node:crypto); core migrations (node:os). The jobs runner edge (#3615) is reported on smrt-jobs; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-ledgers, @happyvertical/smrt-subscriptions, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3626, #3627)."
19916
+ ]
19917
+ }
19918
+ },
19726
19919
  {
19727
19920
  "id": "commerce.customers",
19728
19921
  "className": "CustomersRecipe",
19729
19922
  "label": "Customers",
19730
- "summary": "Keep a list of the customers you sell to.",
19923
+ "summary": "Keep a list of the people and companies you sell to.",
19731
19924
  "synonyms": [
19732
19925
  "clients",
19733
19926
  "buyers",
@@ -19739,10 +19932,28 @@
19739
19932
  "nav": [
19740
19933
  {
19741
19934
  "label": "Customers",
19742
- "model": "@happyvertical/smrt-commerce:Customer"
19935
+ "model": "@happyvertical/smrt-commerce:Customer",
19936
+ "icon": "users",
19937
+ "description": "The people and companies you sell to, with how to reach them."
19743
19938
  }
19744
19939
  ],
19745
19940
  "requires": [],
19941
+ "section": {
19942
+ "id": "sales",
19943
+ "label": "Sales",
19944
+ "icon": "shoppingBag",
19945
+ "description": "Customers, orders and quotes: everything you sell."
19946
+ },
19947
+ "options": {
19948
+ "@happyvertical/smrt-commerce:Customer": {
19949
+ "fields": {
19950
+ "profileId": {
19951
+ "label": "Name",
19952
+ "order": -1
19953
+ }
19954
+ }
19955
+ }
19956
+ },
19746
19957
  "help": {
19747
19958
  "markdown": "## Overview\n\nCustomers is your address book for the people and businesses you sell to. Keep\none record for each customer so you can see who they are, how they like to pay\nand what you have agreed with them, then refer back to them whenever you take an\norder.\n\n## Tasks\n\n### Add a new customer\n\n1. Choose the **New** button above the list.\n2. Describe the customer with **{field:customerType}**: a direct shopper, a\n wholesale buyer or a retail customer. Note in **{field:status}** whether you\n are still dealing with them.\n3. Fill in the **{field:defaultShippingAddress}** and\n **{field:defaultBillingAddress}** so you do not have to type them again on\n every order.\n4. Add **{field:paymentTerms}**, for example \"Net 30\", if you give this\n customer time to pay.\n5. Save. The customer is now on your list and ready to use on a sales order.\n\n### Keep track of what a customer can owe\n\n1. Open the customer from the list and choose Edit.\n2. Set **{field:creditLimit}** to the most you are comfortable letting them owe\n you at one time.\n3. If you do not charge them sales tax, turn on **{field:taxExempt}**, and keep\n their tax number on file.\n4. Use **{field:notes}** for anything the next person who deals with this\n customer should know.\n\n### Find a customer\n\nScan the list for the customer you want, or open a record to see everything you\nhave on file. To stop dealing with someone without losing their history, change\ntheir **{field:status}** to inactive instead of deleting them.\n",
19748
19959
  "fieldRefs": [
@@ -19755,103 +19966,772 @@
19755
19966
  "status",
19756
19967
  "taxExempt"
19757
19968
  ]
19969
+ },
19970
+ "demo": {
19971
+ "mode": "server",
19972
+ "reasons": [
19973
+ "Needs a server because its package does not build for a browser: @happyvertical/payments btcpay (node:crypto); core migrations (node:os). The jobs runner edge (#3615) is reported on smrt-jobs; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-ledgers, @happyvertical/smrt-subscriptions, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3626, #3627)."
19974
+ ]
19758
19975
  }
19759
19976
  },
19760
19977
  {
19761
- "id": "commerce.purchases",
19762
- "className": "PurchasesRecipe",
19763
- "label": "Purchases",
19764
- "summary": "Place orders with vendors and track them.",
19978
+ "id": "commerce.estimates",
19979
+ "className": "EstimatesRecipe",
19980
+ "label": "Estimates",
19981
+ "summary": "Send customers a price quote before they commit.",
19765
19982
  "synonyms": [
19766
- "purchase orders",
19767
- "buying",
19768
- "procurement"
19983
+ "quotes",
19984
+ "quotations",
19985
+ "proposals",
19986
+ "bids"
19769
19987
  ],
19770
19988
  "models": [
19771
- "@happyvertical/smrt-commerce:PurchaseOrder"
19989
+ "@happyvertical/smrt-commerce:Estimate",
19990
+ "@happyvertical/smrt-commerce:ContractLineItem"
19772
19991
  ],
19773
19992
  "nav": [
19774
19993
  {
19775
- "label": "Purchase Orders",
19776
- "model": "@happyvertical/smrt-commerce:PurchaseOrder"
19994
+ "label": "Estimates",
19995
+ "model": "@happyvertical/smrt-commerce:Estimate",
19996
+ "icon": "fileText",
19997
+ "description": "Price quotes you have sent to customers."
19777
19998
  }
19778
19999
  ],
19779
20000
  "requires": [
19780
- "commerce.vendors"
20001
+ "commerce.customers"
19781
20002
  ],
20003
+ "section": {
20004
+ "id": "sales",
20005
+ "label": "Sales",
20006
+ "icon": "shoppingBag",
20007
+ "description": "Customers, orders and quotes: everything you sell."
20008
+ },
19782
20009
  "options": {
19783
- "@happyvertical/smrt-commerce:PurchaseOrder": {
20010
+ "@happyvertical/smrt-commerce:Estimate": {
19784
20011
  "fields": {
19785
20012
  "contractType": {
19786
- "default": "purchase_order",
20013
+ "default": "estimate",
19787
20014
  "visibility": "hidden",
19788
20015
  "locked": true
19789
20016
  },
19790
- "customerId": {
20017
+ "vendorId": {
19791
20018
  "visibility": "hidden",
19792
20019
  "locked": true
19793
20020
  },
19794
- "vendorId": {
19795
- "label": "Vendor",
19796
- "order": 1
20021
+ "customerId": {
20022
+ "label": "Customer",
20023
+ "order": 1,
20024
+ "required": true
20025
+ },
20026
+ "channelId": {
20027
+ "visibility": "hidden"
20028
+ }
20029
+ }
20030
+ },
20031
+ "@happyvertical/smrt-commerce:ContractLineItem": {
20032
+ "fields": {
20033
+ "sku": {
20034
+ "visibility": "hidden"
20035
+ },
20036
+ "startDate": {
20037
+ "visibility": "hidden"
20038
+ },
20039
+ "endDate": {
20040
+ "visibility": "hidden"
20041
+ },
20042
+ "billingPeriod": {
20043
+ "visibility": "hidden"
20044
+ },
20045
+ "sortOrder": {
20046
+ "visibility": "hidden"
20047
+ },
20048
+ "discount": {
20049
+ "visibility": "hidden"
20050
+ },
20051
+ "taxRate": {
20052
+ "help": "Type a percentage: 5 means 5%."
19797
20053
  }
19798
20054
  }
19799
20055
  }
19800
20056
  },
19801
20057
  "help": {
19802
- "markdown": "## Overview\n\nPurchases is where you order from your vendors and keep track of what you have\nbought. Every purchase order belongs to one of your vendors and records the\namounts, the dates and where the order stands.\n\n## Tasks\n\n### Place a purchase order\n\n1. Choose the **New** button above the list.\n2. Enter the **{field:vendorId}** you are buying from. Add them under Vendors\n first so they are on your list.\n3. Enter the **{field:subtotal}** and **{field:taxAmount}**, then the\n **{field:totalAmount}** you expect to pay.\n4. Set the **{field:issueDate}**, and the **{field:dueDate}** for when the\n goods or the payment are due.\n5. Add your own **{field:reference}**, such as the vendor's quote number, so\n you can match it to their paperwork.\n6. Save.\n\n### Follow an order until it arrives\n\n1. Open the purchase order from the list and choose Edit.\n2. Update **{field:status}** as the order moves along, for example from draft to\n sent, accepted and completed.\n3. Use **{field:notes}** to record what the vendor told you, such as delays or\n partial shipments.\n\n### Record the agreement\n\n1. Open the purchase order and choose Edit.\n2. Write down the **{field:terms}** you agreed with the vendor, such as payment\n or delivery conditions.\n3. Set **{field:expiryDate}** if the vendor's offer is only good until a certain\n day.\n",
20058
+ "markdown": "## Overview\n\nEstimates are price quotes you send before a customer commits. Each one belongs\nto a customer, lists its line items and shows how long the price is good for.\n\n## Tasks\n\n### Write an estimate\n\n1. Create a new estimate.\n2. Pick the **{field:customerId}** the quote is for.\n3. Add the line items, then check the **{field:totalAmount}**.\n4. Set **{field:expiryDate}** to the last day the price is good for.\n5. Put your conditions in **{field:terms}** and save.\n\n### Follow it up\n\n1. Set **{field:status}** to sent once it goes out.\n2. Move it to accepted or declined when the customer answers.\n",
19803
20059
  "fieldRefs": [
19804
- "dueDate",
20060
+ "customerId",
19805
20061
  "expiryDate",
19806
- "issueDate",
19807
- "notes",
19808
- "reference",
19809
20062
  "status",
19810
- "subtotal",
19811
- "taxAmount",
19812
20063
  "terms",
19813
- "totalAmount",
19814
- "vendorId"
20064
+ "totalAmount"
20065
+ ]
20066
+ },
20067
+ "demo": {
20068
+ "mode": "server",
20069
+ "reasons": [
20070
+ "Needs a server because its package does not build for a browser: @happyvertical/payments btcpay (node:crypto); core migrations (node:os). The jobs runner edge (#3615) is reported on smrt-jobs; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-ledgers, @happyvertical/smrt-subscriptions, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3626, #3627)."
19815
20071
  ]
19816
20072
  }
19817
20073
  },
19818
20074
  {
19819
- "id": "commerce.sales",
19820
- "className": "SalesRecipe",
19821
- "label": "Sales",
19822
- "summary": "Take customer orders and track them.",
20075
+ "id": "commerce.fulfillment",
20076
+ "className": "FulfillmentRecipe",
20077
+ "label": "Shipping",
20078
+ "summary": "Pack, ship and deliver orders, with tracking.",
19823
20079
  "synonyms": [
19824
- "sales orders",
19825
- "orders",
19826
- "customer orders"
20080
+ "fulfillment",
20081
+ "delivery",
20082
+ "shipments",
20083
+ "tracking"
19827
20084
  ],
19828
20085
  "models": [
19829
- "@happyvertical/smrt-commerce:Order"
20086
+ "@happyvertical/smrt-commerce:Fulfillment",
20087
+ "@happyvertical/smrt-commerce:FulfillmentLineItem"
19830
20088
  ],
19831
20089
  "nav": [
19832
20090
  {
19833
- "label": "Sales Orders",
19834
- "model": "@happyvertical/smrt-commerce:Order"
20091
+ "label": "Shipments",
20092
+ "model": "@happyvertical/smrt-commerce:Fulfillment",
20093
+ "icon": "truck",
20094
+ "description": "What has gone out the door and where it is now."
19835
20095
  }
19836
20096
  ],
19837
- "requires": [
19838
- "commerce.customers"
20097
+ "requires": [],
20098
+ "requiresAny": [
20099
+ [
20100
+ "commerce.sales",
20101
+ "commerce.wholesale"
20102
+ ]
19839
20103
  ],
20104
+ "group": {
20105
+ "id": "billing",
20106
+ "label": "Billing & shipping",
20107
+ "summary": "Invoice your customers, record what they pay and ship what they ordered."
20108
+ },
20109
+ "section": {
20110
+ "id": "operations",
20111
+ "label": "Operations",
20112
+ "icon": "wrench",
20113
+ "description": "Fulfillment and the day-to-day work behind an order."
20114
+ },
19840
20115
  "options": {
19841
- "@happyvertical/smrt-commerce:Order": {
20116
+ "@happyvertical/smrt-commerce:Fulfillment": {
19842
20117
  "fields": {
19843
- "contractType": {
19844
- "default": "order",
19845
- "visibility": "hidden",
19846
- "locked": true
19847
- },
19848
- "vendorId": {
19849
- "visibility": "hidden",
19850
- "locked": true
20118
+ "estimatedDelivery": {
20119
+ "visibility": "hidden"
19851
20120
  },
19852
- "customerId": {
19853
- "label": "Customer",
19854
- "order": 1
20121
+ "shippingAddress": {
20122
+ "visibility": "hidden"
20123
+ }
20124
+ }
20125
+ },
20126
+ "@happyvertical/smrt-commerce:FulfillmentLineItem": {
20127
+ "fields": {
20128
+ "notes": {
20129
+ "visibility": "hidden"
20130
+ }
20131
+ }
20132
+ }
20133
+ },
20134
+ "help": {
20135
+ "markdown": "## Overview\n\nShipping tracks how an order gets to the customer. Each shipment belongs to an\norder and lists which line items are in it, so an order can go out in parts.\n\n## Tasks\n\n### Ship an order\n\n1. Create a new shipment.\n2. Pick the order in **{field:contractId}** and choose the\n **{field:fulfillmentType}**.\n3. Enter the **{field:carrier}** and **{field:trackingNumber}**.\n4. List the items and quantities in the shipment.\n5. Set **{field:status}** to shipped and record **{field:shippedAt}**.\n\n### Confirm delivery\n\n1. Open the shipment and set **{field:status}** to delivered.\n2. Record **{field:deliveredAt}**.\n",
20136
+ "fieldRefs": [
20137
+ "carrier",
20138
+ "contractId",
20139
+ "deliveredAt",
20140
+ "fulfillmentType",
20141
+ "shippedAt",
20142
+ "status",
20143
+ "trackingNumber"
20144
+ ]
20145
+ },
20146
+ "demo": {
20147
+ "mode": "server",
20148
+ "reasons": [
20149
+ "Needs a server because its package does not build for a browser: @happyvertical/payments btcpay (node:crypto); core migrations (node:os). The jobs runner edge (#3615) is reported on smrt-jobs; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-ledgers, @happyvertical/smrt-subscriptions, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3626, #3627)."
20150
+ ]
20151
+ }
20152
+ },
20153
+ {
20154
+ "id": "commerce.invoicing",
20155
+ "className": "InvoicingRecipe",
20156
+ "label": "Invoicing",
20157
+ "summary": "Bill customers and record the payments that come in.",
20158
+ "synonyms": [
20159
+ "invoices",
20160
+ "billing",
20161
+ "payments",
20162
+ "receivables"
20163
+ ],
20164
+ "models": [
20165
+ "@happyvertical/smrt-commerce:Invoice",
20166
+ "@happyvertical/smrt-commerce:InvoiceLineItem",
20167
+ "@happyvertical/smrt-commerce:Payment",
20168
+ "@happyvertical/smrt-commerce:PaymentAllocation"
20169
+ ],
20170
+ "nav": [
20171
+ {
20172
+ "label": "Invoices",
20173
+ "model": "@happyvertical/smrt-commerce:Invoice",
20174
+ "icon": "receipt",
20175
+ "description": "Bills you have sent and which ones are still waiting to be paid."
20176
+ },
20177
+ {
20178
+ "label": "Payments",
20179
+ "model": "@happyvertical/smrt-commerce:Payment",
20180
+ "icon": "creditCard",
20181
+ "description": "Money received, matched to the invoices it paid."
20182
+ }
20183
+ ],
20184
+ "requires": [
20185
+ "commerce.customers"
20186
+ ],
20187
+ "group": {
20188
+ "id": "billing",
20189
+ "label": "Billing & shipping",
20190
+ "summary": "Invoice your customers, record what they pay and ship what they ordered."
20191
+ },
20192
+ "section": {
20193
+ "id": "billing",
20194
+ "label": "Billing",
20195
+ "icon": "receipt",
20196
+ "description": "Invoices and the payments against them."
20197
+ },
20198
+ "options": {
20199
+ "@happyvertical/smrt-commerce:Invoice": {
20200
+ "fields": {
20201
+ "providerTaxAmount": {
20202
+ "visibility": "hidden"
20203
+ },
20204
+ "arJournalId": {
20205
+ "visibility": "hidden"
20206
+ },
20207
+ "revenueJournalId": {
20208
+ "visibility": "hidden"
20209
+ },
20210
+ "externalId": {
20211
+ "visibility": "hidden"
20212
+ },
20213
+ "customerExternalId": {
20214
+ "visibility": "hidden"
20215
+ },
20216
+ "externalProvider": {
20217
+ "visibility": "hidden"
20218
+ },
20219
+ "syncedAt": {
20220
+ "visibility": "hidden"
20221
+ },
20222
+ "collectionMethod": {
20223
+ "visibility": "hidden"
20224
+ },
20225
+ "sentAt": {
20226
+ "visibility": "hidden"
20227
+ },
20228
+ "viewedAt": {
20229
+ "visibility": "hidden"
20230
+ },
20231
+ "remindersSent": {
20232
+ "visibility": "hidden"
20233
+ },
20234
+ "lastReminderAt": {
20235
+ "visibility": "hidden"
20236
+ },
20237
+ "reference": {
20238
+ "visibility": "hidden"
20239
+ },
20240
+ "customerId": {
20241
+ "required": true
20242
+ }
20243
+ }
20244
+ },
20245
+ "@happyvertical/smrt-commerce:InvoiceLineItem": {
20246
+ "fields": {
20247
+ "invoiceEditorStateJson": {
20248
+ "visibility": "hidden"
20249
+ },
20250
+ "sourceType": {
20251
+ "visibility": "hidden"
20252
+ },
20253
+ "sourceId": {
20254
+ "visibility": "hidden"
20255
+ },
20256
+ "periodStart": {
20257
+ "visibility": "hidden"
20258
+ },
20259
+ "periodEnd": {
20260
+ "visibility": "hidden"
20261
+ },
20262
+ "revenueAccountId": {
20263
+ "visibility": "hidden"
20264
+ },
20265
+ "sortOrder": {
20266
+ "visibility": "hidden"
20267
+ },
20268
+ "sku": {
20269
+ "visibility": "hidden"
20270
+ },
20271
+ "discount": {
20272
+ "visibility": "hidden"
20273
+ },
20274
+ "taxRate": {
20275
+ "help": "Type a percentage: 5 means 5%."
20276
+ }
20277
+ }
20278
+ },
20279
+ "@happyvertical/smrt-commerce:Payment": {
20280
+ "fields": {
20281
+ "journalId": {
20282
+ "visibility": "hidden"
20283
+ },
20284
+ "externalId": {
20285
+ "visibility": "hidden"
20286
+ },
20287
+ "externalProvider": {
20288
+ "visibility": "hidden"
20289
+ },
20290
+ "syncedAt": {
20291
+ "visibility": "hidden"
20292
+ },
20293
+ "backendId": {
20294
+ "visibility": "hidden"
20295
+ },
20296
+ "backendTxRef": {
20297
+ "visibility": "hidden"
20298
+ },
20299
+ "nativeAmount": {
20300
+ "visibility": "hidden"
20301
+ },
20302
+ "nativeCurrency": {
20303
+ "visibility": "hidden"
20304
+ },
20305
+ "usdAtQuote": {
20306
+ "visibility": "hidden"
20307
+ },
20308
+ "usdAtConfirmation": {
20309
+ "visibility": "hidden"
20310
+ },
20311
+ "transactionId": {
20312
+ "visibility": "hidden"
20313
+ }
20314
+ }
20315
+ },
20316
+ "@happyvertical/smrt-commerce:PaymentAllocation": {
20317
+ "fields": {
20318
+ "allocatedBy": {
20319
+ "visibility": "hidden"
20320
+ }
20321
+ }
20322
+ }
20323
+ },
20324
+ "help": {
20325
+ "markdown": "## Overview\n\nInvoicing bills your customers and records what they pay. An invoice lists its\nline items; a payment can be spread across one or more invoices.\n\n## Tasks\n\n### Send an invoice\n\n1. Create a new invoice.\n2. Pick the **{field:customerId}** and add the line items.\n3. Check the **{field:totalAmount}** and set the **{field:dueDate}**.\n4. Set **{field:status}** to sent once it goes out.\n\n### Record a payment\n\n1. Create a new payment.\n2. Pick the **{field:customerId}**, then enter the **{field:Payment.amount}** and the\n **{field:method}** used.\n3. Set **{field:Payment.status}** to completed once the money has arrived.\n4. Apply it to the invoices it settles; each one shows what is still owed.\n",
20326
+ "fieldRefs": [
20327
+ "Payment.amount",
20328
+ "Payment.status",
20329
+ "customerId",
20330
+ "dueDate",
20331
+ "method",
20332
+ "status",
20333
+ "totalAmount"
20334
+ ]
20335
+ },
20336
+ "demo": {
20337
+ "mode": "server",
20338
+ "reasons": [
20339
+ "Needs a server because its package does not build for a browser: @happyvertical/payments btcpay (node:crypto); core migrations (node:os). The jobs runner edge (#3615) is reported on smrt-jobs; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-ledgers, @happyvertical/smrt-subscriptions, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3626, #3627)."
20340
+ ]
20341
+ }
20342
+ },
20343
+ {
20344
+ "id": "commerce.leases",
20345
+ "className": "LeasesRecipe",
20346
+ "label": "Leases",
20347
+ "summary": "Rent things out and track the term and the payments due.",
20348
+ "synonyms": [
20349
+ "rentals",
20350
+ "renting",
20351
+ "hire"
20352
+ ],
20353
+ "models": [
20354
+ "@happyvertical/smrt-commerce:Lease",
20355
+ "@happyvertical/smrt-commerce:ContractLineItem"
20356
+ ],
20357
+ "nav": [
20358
+ {
20359
+ "label": "Leases",
20360
+ "model": "@happyvertical/smrt-commerce:Lease",
20361
+ "icon": "home",
20362
+ "description": "Rentals you give or take, with terms and dates."
20363
+ }
20364
+ ],
20365
+ "requires": [
20366
+ "commerce.customers"
20367
+ ],
20368
+ "group": {
20369
+ "id": "agreements",
20370
+ "label": "Agreements",
20371
+ "summary": "Contracts with customers: general agreements, leases and licence sales."
20372
+ },
20373
+ "section": {
20374
+ "id": "agreements",
20375
+ "label": "Agreements",
20376
+ "icon": "fileText",
20377
+ "description": "Contracts, leases and licenses with customers."
20378
+ },
20379
+ "options": {
20380
+ "@happyvertical/smrt-commerce:Lease": {
20381
+ "fields": {
20382
+ "contractType": {
20383
+ "default": "lease",
20384
+ "visibility": "hidden",
20385
+ "locked": true
20386
+ },
20387
+ "customerId": {
20388
+ "label": "Customer",
20389
+ "order": 1,
20390
+ "required": true
20391
+ },
20392
+ "vendorId": {
20393
+ "visibility": "hidden"
20394
+ },
20395
+ "subtotal": {
20396
+ "visibility": "hidden"
20397
+ },
20398
+ "taxAmount": {
20399
+ "visibility": "hidden"
20400
+ },
20401
+ "channelId": {
20402
+ "visibility": "hidden"
20403
+ }
20404
+ }
20405
+ },
20406
+ "@happyvertical/smrt-commerce:ContractLineItem": {
20407
+ "fields": {
20408
+ "sku": {
20409
+ "visibility": "hidden"
20410
+ },
20411
+ "startDate": {
20412
+ "visibility": "hidden"
20413
+ },
20414
+ "endDate": {
20415
+ "visibility": "hidden"
20416
+ },
20417
+ "billingPeriod": {
20418
+ "visibility": "hidden"
20419
+ },
20420
+ "sortOrder": {
20421
+ "visibility": "hidden"
20422
+ },
20423
+ "discount": {
20424
+ "visibility": "hidden"
20425
+ },
20426
+ "taxRate": {
20427
+ "help": "Type a percentage: 5 means 5%."
20428
+ }
20429
+ }
20430
+ }
20431
+ },
20432
+ "help": {
20433
+ "markdown": "## Overview\n\nLeases are agreements where a customer rents something from you for a period.\nEach lease keeps its own line items and dates.\n\n## Tasks\n\n### Record a lease\n\n1. Create a new lease.\n2. Pick the **{field:customerId}** renting from you.\n3. Set the **{field:issueDate}** it starts and the **{field:expiryDate}** it ends.\n4. Add a line item for each thing rented, then save.\n\n### Keep an eye on it\n\n1. Update **{field:status}** as the lease goes from draft to accepted to\n completed.\n2. Check **{field:expiryDate}** to see which leases end soon.\n",
20434
+ "fieldRefs": [
20435
+ "customerId",
20436
+ "expiryDate",
20437
+ "issueDate",
20438
+ "status"
20439
+ ]
20440
+ },
20441
+ "demo": {
20442
+ "mode": "server",
20443
+ "reasons": [
20444
+ "Needs a server because its package does not build for a browser: @happyvertical/payments btcpay (node:crypto); core migrations (node:os). The jobs runner edge (#3615) is reported on smrt-jobs; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-ledgers, @happyvertical/smrt-subscriptions, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3626, #3627)."
20445
+ ]
20446
+ }
20447
+ },
20448
+ {
20449
+ "id": "commerce.licenses",
20450
+ "className": "LicensesRecipe",
20451
+ "label": "Licence sales",
20452
+ "summary": "Sell licences to software, content or other rights.",
20453
+ "synonyms": [
20454
+ "licenses",
20455
+ "licensing",
20456
+ "subscriptions"
20457
+ ],
20458
+ "models": [
20459
+ "@happyvertical/smrt-commerce:LicenseSale",
20460
+ "@happyvertical/smrt-commerce:ContractLineItem"
20461
+ ],
20462
+ "nav": [
20463
+ {
20464
+ "label": "Licence sales",
20465
+ "model": "@happyvertical/smrt-commerce:LicenseSale",
20466
+ "icon": "shoppingBag",
20467
+ "description": "Licences you have sold and who holds them."
20468
+ }
20469
+ ],
20470
+ "requires": [
20471
+ "commerce.customers"
20472
+ ],
20473
+ "group": {
20474
+ "id": "agreements",
20475
+ "label": "Agreements",
20476
+ "summary": "Contracts with customers: general agreements, leases and licence sales."
20477
+ },
20478
+ "section": {
20479
+ "id": "agreements",
20480
+ "label": "Agreements",
20481
+ "icon": "fileText",
20482
+ "description": "Contracts, leases and licenses with customers."
20483
+ },
20484
+ "options": {
20485
+ "@happyvertical/smrt-commerce:LicenseSale": {
20486
+ "fields": {
20487
+ "contractType": {
20488
+ "default": "license_sale",
20489
+ "visibility": "hidden",
20490
+ "locked": true
20491
+ },
20492
+ "customerId": {
20493
+ "label": "Customer",
20494
+ "order": 1,
20495
+ "required": true
20496
+ },
20497
+ "vendorId": {
20498
+ "visibility": "hidden"
20499
+ },
20500
+ "subtotal": {
20501
+ "visibility": "hidden"
20502
+ },
20503
+ "taxAmount": {
20504
+ "visibility": "hidden"
20505
+ },
20506
+ "channelId": {
20507
+ "visibility": "hidden"
20508
+ }
20509
+ }
20510
+ },
20511
+ "@happyvertical/smrt-commerce:ContractLineItem": {
20512
+ "fields": {
20513
+ "sku": {
20514
+ "visibility": "hidden"
20515
+ },
20516
+ "startDate": {
20517
+ "visibility": "hidden"
20518
+ },
20519
+ "endDate": {
20520
+ "visibility": "hidden"
20521
+ },
20522
+ "billingPeriod": {
20523
+ "visibility": "hidden"
20524
+ },
20525
+ "sortOrder": {
20526
+ "visibility": "hidden"
20527
+ },
20528
+ "discount": {
20529
+ "visibility": "hidden"
20530
+ },
20531
+ "taxRate": {
20532
+ "help": "Type a percentage: 5 means 5%."
20533
+ }
20534
+ }
20535
+ }
20536
+ },
20537
+ "help": {
20538
+ "markdown": "## Overview\n\nLicence sales record selling the right to use something, such as software or\ncontent, rather than a physical item.\n\n## Tasks\n\n### Record a licence sale\n\n1. Create a new licence sale.\n2. Pick the **{field:customerId}** who bought the licence.\n3. Add a line item for what is licensed and check the **{field:totalAmount}**.\n4. Set the **{field:expiryDate}** if the licence runs out, then save.\n\n### Follow it\n\n1. Update **{field:status}** when the customer accepts or the sale completes.\n",
20539
+ "fieldRefs": [
20540
+ "customerId",
20541
+ "expiryDate",
20542
+ "status",
20543
+ "totalAmount"
20544
+ ]
20545
+ },
20546
+ "demo": {
20547
+ "mode": "server",
20548
+ "reasons": [
20549
+ "Needs a server because its package does not build for a browser: @happyvertical/payments btcpay (node:crypto); core migrations (node:os). The jobs runner edge (#3615) is reported on smrt-jobs; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-ledgers, @happyvertical/smrt-subscriptions, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3626, #3627)."
20550
+ ]
20551
+ }
20552
+ },
20553
+ {
20554
+ "id": "commerce.purchases",
20555
+ "className": "PurchasesRecipe",
20556
+ "label": "Purchases",
20557
+ "summary": "Order from your vendors and track what you have bought.",
20558
+ "synonyms": [
20559
+ "purchase orders",
20560
+ "buying",
20561
+ "procurement"
20562
+ ],
20563
+ "models": [
20564
+ "@happyvertical/smrt-commerce:PurchaseOrder",
20565
+ "@happyvertical/smrt-commerce:ContractLineItem"
20566
+ ],
20567
+ "nav": [
20568
+ {
20569
+ "label": "Purchase orders",
20570
+ "model": "@happyvertical/smrt-commerce:PurchaseOrder",
20571
+ "icon": "truck",
20572
+ "description": "What you have ordered from your suppliers and when it arrives."
20573
+ }
20574
+ ],
20575
+ "requires": [
20576
+ "commerce.vendors"
20577
+ ],
20578
+ "section": {
20579
+ "id": "purchasing",
20580
+ "label": "Purchasing",
20581
+ "icon": "truck",
20582
+ "description": "Vendors and purchase orders: everything you buy."
20583
+ },
20584
+ "options": {
20585
+ "@happyvertical/smrt-commerce:PurchaseOrder": {
20586
+ "fields": {
20587
+ "contractType": {
20588
+ "default": "purchase_order",
20589
+ "visibility": "hidden",
20590
+ "locked": true
20591
+ },
20592
+ "customerId": {
20593
+ "visibility": "hidden",
20594
+ "locked": true
20595
+ },
20596
+ "vendorId": {
20597
+ "label": "Vendor",
20598
+ "order": 1
20599
+ },
20600
+ "channelId": {
20601
+ "visibility": "hidden"
20602
+ },
20603
+ "expiryDate": {
20604
+ "visibility": "hidden"
20605
+ }
20606
+ }
20607
+ },
20608
+ "@happyvertical/smrt-commerce:ContractLineItem": {
20609
+ "fields": {
20610
+ "sku": {
20611
+ "visibility": "hidden"
20612
+ },
20613
+ "startDate": {
20614
+ "visibility": "hidden"
20615
+ },
20616
+ "endDate": {
20617
+ "visibility": "hidden"
20618
+ },
20619
+ "billingPeriod": {
20620
+ "visibility": "hidden"
20621
+ },
20622
+ "sortOrder": {
20623
+ "visibility": "hidden"
20624
+ },
20625
+ "discount": {
20626
+ "visibility": "hidden"
20627
+ },
20628
+ "taxRate": {
20629
+ "help": "Type a percentage: 5 means 5%."
20630
+ }
20631
+ }
20632
+ }
20633
+ },
20634
+ "help": {
20635
+ "markdown": "## Overview\n\nPurchases is where you order from your vendors and keep track of what you have\nbought. Every purchase order belongs to one of your vendors and records the\namounts, the dates and where the order stands.\n\n## Tasks\n\n### Place a purchase order\n\n1. Choose the **New** button above the list.\n2. Enter the **{field:vendorId}** you are buying from. Add them under Vendors\n first so they are on your list.\n3. Enter the **{field:subtotal}** and **{field:taxAmount}**, then the\n **{field:totalAmount}** you expect to pay.\n4. Set the **{field:issueDate}**, and the **{field:dueDate}** for when the\n goods or the payment are due.\n5. Add your own **{field:reference}**, such as the vendor's quote number, so\n you can match it to their paperwork.\n6. Save.\n\n### Follow an order until it arrives\n\n1. Open the purchase order from the list and choose Edit.\n2. Update **{field:status}** as the order moves along, for example from draft to\n sent, accepted and completed.\n3. Use **{field:notes}** to record what the vendor told you, such as delays or\n partial shipments.\n\n### Record the agreement\n\n1. Open the purchase order and choose Edit.\n2. Write down the **{field:terms}** you agreed with the vendor, such as payment\n or delivery conditions.\n3. Set **{field:expiryDate}** if the vendor's offer is only good until a certain\n day.\n",
20636
+ "fieldRefs": [
20637
+ "dueDate",
20638
+ "expiryDate",
20639
+ "issueDate",
20640
+ "notes",
20641
+ "reference",
20642
+ "status",
20643
+ "subtotal",
20644
+ "taxAmount",
20645
+ "terms",
20646
+ "totalAmount",
20647
+ "vendorId"
20648
+ ]
20649
+ },
20650
+ "demo": {
20651
+ "mode": "server",
20652
+ "reasons": [
20653
+ "Needs a server because its package does not build for a browser: @happyvertical/payments btcpay (node:crypto); core migrations (node:os). The jobs runner edge (#3615) is reported on smrt-jobs; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-ledgers, @happyvertical/smrt-subscriptions, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3626, #3627)."
20654
+ ]
20655
+ }
20656
+ },
20657
+ {
20658
+ "id": "commerce.sales",
20659
+ "className": "SalesRecipe",
20660
+ "label": "Sales",
20661
+ "summary": "Take customer orders and track them.",
20662
+ "synonyms": [
20663
+ "sales orders",
20664
+ "orders",
20665
+ "customer orders"
20666
+ ],
20667
+ "models": [
20668
+ "@happyvertical/smrt-commerce:Order",
20669
+ "@happyvertical/smrt-commerce:ContractLineItem"
20670
+ ],
20671
+ "nav": [
20672
+ {
20673
+ "label": "Sales orders",
20674
+ "model": "@happyvertical/smrt-commerce:Order",
20675
+ "icon": "shoppingBag",
20676
+ "description": "Orders from your customers, from placed to delivered."
20677
+ }
20678
+ ],
20679
+ "requires": [
20680
+ "commerce.customers"
20681
+ ],
20682
+ "section": {
20683
+ "id": "sales",
20684
+ "label": "Sales",
20685
+ "icon": "shoppingBag",
20686
+ "description": "Customers, orders and quotes: everything you sell."
20687
+ },
20688
+ "options": {
20689
+ "@happyvertical/smrt-commerce:Order": {
20690
+ "fields": {
20691
+ "contractType": {
20692
+ "default": "order",
20693
+ "visibility": "hidden",
20694
+ "locked": true
20695
+ },
20696
+ "vendorId": {
20697
+ "visibility": "hidden",
20698
+ "locked": true
20699
+ },
20700
+ "customerId": {
20701
+ "label": "Customer",
20702
+ "order": 1,
20703
+ "required": true
20704
+ },
20705
+ "channelId": {
20706
+ "visibility": "hidden"
20707
+ },
20708
+ "expiryDate": {
20709
+ "visibility": "hidden"
20710
+ }
20711
+ }
20712
+ },
20713
+ "@happyvertical/smrt-commerce:ContractLineItem": {
20714
+ "fields": {
20715
+ "sku": {
20716
+ "visibility": "hidden"
20717
+ },
20718
+ "startDate": {
20719
+ "visibility": "hidden"
20720
+ },
20721
+ "endDate": {
20722
+ "visibility": "hidden"
20723
+ },
20724
+ "billingPeriod": {
20725
+ "visibility": "hidden"
20726
+ },
20727
+ "sortOrder": {
20728
+ "visibility": "hidden"
20729
+ },
20730
+ "discount": {
20731
+ "visibility": "hidden"
20732
+ },
20733
+ "taxRate": {
20734
+ "help": "Type a percentage: 5 means 5%."
19855
20735
  }
19856
20736
  }
19857
20737
  }
@@ -19871,13 +20751,19 @@
19871
20751
  "terms",
19872
20752
  "totalAmount"
19873
20753
  ]
20754
+ },
20755
+ "demo": {
20756
+ "mode": "server",
20757
+ "reasons": [
20758
+ "Needs a server because its package does not build for a browser: @happyvertical/payments btcpay (node:crypto); core migrations (node:os). The jobs runner edge (#3615) is reported on smrt-jobs; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-ledgers, @happyvertical/smrt-subscriptions, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3626, #3627)."
20759
+ ]
19874
20760
  }
19875
20761
  },
19876
20762
  {
19877
20763
  "id": "commerce.vendors",
19878
20764
  "className": "VendorsRecipe",
19879
20765
  "label": "Vendors",
19880
- "summary": "Keep a list of the vendors and suppliers you buy from.",
20766
+ "summary": "Keep a list of the suppliers you buy from.",
19881
20767
  "synonyms": [
19882
20768
  "suppliers",
19883
20769
  "sellers"
@@ -19888,10 +20774,28 @@
19888
20774
  "nav": [
19889
20775
  {
19890
20776
  "label": "Vendors",
19891
- "model": "@happyvertical/smrt-commerce:Vendor"
20777
+ "model": "@happyvertical/smrt-commerce:Vendor",
20778
+ "icon": "briefcase",
20779
+ "description": "The suppliers you buy from, with terms and contacts."
19892
20780
  }
19893
20781
  ],
19894
20782
  "requires": [],
20783
+ "section": {
20784
+ "id": "purchasing",
20785
+ "label": "Purchasing",
20786
+ "icon": "truck",
20787
+ "description": "Vendors and purchase orders: everything you buy."
20788
+ },
20789
+ "options": {
20790
+ "@happyvertical/smrt-commerce:Vendor": {
20791
+ "fields": {
20792
+ "profileId": {
20793
+ "label": "Name",
20794
+ "order": -1
20795
+ }
20796
+ }
20797
+ }
20798
+ },
19895
20799
  "help": {
19896
20800
  "markdown": "## Overview\n\nVendors is your list of the suppliers you buy from. Each record holds who they\nare, how to reach them and the terms you have agreed, so you can order from them\nquickly and keep track of what you have bought.\n\n## Tasks\n\n### Add a vendor\n\n1. Choose the **New** button above the list.\n2. Enter **{field:defaultContactEmail}** and **{field:defaultContactPhone}** for\n the person you normally deal with.\n3. Set the **{field:currency}** they bill you in and your **{field:paymentTerms}**,\n for example \"Net 30\".\n4. Save. The vendor is now on your list and ready to use on a purchase order.\n\n### Note how a vendor likes to work\n\n1. Open the vendor from the list and choose Edit.\n2. Enter **{field:leadTimeDays}**, the number of days they usually need between\n your order and delivery, so you can plan ahead.\n3. Enter **{field:minimumOrderAmount}** if they will not accept small orders.\n4. Use **{field:notes}** for anything else worth remembering, such as the best\n time to call.\n\n### Keep payment details in one place\n\n1. Open the vendor and choose Edit.\n2. Record where payments to this vendor should be sent, so it is all in one\n place.\n3. Set **{field:status}** to show whether you still buy from them: active,\n inactive or suspended.\n",
19897
20801
  "fieldRefs": [
@@ -19904,7 +20808,127 @@
19904
20808
  "paymentTerms",
19905
20809
  "status"
19906
20810
  ]
20811
+ },
20812
+ "demo": {
20813
+ "mode": "server",
20814
+ "reasons": [
20815
+ "Needs a server because its package does not build for a browser: @happyvertical/payments btcpay (node:crypto); core migrations (node:os). The jobs runner edge (#3615) is reported on smrt-jobs; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-ledgers, @happyvertical/smrt-subscriptions, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3626, #3627)."
20816
+ ]
20817
+ }
20818
+ },
20819
+ {
20820
+ "id": "commerce.wholesale",
20821
+ "className": "WholesaleRecipe",
20822
+ "label": "Wholesale orders",
20823
+ "summary": "Take bulk orders from trade customers.",
20824
+ "synonyms": [
20825
+ "bulk orders",
20826
+ "trade orders",
20827
+ "b2b"
20828
+ ],
20829
+ "models": [
20830
+ "@happyvertical/smrt-commerce:WholesaleOrder",
20831
+ "@happyvertical/smrt-commerce:ContractLineItem"
20832
+ ],
20833
+ "nav": [
20834
+ {
20835
+ "label": "Wholesale orders",
20836
+ "model": "@happyvertical/smrt-commerce:WholesaleOrder",
20837
+ "icon": "shoppingBag",
20838
+ "description": "Bulk orders from cafes, shops and other businesses."
20839
+ }
20840
+ ],
20841
+ "requires": [
20842
+ "commerce.customers"
20843
+ ],
20844
+ "section": {
20845
+ "id": "sales",
20846
+ "label": "Sales",
20847
+ "icon": "shoppingBag",
20848
+ "description": "Customers, orders and quotes: everything you sell."
20849
+ },
20850
+ "options": {
20851
+ "@happyvertical/smrt-commerce:WholesaleOrder": {
20852
+ "fields": {
20853
+ "contractType": {
20854
+ "default": "wholesale_order",
20855
+ "visibility": "hidden",
20856
+ "locked": true
20857
+ },
20858
+ "vendorId": {
20859
+ "visibility": "hidden",
20860
+ "locked": true
20861
+ },
20862
+ "customerId": {
20863
+ "label": "Customer",
20864
+ "order": 1
20865
+ },
20866
+ "channelId": {
20867
+ "visibility": "hidden"
20868
+ },
20869
+ "expiryDate": {
20870
+ "visibility": "hidden"
20871
+ }
20872
+ }
20873
+ },
20874
+ "@happyvertical/smrt-commerce:ContractLineItem": {
20875
+ "fields": {
20876
+ "sku": {
20877
+ "visibility": "hidden"
20878
+ },
20879
+ "startDate": {
20880
+ "visibility": "hidden"
20881
+ },
20882
+ "endDate": {
20883
+ "visibility": "hidden"
20884
+ },
20885
+ "billingPeriod": {
20886
+ "visibility": "hidden"
20887
+ },
20888
+ "sortOrder": {
20889
+ "visibility": "hidden"
20890
+ },
20891
+ "discount": {
20892
+ "visibility": "hidden"
20893
+ },
20894
+ "taxRate": {
20895
+ "help": "Type a percentage: 5 means 5%."
20896
+ }
20897
+ }
20898
+ }
20899
+ },
20900
+ "help": {
20901
+ "markdown": "## Overview\n\nWholesale orders are bulk orders from trade customers. They work like sales\norders, with a list of line items, but are kept in their own list so trade and\nretail sales stay apart.\n\n## Tasks\n\n### Take a wholesale order\n\n1. Create a new wholesale order.\n2. Pick the **{field:customerId}** placing the order.\n3. Add the line items and check the **{field:totalAmount}**.\n4. Set the **{field:dueDate}** for payment and save.\n\n### Track it\n\n1. Update **{field:status}** as the order moves from draft to completed.\n2. Use **{field:reference}** for the buyer's own order number.\n",
20902
+ "fieldRefs": [
20903
+ "customerId",
20904
+ "dueDate",
20905
+ "reference",
20906
+ "status",
20907
+ "totalAmount"
20908
+ ]
20909
+ },
20910
+ "demo": {
20911
+ "mode": "server",
20912
+ "reasons": [
20913
+ "Needs a server because its package does not build for a browser: @happyvertical/payments btcpay (node:crypto); core migrations (node:os). The jobs runner edge (#3615) is reported on smrt-jobs; and its package depends on @happyvertical/smrt-jobs, @happyvertical/smrt-ledgers, @happyvertical/smrt-subscriptions, @happyvertical/smrt-tenancy, which do not build for a browser (#3615, #3624, #3626, #3627)."
20914
+ ]
19907
20915
  }
19908
20916
  }
19909
- ]
20917
+ ],
20918
+ "browser": {
20919
+ "status": "server-only",
20920
+ "issues": [
20921
+ "#3615",
20922
+ "#3624",
20923
+ "#3626",
20924
+ "#3627"
20925
+ ],
20926
+ "reason": "@happyvertical/payments btcpay (node:crypto); core migrations (node:os). The jobs runner edge (#3615) is reported on smrt-jobs",
20927
+ "via": [
20928
+ "@happyvertical/smrt-jobs",
20929
+ "@happyvertical/smrt-ledgers",
20930
+ "@happyvertical/smrt-subscriptions",
20931
+ "@happyvertical/smrt-tenancy"
20932
+ ]
20933
+ }
19910
20934
  }