@happyvertical/smrt-commerce 0.57.10 → 0.57.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/README.md +61 -0
- package/agents/connected-payments.md +135 -0
- package/agents/contract-billing-schedules.md +6 -0
- package/agents/money-upgrade.md +5 -0
- package/dist/chunks/{ContractLineItemCollection-DQZqRiZi.js → ContractLineItemCollection-DkBeBR7a.js} +4 -4
- package/dist/chunks/{ContractLineItemCollection-DQZqRiZi.js.map → ContractLineItemCollection-DkBeBR7a.js.map} +1 -1
- package/dist/chunks/{DocumentTaxCollection-Dg942m_Z.js → DocumentTaxCollection-D85ut-Fo.js} +14 -10
- package/dist/chunks/{DocumentTaxCollection-Dg942m_Z.js.map → DocumentTaxCollection-D85ut-Fo.js.map} +1 -1
- package/dist/chunks/{InvoiceCollection-XPcPyibo.js → InvoiceCollection-Datg_eZM.js} +9 -9
- package/dist/chunks/{InvoiceCollection-XPcPyibo.js.map → InvoiceCollection-Datg_eZM.js.map} +1 -1
- package/dist/chunks/{TaxCodeCollection-CbTBWO8z.js → TaxCodeCollection-CIC1skxr.js} +2 -2
- package/dist/chunks/{TaxCodeCollection-CbTBWO8z.js.map → TaxCodeCollection-CIC1skxr.js.map} +1 -1
- package/dist/chunks/{TaxComponentCollection-VZ_cGr2n.js → TaxComponentCollection-BHBV-_1J.js} +3 -3
- package/dist/chunks/{TaxComponentCollection-VZ_cGr2n.js.map → TaxComponentCollection-BHBV-_1J.js.map} +1 -1
- package/dist/chunks/{allocation-integrity-Kr8uceRx.js → allocation-integrity-B2SyO1mu.js} +6 -6
- package/dist/chunks/{allocation-integrity-Kr8uceRx.js.map → allocation-integrity-B2SyO1mu.js.map} +1 -1
- package/dist/chunks/{contract-money-DGNqQwf0.js → contract-money-C61kGBkU.js} +7 -6
- package/dist/chunks/contract-money-C61kGBkU.js.map +1 -0
- package/dist/chunks/{invoice-posting-DVSkfSUm.js → invoice-posting-uLfdyLIi.js} +3 -3
- package/dist/chunks/{invoice-posting-DVSkfSUm.js.map → invoice-posting-uLfdyLIi.js.map} +1 -1
- package/dist/chunks/{retention-accounting-Ct5Df6xx.js → retention-accounting-DGuXzt9g.js} +149 -11
- package/dist/chunks/retention-accounting-DGuXzt9g.js.map +1 -0
- package/dist/chunks/{tenant-money-barrier-CG78Xxja.js → tenant-money-barrier-DXnJWxiZ.js} +3 -3
- package/dist/chunks/{tenant-money-barrier-CG78Xxja.js.map → tenant-money-barrier-DXnJWxiZ.js.map} +1 -1
- package/dist/chunks/types-Bbm0Xflc.js.map +1 -1
- package/dist/collections/TenantPaymentAccountCollection.d.ts +28 -0
- package/dist/collections/TenantPaymentAccountCollection.d.ts.map +1 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/connected-payments-internal.d.ts +5 -0
- package/dist/connected-payments-internal.d.ts.map +1 -0
- package/dist/connected-payments.d.ts +361 -0
- package/dist/connected-payments.d.ts.map +1 -0
- package/dist/contract-money.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1241 -160
- package/dist/index.js.map +1 -1
- package/dist/invoices.js +1 -1
- package/dist/manifest.json +1404 -129
- package/dist/models/Contract.d.ts +5 -1
- package/dist/models/Contract.d.ts.map +1 -1
- package/dist/models/Payment.d.ts +33 -0
- package/dist/models/Payment.d.ts.map +1 -1
- package/dist/models/PaymentIntent.d.ts +35 -0
- package/dist/models/PaymentIntent.d.ts.map +1 -1
- package/dist/models/TenantPaymentAccount.d.ts +121 -0
- package/dist/models/TenantPaymentAccount.d.ts.map +1 -0
- package/dist/models/TenantPaymentWebhookEvent.d.ts +50 -0
- package/dist/models/TenantPaymentWebhookEvent.d.ts.map +1 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/recipes.d.ts +15 -0
- package/dist/recipes.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +622 -27
- package/dist/types/index.d.ts +16 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/upgrade/money-v2.d.ts +2 -0
- package/dist/upgrade/money-v2.d.ts.map +1 -1
- package/package.json +16 -16
- package/dist/chunks/contract-money-DGNqQwf0.js.map +0 -1
- package/dist/chunks/retention-accounting-Ct5Df6xx.js.map +0 -1
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,21 +3,22 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-commerce",
|
|
6
|
-
"packageVersion": "0.57.
|
|
6
|
+
"packageVersion": "0.57.11",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "c001bded9f42eeff4a07e5da8942841a1ae0789a80b2f8506cdfdeb5c6f3ff36",
|
|
11
|
+
"packageJson": "a94869584d49bb7ca3a59c58b388cee5e9d77ecc4eb66c264c37f35bfef1e231",
|
|
12
|
+
"agents": "8fae989030badeabdb15109fab804aa7910c01570d449046c24a04678fe432a7",
|
|
13
13
|
"moduleDoc:agents/customer-vendor-ui.md": "2b22e519411a16d8c493496fc168da77b9acd25f6077ea8fbac00cfee3a3501c",
|
|
14
|
-
"moduleDoc:README.md": "
|
|
14
|
+
"moduleDoc:README.md": "beb2fada61d0babc7aa0528b883a9d47d6c827d876ace1cce04a908338fa01f8",
|
|
15
15
|
"moduleDoc:agents/invoice-quantity.md": "3a5fbb464c8fe63acc021b2259ba36f052bb22c49b29e503639c5316814e2028",
|
|
16
|
+
"moduleDoc:agents/connected-payments.md": "4f16eed36c4b5270eff5fade2c9fa51632320fb76323068b4273ecbfbb351e32",
|
|
16
17
|
"moduleDoc:agents/historical-sales.md": "d7e68bb3a93495c91449b7406a5eefdcddb54fafea83d14d5ba8e2c8d6168de4",
|
|
17
18
|
"moduleDoc:agents/payment-allocations.md": "679894f603f62522515bce338741bafda3f10d16ef0b817b105dc2e732471fdb",
|
|
18
19
|
"moduleDoc:agents/tax-codes.md": "6b07282f0ac93db4fd42ddb4a9551d880e24ec2bd2fae9d06c06561313b3cb9f",
|
|
19
20
|
"moduleDoc:agents/document-money.md": "7f198737c66d190dbdc9cc9a3dbef460fb7c9fb8ee47dd0eaaadde2f2ba91d47",
|
|
20
|
-
"moduleDoc:agents/money-upgrade.md": "
|
|
21
|
+
"moduleDoc:agents/money-upgrade.md": "8e9664e5c0c572654ac0cb4b6d7516415936bbba63d050fa11010b11c2d7e63a",
|
|
21
22
|
"moduleDoc:agents/contract-money.md": "3806a844f0ee14fe429ac74880d59e728d6eab2eb991f83743ffcd6669388729",
|
|
22
23
|
"moduleDoc:agents/money-model.md": "ce996509c0a522aaa2f7fd63e66ae8a9958c2737aafb5578304d6122ca7e996a",
|
|
23
24
|
"moduleDoc:agents/money-calculator.md": "5cf5e89c926fc8a2ac4c4e65d85c0c4444fd323aa4131259bfe66843da48393d",
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
"moduleDoc:agents/quote-ui.md": "9d1faaa12f39e330294693f5309d9766c593dc0f7ced9e8b1dcaf5c81b066205",
|
|
26
27
|
"moduleDoc:agents/purchasing-ui.md": "d2b413009d466f3320934adf1e30ffbb7278138b859df69635e7c1f77efbbd1c",
|
|
27
28
|
"moduleDoc:agents/cost-billing.md": "eaedcc68bb5c2ad7994bf1fc873175b6b800a6ca87e04ba6a51e041d4b765561",
|
|
28
|
-
"moduleDoc:agents/contract-billing-schedules.md": "
|
|
29
|
+
"moduleDoc:agents/contract-billing-schedules.md": "3a6a0a88279f90dcc6d20dc7f0673601784ed78a06741b3d5354f9f1b13077a4",
|
|
29
30
|
"moduleDoc:agents/customer-funds.md": "417af24b9de9c41c89917e3c6e50e324a1baa70de9e45a61772b3d84c8bfeda0",
|
|
30
31
|
"moduleDoc:agents/observed-purchase-prices.md": "e8e68d733b7b9453b4dbd54e1dcf11e1486b70ee855ac629bb4e6888eb90ca9c",
|
|
31
32
|
"moduleDoc:agents/received-sales-contract-money.md": "675d728c565346e5d808e8299a55f2543a423853be0ce9abc99dc7041af79570",
|
|
@@ -99,7 +100,7 @@
|
|
|
99
100
|
"smrt"
|
|
100
101
|
],
|
|
101
102
|
"risks": [
|
|
102
|
-
"cross-package-refs:
|
|
103
|
+
"cross-package-refs:19",
|
|
103
104
|
"sensitive-fields-excluded",
|
|
104
105
|
"sti-inheritance"
|
|
105
106
|
],
|
|
@@ -3381,6 +3382,60 @@
|
|
|
3381
3382
|
"tags": [],
|
|
3382
3383
|
"risks": []
|
|
3383
3384
|
},
|
|
3385
|
+
{
|
|
3386
|
+
"name": "TenantPaymentAccountCollection",
|
|
3387
|
+
"qualifiedName": "@happyvertical/smrt-commerce:TenantPaymentAccountCollection",
|
|
3388
|
+
"collection": "tenantpaymentaccounts",
|
|
3389
|
+
"tableName": "tenant_payment_accounts",
|
|
3390
|
+
"packageName": "@happyvertical/smrt-commerce",
|
|
3391
|
+
"extends": "SmrtCollection",
|
|
3392
|
+
"fields": [],
|
|
3393
|
+
"relationships": [],
|
|
3394
|
+
"methods": [
|
|
3395
|
+
"build",
|
|
3396
|
+
"findForTenant"
|
|
3397
|
+
],
|
|
3398
|
+
"methodSignatures": [
|
|
3399
|
+
{
|
|
3400
|
+
"name": "build",
|
|
3401
|
+
"async": true,
|
|
3402
|
+
"params": [
|
|
3403
|
+
"options: any"
|
|
3404
|
+
],
|
|
3405
|
+
"returns": "Promise<TenantPaymentAccount>"
|
|
3406
|
+
},
|
|
3407
|
+
{
|
|
3408
|
+
"name": "findForTenant",
|
|
3409
|
+
"async": true,
|
|
3410
|
+
"params": [
|
|
3411
|
+
"args: object"
|
|
3412
|
+
],
|
|
3413
|
+
"returns": "Promise<TenantPaymentAccount | null>"
|
|
3414
|
+
}
|
|
3415
|
+
],
|
|
3416
|
+
"surfaces": [],
|
|
3417
|
+
"withheldSurfaces": [
|
|
3418
|
+
{
|
|
3419
|
+
"kind": "api",
|
|
3420
|
+
"operation": "build",
|
|
3421
|
+
"code": "not-included",
|
|
3422
|
+
"reason": "not listed in api.include",
|
|
3423
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccountCollection"
|
|
3424
|
+
},
|
|
3425
|
+
{
|
|
3426
|
+
"kind": "api",
|
|
3427
|
+
"operation": "findForTenant",
|
|
3428
|
+
"code": "not-included",
|
|
3429
|
+
"reason": "not listed in api.include",
|
|
3430
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccountCollection"
|
|
3431
|
+
}
|
|
3432
|
+
],
|
|
3433
|
+
"relationshipFeatures": [
|
|
3434
|
+
"uuidColumns"
|
|
3435
|
+
],
|
|
3436
|
+
"tags": [],
|
|
3437
|
+
"risks": []
|
|
3438
|
+
},
|
|
3384
3439
|
{
|
|
3385
3440
|
"name": "VendorCollection",
|
|
3386
3441
|
"qualifiedName": "@happyvertical/smrt-commerce:VendorCollection",
|
|
@@ -3651,7 +3706,8 @@
|
|
|
3651
3706
|
"name": "billingScheduleJson",
|
|
3652
3707
|
"type": "text",
|
|
3653
3708
|
"required": false,
|
|
3654
|
-
"columnType": "TEXT"
|
|
3709
|
+
"columnType": "TEXT",
|
|
3710
|
+
"default": ""
|
|
3655
3711
|
},
|
|
3656
3712
|
{
|
|
3657
3713
|
"name": "lineAmountType",
|
|
@@ -4149,7 +4205,8 @@
|
|
|
4149
4205
|
"name": "billingScheduleJson",
|
|
4150
4206
|
"type": "text",
|
|
4151
4207
|
"required": false,
|
|
4152
|
-
"columnType": "TEXT"
|
|
4208
|
+
"columnType": "TEXT",
|
|
4209
|
+
"default": ""
|
|
4153
4210
|
},
|
|
4154
4211
|
{
|
|
4155
4212
|
"name": "lineAmountType",
|
|
@@ -4570,7 +4627,8 @@
|
|
|
4570
4627
|
"name": "billingScheduleJson",
|
|
4571
4628
|
"type": "text",
|
|
4572
4629
|
"required": false,
|
|
4573
|
-
"columnType": "TEXT"
|
|
4630
|
+
"columnType": "TEXT",
|
|
4631
|
+
"default": ""
|
|
4574
4632
|
},
|
|
4575
4633
|
{
|
|
4576
4634
|
"name": "lineAmountType",
|
|
@@ -4990,7 +5048,8 @@
|
|
|
4990
5048
|
"name": "billingScheduleJson",
|
|
4991
5049
|
"type": "text",
|
|
4992
5050
|
"required": false,
|
|
4993
|
-
"columnType": "TEXT"
|
|
5051
|
+
"columnType": "TEXT",
|
|
5052
|
+
"default": ""
|
|
4994
5053
|
},
|
|
4995
5054
|
{
|
|
4996
5055
|
"name": "lineAmountType",
|
|
@@ -5497,7 +5556,8 @@
|
|
|
5497
5556
|
"name": "billingScheduleJson",
|
|
5498
5557
|
"type": "text",
|
|
5499
5558
|
"required": false,
|
|
5500
|
-
"columnType": "TEXT"
|
|
5559
|
+
"columnType": "TEXT",
|
|
5560
|
+
"default": ""
|
|
5501
5561
|
},
|
|
5502
5562
|
{
|
|
5503
5563
|
"name": "lineAmountType",
|
|
@@ -6004,7 +6064,8 @@
|
|
|
6004
6064
|
"name": "billingScheduleJson",
|
|
6005
6065
|
"type": "text",
|
|
6006
6066
|
"required": false,
|
|
6007
|
-
"columnType": "TEXT"
|
|
6067
|
+
"columnType": "TEXT",
|
|
6068
|
+
"default": ""
|
|
6008
6069
|
},
|
|
6009
6070
|
{
|
|
6010
6071
|
"name": "lineAmountType",
|
|
@@ -6511,7 +6572,8 @@
|
|
|
6511
6572
|
"name": "billingScheduleJson",
|
|
6512
6573
|
"type": "text",
|
|
6513
6574
|
"required": false,
|
|
6514
|
-
"columnType": "TEXT"
|
|
6575
|
+
"columnType": "TEXT",
|
|
6576
|
+
"default": ""
|
|
6515
6577
|
},
|
|
6516
6578
|
{
|
|
6517
6579
|
"name": "lineAmountType",
|
|
@@ -7018,7 +7080,8 @@
|
|
|
7018
7080
|
"name": "billingScheduleJson",
|
|
7019
7081
|
"type": "text",
|
|
7020
7082
|
"required": false,
|
|
7021
|
-
"columnType": "TEXT"
|
|
7083
|
+
"columnType": "TEXT",
|
|
7084
|
+
"default": ""
|
|
7022
7085
|
},
|
|
7023
7086
|
{
|
|
7024
7087
|
"name": "lineAmountType",
|
|
@@ -7525,7 +7588,8 @@
|
|
|
7525
7588
|
"name": "billingScheduleJson",
|
|
7526
7589
|
"type": "text",
|
|
7527
7590
|
"required": false,
|
|
7528
|
-
"columnType": "TEXT"
|
|
7591
|
+
"columnType": "TEXT",
|
|
7592
|
+
"default": ""
|
|
7529
7593
|
},
|
|
7530
7594
|
{
|
|
7531
7595
|
"name": "lineAmountType",
|
|
@@ -8046,7 +8110,8 @@
|
|
|
8046
8110
|
"name": "billingScheduleJson",
|
|
8047
8111
|
"type": "text",
|
|
8048
8112
|
"required": false,
|
|
8049
|
-
"columnType": "TEXT"
|
|
8113
|
+
"columnType": "TEXT",
|
|
8114
|
+
"default": ""
|
|
8050
8115
|
},
|
|
8051
8116
|
{
|
|
8052
8117
|
"name": "lineAmountType",
|
|
@@ -8553,7 +8618,8 @@
|
|
|
8553
8618
|
"name": "billingScheduleJson",
|
|
8554
8619
|
"type": "text",
|
|
8555
8620
|
"required": false,
|
|
8556
|
-
"columnType": "TEXT"
|
|
8621
|
+
"columnType": "TEXT",
|
|
8622
|
+
"default": ""
|
|
8557
8623
|
},
|
|
8558
8624
|
{
|
|
8559
8625
|
"name": "lineAmountType",
|
|
@@ -9073,7 +9139,8 @@
|
|
|
9073
9139
|
"name": "billingScheduleJson",
|
|
9074
9140
|
"type": "text",
|
|
9075
9141
|
"required": false,
|
|
9076
|
-
"columnType": "TEXT"
|
|
9142
|
+
"columnType": "TEXT",
|
|
9143
|
+
"default": ""
|
|
9077
9144
|
},
|
|
9078
9145
|
{
|
|
9079
9146
|
"name": "lineAmountType",
|
|
@@ -9581,7 +9648,8 @@
|
|
|
9581
9648
|
"name": "billingScheduleJson",
|
|
9582
9649
|
"type": "text",
|
|
9583
9650
|
"required": false,
|
|
9584
|
-
"columnType": "TEXT"
|
|
9651
|
+
"columnType": "TEXT",
|
|
9652
|
+
"default": ""
|
|
9585
9653
|
},
|
|
9586
9654
|
{
|
|
9587
9655
|
"name": "lineAmountType",
|
|
@@ -13781,6 +13849,41 @@
|
|
|
13781
13849
|
"required": false,
|
|
13782
13850
|
"columnType": "INTEGER",
|
|
13783
13851
|
"default": 0
|
|
13852
|
+
},
|
|
13853
|
+
{
|
|
13854
|
+
"name": "connectedAccountId",
|
|
13855
|
+
"type": "text",
|
|
13856
|
+
"required": false,
|
|
13857
|
+
"columnType": "TEXT",
|
|
13858
|
+
"default": ""
|
|
13859
|
+
},
|
|
13860
|
+
{
|
|
13861
|
+
"name": "applicationFeeCents",
|
|
13862
|
+
"type": "integer",
|
|
13863
|
+
"required": false,
|
|
13864
|
+
"columnType": "INTEGER",
|
|
13865
|
+
"default": 0
|
|
13866
|
+
},
|
|
13867
|
+
{
|
|
13868
|
+
"name": "refundOfPaymentId",
|
|
13869
|
+
"type": "text",
|
|
13870
|
+
"required": false,
|
|
13871
|
+
"columnType": "TEXT",
|
|
13872
|
+
"default": ""
|
|
13873
|
+
},
|
|
13874
|
+
{
|
|
13875
|
+
"name": "connectedRefundState",
|
|
13876
|
+
"type": "text",
|
|
13877
|
+
"required": false,
|
|
13878
|
+
"columnType": "TEXT",
|
|
13879
|
+
"default": ""
|
|
13880
|
+
},
|
|
13881
|
+
{
|
|
13882
|
+
"name": "applicationFeeRefunded",
|
|
13883
|
+
"type": "boolean",
|
|
13884
|
+
"required": false,
|
|
13885
|
+
"columnType": "BOOLEAN",
|
|
13886
|
+
"default": false
|
|
13784
13887
|
}
|
|
13785
13888
|
],
|
|
13786
13889
|
"relationships": [
|
|
@@ -14701,6 +14804,41 @@
|
|
|
14701
14804
|
"required": false,
|
|
14702
14805
|
"columnType": "TEXT",
|
|
14703
14806
|
"default": ""
|
|
14807
|
+
},
|
|
14808
|
+
{
|
|
14809
|
+
"name": "connectedAccountId",
|
|
14810
|
+
"type": "text",
|
|
14811
|
+
"required": false,
|
|
14812
|
+
"columnType": "TEXT",
|
|
14813
|
+
"default": ""
|
|
14814
|
+
},
|
|
14815
|
+
{
|
|
14816
|
+
"name": "applicationFeeCents",
|
|
14817
|
+
"type": "integer",
|
|
14818
|
+
"required": false,
|
|
14819
|
+
"columnType": "INTEGER",
|
|
14820
|
+
"default": 0
|
|
14821
|
+
},
|
|
14822
|
+
{
|
|
14823
|
+
"name": "connectedEnvironment",
|
|
14824
|
+
"type": "text",
|
|
14825
|
+
"required": false,
|
|
14826
|
+
"columnType": "TEXT",
|
|
14827
|
+
"default": ""
|
|
14828
|
+
},
|
|
14829
|
+
{
|
|
14830
|
+
"name": "connectedAmount",
|
|
14831
|
+
"type": "integer",
|
|
14832
|
+
"required": false,
|
|
14833
|
+
"columnType": "INTEGER",
|
|
14834
|
+
"default": 0
|
|
14835
|
+
},
|
|
14836
|
+
{
|
|
14837
|
+
"name": "connectedCurrency",
|
|
14838
|
+
"type": "text",
|
|
14839
|
+
"required": false,
|
|
14840
|
+
"columnType": "TEXT",
|
|
14841
|
+
"default": ""
|
|
14704
14842
|
}
|
|
14705
14843
|
],
|
|
14706
14844
|
"relationships": [],
|
|
@@ -16371,6 +16509,391 @@
|
|
|
16371
16509
|
"tags": [],
|
|
16372
16510
|
"risks": []
|
|
16373
16511
|
},
|
|
16512
|
+
{
|
|
16513
|
+
"name": "TenantPaymentAccount",
|
|
16514
|
+
"qualifiedName": "@happyvertical/smrt-commerce:TenantPaymentAccount",
|
|
16515
|
+
"collection": "tenantpaymentaccounts",
|
|
16516
|
+
"description": "A tenant's connected payment account for one backend and environment.",
|
|
16517
|
+
"tableName": "tenant_payment_accounts",
|
|
16518
|
+
"packageName": "@happyvertical/smrt-commerce",
|
|
16519
|
+
"extends": "SmrtObject",
|
|
16520
|
+
"fields": [
|
|
16521
|
+
{
|
|
16522
|
+
"name": "tenantId",
|
|
16523
|
+
"type": "text",
|
|
16524
|
+
"required": true,
|
|
16525
|
+
"columnType": "UUID"
|
|
16526
|
+
},
|
|
16527
|
+
{
|
|
16528
|
+
"name": "backendId",
|
|
16529
|
+
"type": "text",
|
|
16530
|
+
"required": true,
|
|
16531
|
+
"columnType": "TEXT",
|
|
16532
|
+
"default": "stripe"
|
|
16533
|
+
},
|
|
16534
|
+
{
|
|
16535
|
+
"name": "environment",
|
|
16536
|
+
"type": "text",
|
|
16537
|
+
"required": true,
|
|
16538
|
+
"columnType": "TEXT",
|
|
16539
|
+
"default": "test"
|
|
16540
|
+
},
|
|
16541
|
+
{
|
|
16542
|
+
"name": "providerAccountId",
|
|
16543
|
+
"type": "text",
|
|
16544
|
+
"required": true,
|
|
16545
|
+
"columnType": "TEXT",
|
|
16546
|
+
"default": ""
|
|
16547
|
+
},
|
|
16548
|
+
{
|
|
16549
|
+
"name": "status",
|
|
16550
|
+
"type": "text",
|
|
16551
|
+
"required": false,
|
|
16552
|
+
"columnType": "TEXT",
|
|
16553
|
+
"default": "pending"
|
|
16554
|
+
},
|
|
16555
|
+
{
|
|
16556
|
+
"name": "chargesEnabled",
|
|
16557
|
+
"type": "boolean",
|
|
16558
|
+
"required": false,
|
|
16559
|
+
"columnType": "BOOLEAN",
|
|
16560
|
+
"default": false
|
|
16561
|
+
},
|
|
16562
|
+
{
|
|
16563
|
+
"name": "detailsSubmitted",
|
|
16564
|
+
"type": "boolean",
|
|
16565
|
+
"required": false,
|
|
16566
|
+
"columnType": "BOOLEAN",
|
|
16567
|
+
"default": false
|
|
16568
|
+
},
|
|
16569
|
+
{
|
|
16570
|
+
"name": "payoutsEnabled",
|
|
16571
|
+
"type": "boolean",
|
|
16572
|
+
"required": false,
|
|
16573
|
+
"columnType": "BOOLEAN",
|
|
16574
|
+
"default": false
|
|
16575
|
+
},
|
|
16576
|
+
{
|
|
16577
|
+
"name": "capabilitiesJson",
|
|
16578
|
+
"type": "text",
|
|
16579
|
+
"required": false,
|
|
16580
|
+
"columnType": "TEXT"
|
|
16581
|
+
},
|
|
16582
|
+
{
|
|
16583
|
+
"name": "capabilitiesObservedAt",
|
|
16584
|
+
"type": "datetime",
|
|
16585
|
+
"required": false,
|
|
16586
|
+
"columnType": "TIMESTAMP"
|
|
16587
|
+
},
|
|
16588
|
+
{
|
|
16589
|
+
"name": "feePolicyRef",
|
|
16590
|
+
"type": "text",
|
|
16591
|
+
"required": false,
|
|
16592
|
+
"columnType": "TEXT",
|
|
16593
|
+
"default": ""
|
|
16594
|
+
},
|
|
16595
|
+
{
|
|
16596
|
+
"name": "disabledAt",
|
|
16597
|
+
"type": "datetime",
|
|
16598
|
+
"required": false,
|
|
16599
|
+
"columnType": "TIMESTAMP"
|
|
16600
|
+
},
|
|
16601
|
+
{
|
|
16602
|
+
"name": "disabledReason",
|
|
16603
|
+
"type": "text",
|
|
16604
|
+
"required": false,
|
|
16605
|
+
"columnType": "TEXT",
|
|
16606
|
+
"default": ""
|
|
16607
|
+
}
|
|
16608
|
+
],
|
|
16609
|
+
"relationships": [],
|
|
16610
|
+
"methods": [
|
|
16611
|
+
"delete",
|
|
16612
|
+
"getCapabilities",
|
|
16613
|
+
"isEnabled",
|
|
16614
|
+
"save",
|
|
16615
|
+
"setCapabilities"
|
|
16616
|
+
],
|
|
16617
|
+
"methodSignatures": [
|
|
16618
|
+
{
|
|
16619
|
+
"name": "delete",
|
|
16620
|
+
"async": true,
|
|
16621
|
+
"params": [
|
|
16622
|
+
"_options?: SmrtDeleteOptions"
|
|
16623
|
+
],
|
|
16624
|
+
"returns": "Promise<void>"
|
|
16625
|
+
},
|
|
16626
|
+
{
|
|
16627
|
+
"name": "getCapabilities",
|
|
16628
|
+
"returns": "TenantPaymentCapabilitySnapshot | null"
|
|
16629
|
+
},
|
|
16630
|
+
{
|
|
16631
|
+
"name": "isEnabled",
|
|
16632
|
+
"returns": "boolean"
|
|
16633
|
+
},
|
|
16634
|
+
{
|
|
16635
|
+
"name": "save",
|
|
16636
|
+
"async": true,
|
|
16637
|
+
"params": [
|
|
16638
|
+
"options?: SmrtSaveOptions"
|
|
16639
|
+
],
|
|
16640
|
+
"returns": "Promise<this>"
|
|
16641
|
+
},
|
|
16642
|
+
{
|
|
16643
|
+
"name": "setCapabilities",
|
|
16644
|
+
"params": [
|
|
16645
|
+
"snapshot: TenantPaymentCapabilitySnapshot",
|
|
16646
|
+
"observedAt: Date"
|
|
16647
|
+
],
|
|
16648
|
+
"returns": "void"
|
|
16649
|
+
}
|
|
16650
|
+
],
|
|
16651
|
+
"tenant": {
|
|
16652
|
+
"scoped": true,
|
|
16653
|
+
"mode": "required",
|
|
16654
|
+
"field": "tenantId"
|
|
16655
|
+
},
|
|
16656
|
+
"conflictColumns": [
|
|
16657
|
+
"tenant_id",
|
|
16658
|
+
"slug",
|
|
16659
|
+
"context"
|
|
16660
|
+
],
|
|
16661
|
+
"surfaces": [
|
|
16662
|
+
{
|
|
16663
|
+
"kind": "api",
|
|
16664
|
+
"name": "tenantpaymentaccounts.list",
|
|
16665
|
+
"operation": "list",
|
|
16666
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount",
|
|
16667
|
+
"path": "/tenantpaymentaccounts",
|
|
16668
|
+
"method": "GET"
|
|
16669
|
+
},
|
|
16670
|
+
{
|
|
16671
|
+
"kind": "api",
|
|
16672
|
+
"name": "tenantpaymentaccounts.get",
|
|
16673
|
+
"operation": "get",
|
|
16674
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount",
|
|
16675
|
+
"path": "/tenantpaymentaccounts/[id]",
|
|
16676
|
+
"method": "GET"
|
|
16677
|
+
},
|
|
16678
|
+
{
|
|
16679
|
+
"kind": "cli",
|
|
16680
|
+
"name": "tenantpaymentaccount_list",
|
|
16681
|
+
"operation": "list",
|
|
16682
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
16683
|
+
},
|
|
16684
|
+
{
|
|
16685
|
+
"kind": "cli",
|
|
16686
|
+
"name": "tenantpaymentaccount_get",
|
|
16687
|
+
"operation": "get",
|
|
16688
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
16689
|
+
},
|
|
16690
|
+
{
|
|
16691
|
+
"kind": "cli",
|
|
16692
|
+
"name": "tenantpaymentaccount_isEnabled",
|
|
16693
|
+
"operation": "isEnabled",
|
|
16694
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
16695
|
+
},
|
|
16696
|
+
{
|
|
16697
|
+
"kind": "cli",
|
|
16698
|
+
"name": "tenantpaymentaccount_getCapabilities",
|
|
16699
|
+
"operation": "getCapabilities",
|
|
16700
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
16701
|
+
},
|
|
16702
|
+
{
|
|
16703
|
+
"kind": "mcp",
|
|
16704
|
+
"name": "tenantpaymentaccount_list",
|
|
16705
|
+
"operation": "list",
|
|
16706
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
16707
|
+
},
|
|
16708
|
+
{
|
|
16709
|
+
"kind": "mcp",
|
|
16710
|
+
"name": "tenantpaymentaccount_get",
|
|
16711
|
+
"operation": "get",
|
|
16712
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
16713
|
+
}
|
|
16714
|
+
],
|
|
16715
|
+
"withheldSurfaces": [
|
|
16716
|
+
{
|
|
16717
|
+
"kind": "api",
|
|
16718
|
+
"operation": "getCapabilities",
|
|
16719
|
+
"code": "not-included",
|
|
16720
|
+
"reason": "not listed in api.include",
|
|
16721
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
16722
|
+
},
|
|
16723
|
+
{
|
|
16724
|
+
"kind": "api",
|
|
16725
|
+
"operation": "isEnabled",
|
|
16726
|
+
"code": "not-included",
|
|
16727
|
+
"reason": "not listed in api.include",
|
|
16728
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
16729
|
+
},
|
|
16730
|
+
{
|
|
16731
|
+
"kind": "api",
|
|
16732
|
+
"operation": "save",
|
|
16733
|
+
"code": "lifecycle-method",
|
|
16734
|
+
"reason": "`save` is a framework lifecycle method, not a distinct operation",
|
|
16735
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
16736
|
+
},
|
|
16737
|
+
{
|
|
16738
|
+
"kind": "api",
|
|
16739
|
+
"operation": "setCapabilities",
|
|
16740
|
+
"code": "not-included",
|
|
16741
|
+
"reason": "not listed in api.include",
|
|
16742
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
16743
|
+
}
|
|
16744
|
+
],
|
|
16745
|
+
"relationshipFeatures": [
|
|
16746
|
+
"uuidColumns"
|
|
16747
|
+
],
|
|
16748
|
+
"tags": [],
|
|
16749
|
+
"risks": []
|
|
16750
|
+
},
|
|
16751
|
+
{
|
|
16752
|
+
"name": "TenantPaymentWebhookEvent",
|
|
16753
|
+
"qualifiedName": "@happyvertical/smrt-commerce:TenantPaymentWebhookEvent",
|
|
16754
|
+
"collection": "tenantpaymentwebhookevents",
|
|
16755
|
+
"description": "One processed provider webhook event.",
|
|
16756
|
+
"tableName": "_smrt_tenant_payment_webhook_events",
|
|
16757
|
+
"packageName": "@happyvertical/smrt-commerce",
|
|
16758
|
+
"extends": "SmrtObject",
|
|
16759
|
+
"fields": [
|
|
16760
|
+
{
|
|
16761
|
+
"name": "backendId",
|
|
16762
|
+
"type": "text",
|
|
16763
|
+
"required": true,
|
|
16764
|
+
"columnType": "TEXT",
|
|
16765
|
+
"default": ""
|
|
16766
|
+
},
|
|
16767
|
+
{
|
|
16768
|
+
"name": "environment",
|
|
16769
|
+
"type": "text",
|
|
16770
|
+
"required": true,
|
|
16771
|
+
"columnType": "TEXT",
|
|
16772
|
+
"default": ""
|
|
16773
|
+
},
|
|
16774
|
+
{
|
|
16775
|
+
"name": "eventId",
|
|
16776
|
+
"type": "text",
|
|
16777
|
+
"required": true,
|
|
16778
|
+
"columnType": "TEXT",
|
|
16779
|
+
"default": ""
|
|
16780
|
+
},
|
|
16781
|
+
{
|
|
16782
|
+
"name": "eventType",
|
|
16783
|
+
"type": "text",
|
|
16784
|
+
"required": false,
|
|
16785
|
+
"columnType": "TEXT",
|
|
16786
|
+
"default": ""
|
|
16787
|
+
},
|
|
16788
|
+
{
|
|
16789
|
+
"name": "endpoint",
|
|
16790
|
+
"type": "text",
|
|
16791
|
+
"required": false,
|
|
16792
|
+
"columnType": "TEXT",
|
|
16793
|
+
"default": ""
|
|
16794
|
+
},
|
|
16795
|
+
{
|
|
16796
|
+
"name": "tenantId",
|
|
16797
|
+
"type": "crossPackageRef",
|
|
16798
|
+
"required": true,
|
|
16799
|
+
"related": "@happyvertical/smrt-users:Tenant",
|
|
16800
|
+
"columnType": "UUID"
|
|
16801
|
+
},
|
|
16802
|
+
{
|
|
16803
|
+
"name": "providerAccountId",
|
|
16804
|
+
"type": "text",
|
|
16805
|
+
"required": false,
|
|
16806
|
+
"columnType": "TEXT",
|
|
16807
|
+
"default": ""
|
|
16808
|
+
},
|
|
16809
|
+
{
|
|
16810
|
+
"name": "outcome",
|
|
16811
|
+
"type": "text",
|
|
16812
|
+
"required": false,
|
|
16813
|
+
"columnType": "TEXT",
|
|
16814
|
+
"default": "ignored"
|
|
16815
|
+
},
|
|
16816
|
+
{
|
|
16817
|
+
"name": "paymentIntentId",
|
|
16818
|
+
"type": "text",
|
|
16819
|
+
"required": false,
|
|
16820
|
+
"columnType": "TEXT",
|
|
16821
|
+
"default": ""
|
|
16822
|
+
},
|
|
16823
|
+
{
|
|
16824
|
+
"name": "applicationFeeJson",
|
|
16825
|
+
"type": "text",
|
|
16826
|
+
"required": false,
|
|
16827
|
+
"columnType": "TEXT"
|
|
16828
|
+
},
|
|
16829
|
+
{
|
|
16830
|
+
"name": "processedAt",
|
|
16831
|
+
"type": "datetime",
|
|
16832
|
+
"required": false,
|
|
16833
|
+
"columnType": "TIMESTAMP"
|
|
16834
|
+
}
|
|
16835
|
+
],
|
|
16836
|
+
"relationships": [
|
|
16837
|
+
{
|
|
16838
|
+
"name": "tenantId",
|
|
16839
|
+
"type": "crossPackageRef",
|
|
16840
|
+
"required": true,
|
|
16841
|
+
"related": "@happyvertical/smrt-users:Tenant",
|
|
16842
|
+
"columnType": "UUID"
|
|
16843
|
+
}
|
|
16844
|
+
],
|
|
16845
|
+
"methods": [],
|
|
16846
|
+
"conflictColumns": [
|
|
16847
|
+
"tenant_id",
|
|
16848
|
+
"slug",
|
|
16849
|
+
"context"
|
|
16850
|
+
],
|
|
16851
|
+
"surfaces": [],
|
|
16852
|
+
"relationshipFeatures": [
|
|
16853
|
+
"crossPackageRef",
|
|
16854
|
+
"uuidColumns"
|
|
16855
|
+
],
|
|
16856
|
+
"tags": [],
|
|
16857
|
+
"risks": []
|
|
16858
|
+
},
|
|
16859
|
+
{
|
|
16860
|
+
"name": "TenantPaymentWebhookEventCollection",
|
|
16861
|
+
"qualifiedName": "@happyvertical/smrt-commerce:TenantPaymentWebhookEventCollection",
|
|
16862
|
+
"collection": "tenantpaymentwebhookevents",
|
|
16863
|
+
"tableName": "_smrt_tenant_payment_webhook_events",
|
|
16864
|
+
"packageName": "@happyvertical/smrt-commerce",
|
|
16865
|
+
"extends": "SmrtCollection",
|
|
16866
|
+
"fields": [],
|
|
16867
|
+
"relationships": [],
|
|
16868
|
+
"methods": [
|
|
16869
|
+
"build"
|
|
16870
|
+
],
|
|
16871
|
+
"methodSignatures": [
|
|
16872
|
+
{
|
|
16873
|
+
"name": "build",
|
|
16874
|
+
"async": true,
|
|
16875
|
+
"params": [
|
|
16876
|
+
"options: any"
|
|
16877
|
+
],
|
|
16878
|
+
"returns": "Promise<TenantPaymentWebhookEvent>"
|
|
16879
|
+
}
|
|
16880
|
+
],
|
|
16881
|
+
"surfaces": [],
|
|
16882
|
+
"withheldSurfaces": [
|
|
16883
|
+
{
|
|
16884
|
+
"kind": "api",
|
|
16885
|
+
"operation": "build",
|
|
16886
|
+
"code": "api-disabled",
|
|
16887
|
+
"reason": "api is disabled",
|
|
16888
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentWebhookEventCollection"
|
|
16889
|
+
}
|
|
16890
|
+
],
|
|
16891
|
+
"relationshipFeatures": [
|
|
16892
|
+
"uuidColumns"
|
|
16893
|
+
],
|
|
16894
|
+
"tags": [],
|
|
16895
|
+
"risks": []
|
|
16896
|
+
},
|
|
16374
16897
|
{
|
|
16375
16898
|
"name": "Vendor",
|
|
16376
16899
|
"qualifiedName": "@happyvertical/smrt-commerce:Vendor",
|
|
@@ -21325,6 +21848,58 @@
|
|
|
21325
21848
|
"operation": "get",
|
|
21326
21849
|
"objectName": "@happyvertical/smrt-commerce:TaxSummaryReport"
|
|
21327
21850
|
},
|
|
21851
|
+
{
|
|
21852
|
+
"kind": "api",
|
|
21853
|
+
"name": "tenantpaymentaccounts.list",
|
|
21854
|
+
"operation": "list",
|
|
21855
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount",
|
|
21856
|
+
"path": "/tenantpaymentaccounts",
|
|
21857
|
+
"method": "GET"
|
|
21858
|
+
},
|
|
21859
|
+
{
|
|
21860
|
+
"kind": "api",
|
|
21861
|
+
"name": "tenantpaymentaccounts.get",
|
|
21862
|
+
"operation": "get",
|
|
21863
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount",
|
|
21864
|
+
"path": "/tenantpaymentaccounts/[id]",
|
|
21865
|
+
"method": "GET"
|
|
21866
|
+
},
|
|
21867
|
+
{
|
|
21868
|
+
"kind": "cli",
|
|
21869
|
+
"name": "tenantpaymentaccount_list",
|
|
21870
|
+
"operation": "list",
|
|
21871
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
21872
|
+
},
|
|
21873
|
+
{
|
|
21874
|
+
"kind": "cli",
|
|
21875
|
+
"name": "tenantpaymentaccount_get",
|
|
21876
|
+
"operation": "get",
|
|
21877
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
21878
|
+
},
|
|
21879
|
+
{
|
|
21880
|
+
"kind": "cli",
|
|
21881
|
+
"name": "tenantpaymentaccount_isEnabled",
|
|
21882
|
+
"operation": "isEnabled",
|
|
21883
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
21884
|
+
},
|
|
21885
|
+
{
|
|
21886
|
+
"kind": "cli",
|
|
21887
|
+
"name": "tenantpaymentaccount_getCapabilities",
|
|
21888
|
+
"operation": "getCapabilities",
|
|
21889
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
21890
|
+
},
|
|
21891
|
+
{
|
|
21892
|
+
"kind": "mcp",
|
|
21893
|
+
"name": "tenantpaymentaccount_list",
|
|
21894
|
+
"operation": "list",
|
|
21895
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
21896
|
+
},
|
|
21897
|
+
{
|
|
21898
|
+
"kind": "mcp",
|
|
21899
|
+
"name": "tenantpaymentaccount_get",
|
|
21900
|
+
"operation": "get",
|
|
21901
|
+
"objectName": "@happyvertical/smrt-commerce:TenantPaymentAccount"
|
|
21902
|
+
},
|
|
21328
21903
|
{
|
|
21329
21904
|
"kind": "api",
|
|
21330
21905
|
"name": "vendors.list",
|
|
@@ -21439,13 +22014,13 @@
|
|
|
21439
22014
|
"prompts": [],
|
|
21440
22015
|
"relationshipsV2": {
|
|
21441
22016
|
"foreignKeyFields": 47,
|
|
21442
|
-
"crossPackageRefFields":
|
|
22017
|
+
"crossPackageRefFields": 19,
|
|
21443
22018
|
"junctionCollections": 0,
|
|
21444
22019
|
"hierarchicalObjects": 0,
|
|
21445
22020
|
"polymorphicAssociations": 0,
|
|
21446
|
-
"uuidColumns":
|
|
22021
|
+
"uuidColumns": 270
|
|
21447
22022
|
},
|
|
21448
|
-
"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). `contacts` (JSON) is written via [`savePartyContacts()`](agents/customer-vendor-ui.md#contact-rows-3501). 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**: concrete STI base. Abstract **SalesContract** owns `customerId`; **PurchaseContract** owns `vendorId`. `channelId` is open-ended. Before upgrade, run `preflightCommercePartyLinks(db)` and repair linked base rows, stray parties, and wrong-branch links. See [README](README.md).\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; the existing-column migration is documented in [invoice quantity persistence](agents/invoice-quantity.md).\n- **Payment** / **PaymentAllocation**: tracks payments against invoices (status: see Gotchas). 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. `isDefault` is domain-managed, not API-writable: `PaymentInstrumentCollection.setDefaultForCustomer(customerId, instrumentId)` validates the target and clears the customer's other defaults (one per customer).\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: see the natural-key Gotcha.\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\nSee [historical issued sales](agents/historical-sales.md) for exact original source\nmoney, immutable source children and combined historical/live customer funds.\nSee [payment allocation integrity](agents/payment-allocations.md) for atomic splits,\nretry identity, parent corrections and supported dialects.\nSee [tax codes](agents/tax-codes.md) for tenant-owned rate components, default codes,\nand the standard codes every tenant gets.\nSee [document money persistence](agents/document-money.md) for the one money\nmodel: tax-code resolution, atomic aggregate writes, issued snapshots, and\nfrozen fields. Every Invoice and Contract money write needs a tenant. Host\nbootstrap is `ensureCommerceTenant(db, tenantId, { cookbook })`, which returns\n`{ tenantId }`. Money data written by an earlier version is refused with\n`COMMERCE_DATA_NEEDS_UPGRADE` until `smrt db:upgrade-commerce-money` converts\nit; see [money data upgrade](agents/money-upgrade.md).\nSee [Contract money persistence](agents/contract-money.md) for the Contract\naggregate and purchase recoverability.\n\n## Ledger Integration\n\n`@happyvertical/smrt-ledgers` is a regular dependency, loaded lazily via dynamic `import()` (never a static import) so the package graph stays a DAG (#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- **Billing invoices are money-model invoices.** The provider taxes them\n (`providerTaxAmount`, server-managed, not API-writable, added to line tax),\n so every billing line is an explicit `taxMode: 'override'` to the seller's\n `out-of-scope` code. Charges are written before credits so the running\n subtotal never goes negative, and provider tax is written in the save that\n issues the invoice. Revenue posting is idempotent. `ledger.bookCurrency` is\n required (ISO 4217, validated by `BillingRuntime`) and must equal the\n billing currency. `toAccountingInput({ mapping })` 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- `vendorId` → `@foreignKey('Vendor')` on PurchaseContract only\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- **Money units and arithmetic**: money fields use signed integer minor units; rates and quantities remain decimal where their contracts require them. Exact rounding, tax-code resolution, safe-integer validation, and journal units are defined in the [commercial money model](agents/money-model.md) and [money calculator](agents/money-calculator.md). Do not reintroduce floating-point tolerances on money paths.\n- **Money documents need a tenant**: models are optionally tenant-scoped, but every Invoice and Contract money write requires one (`MoneyValidationError` `INVALID_MONEY_INPUT`, `Commerce money documents require a tenant`).\n- **Earlier-version money data is refused**: never add a read or conversion path for it. Detection and the one-shot upgrade are in [money data upgrade](agents/money-upgrade.md).\n- **Book currency is required**: `recognizeRevenue()` and `cancelWithRevenueReversal()` take a required `bookCurrency`, which must equal the invoice currency.\n- **Asset-native amounts stay outside line rescales**: `Payment.nativeAmount` and `PaymentIntent.paymentOptions[].nativeAmount` are per-asset values identified by `nativeCurrency`; normalize each asset to its own minor unit before any type conversion, and never apply the commerce ×100 line-money rescale to them.\n- **Invoice controls payment status**: not the Payment model — use `Invoice.updatePaymentStatus()`\n- **Tax comes from tax codes**: the server resolves each line's code and computes line tax and header totals; client totals and a line's stored `taxRate` are never inputs\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 `PaymentIntentCollection.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- **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` / `ui.format` 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\n## Exact cost billing\n\nPure signed cost billing and explicit tax rounding are available through the\nindependent `cost-billing` subpath; see [contract](agents/cost-billing.md).\n\nSee [contract billing schedules](agents/contract-billing-schedules.md) for exact\napproved milestone capacity, immutable amendments and atomic canonical invoice issue.\n\n## Customer funds (#4079)\n\nSee [customer advances](agents/customer-funds.md) for explicitly configured\nunapplied-AR/deposit-liability receipts, conserved application/refund capacity,\nreal liability reclassification, final-invoice attestation and mutation guards.\nUse the public commands; `Payment.customerFundsJson` is command-owned.\n\nSee [observed purchase prices](agents/observed-purchase-prices.md) for incomplete\noriginal supplier prices, explicit unknown totals and reviewed completion.\nSee [received sales agreement money](agents/received-sales-contract-money.md) for\nexact complete source Order/Agreement evidence without synthetic lines or tax recognition.\n\nSee [ordinary invoice accounting adjustments](agents/ordinary-accounting.md) for\nnon-retained provider credit applications, recorded refunds and durable recovery.\n",
|
|
22023
|
+
"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). `contacts` (JSON) is written via [`savePartyContacts()`](agents/customer-vendor-ui.md#contact-rows-3501). 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**: concrete STI base. Abstract **SalesContract** owns `customerId`; **PurchaseContract** owns `vendorId`. `channelId` is open-ended. Before upgrade, run `preflightCommercePartyLinks(db)` and repair linked base rows, stray parties, and wrong-branch links. See [README](README.md).\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; the existing-column migration is documented in [invoice quantity persistence](agents/invoice-quantity.md).\n- **Payment** / **PaymentAllocation**: tracks payments against invoices (status: see Gotchas). 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. `isDefault` is domain-managed, not API-writable: `PaymentInstrumentCollection.setDefaultForCustomer(customerId, instrumentId)` validates the target and clears the customer's other defaults (one per customer).\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: see the natural-key Gotcha.\n- **TenantPaymentAccount**: tenant-owned Stripe Connect account; direct charges with an application fee, account→tenant webhook routing, refunds, no platform fallback. Write only through `connected-payments.ts`; see [connected payments](agents/connected-payments.md).\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\nSee [historical issued sales](agents/historical-sales.md) for exact original source\nmoney, immutable source children and combined historical/live customer funds.\nSee [payment allocation integrity](agents/payment-allocations.md) for atomic splits,\nretry identity, parent corrections and supported dialects.\nSee [tax codes](agents/tax-codes.md) for tenant-owned rate components, default codes,\nand the standard codes every tenant gets.\nSee [document money persistence](agents/document-money.md) for the one money\nmodel: tax-code resolution, atomic aggregate writes, issued snapshots, and\nfrozen fields. Every Invoice and Contract money write needs a tenant. Host\nbootstrap is `ensureCommerceTenant(db, tenantId, { cookbook })`, which returns\n`{ tenantId }`. Money data written by an earlier version is refused with\n`COMMERCE_DATA_NEEDS_UPGRADE` until `smrt db:upgrade-commerce-money` converts\nit; see [money data upgrade](agents/money-upgrade.md).\nSee [Contract money persistence](agents/contract-money.md) for the Contract\naggregate and purchase recoverability.\n\n## Ledger Integration\n\n`@happyvertical/smrt-ledgers` is a regular dependency, loaded lazily via dynamic `import()` (never a static import) so the package graph stays a DAG (#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- **Billing invoices are money-model invoices.** The provider taxes them\n (`providerTaxAmount`, server-managed, not API-writable, added to line tax),\n so every billing line is an explicit `taxMode: 'override'` to the seller's\n `out-of-scope` code. Charges are written before credits so the running\n subtotal never goes negative, and provider tax is written in the save that\n issues the invoice. Revenue posting is idempotent. `ledger.bookCurrency` is\n required (ISO 4217, validated by `BillingRuntime`) and must equal the\n billing currency. `toAccountingInput({ mapping })` 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- `vendorId` → `@foreignKey('Vendor')` on PurchaseContract only\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- **Money units and arithmetic**: money fields use signed integer minor units; rates and quantities remain decimal where their contracts require them. Exact rounding, tax-code resolution, safe-integer validation, and journal units are defined in the [commercial money model](agents/money-model.md) and [money calculator](agents/money-calculator.md). Do not reintroduce floating-point tolerances on money paths.\n- **Money documents need a tenant**: models are optionally tenant-scoped, but every Invoice and Contract money write requires one (`MoneyValidationError` `INVALID_MONEY_INPUT`, `Commerce money documents require a tenant`).\n- **Earlier-version money data is refused**: never add a read or conversion path for it. Detection and the one-shot upgrade are in [money data upgrade](agents/money-upgrade.md).\n- **Book currency is required**: `recognizeRevenue()` and `cancelWithRevenueReversal()` take a required `bookCurrency`, which must equal the invoice currency.\n- **Asset-native amounts stay outside line rescales**: `Payment.nativeAmount` and `PaymentIntent.paymentOptions[].nativeAmount` are per-asset values identified by `nativeCurrency`; normalize each asset to its own minor unit before any type conversion, and never apply the commerce ×100 line-money rescale to them.\n- **Invoice controls payment status**: not the Payment model — use `Invoice.updatePaymentStatus()`\n- **Tax comes from tax codes**: the server resolves each line's code and computes line tax and header totals; client totals and a line's stored `taxRate` are never inputs\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 `PaymentIntentCollection.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- **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` / `ui.format` 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\n## Exact cost billing\n\nPure signed cost billing and explicit tax rounding are available through the\nindependent `cost-billing` subpath; see [contract](agents/cost-billing.md).\n\nSee [contract billing schedules](agents/contract-billing-schedules.md) for exact\napproved milestone capacity, immutable amendments and atomic canonical invoice issue.\n\n## Customer funds (#4079)\n\nSee [customer advances](agents/customer-funds.md) for explicitly configured\nunapplied-AR/deposit-liability receipts, conserved application/refund capacity,\nreal liability reclassification, final-invoice attestation and mutation guards.\nUse the public commands; `Payment.customerFundsJson` is command-owned.\n\nSee [observed purchase prices](agents/observed-purchase-prices.md) for incomplete\noriginal supplier prices, explicit unknown totals and reviewed completion.\nSee [received sales agreement money](agents/received-sales-contract-money.md) for\nexact complete source Order/Agreement evidence without synthetic lines or tax recognition.\n\nSee [ordinary invoice accounting adjustments](agents/ordinary-accounting.md) for\nnon-retained provider credit applications, recorded refunds and durable recovery.\n",
|
|
21449
22024
|
"moduleDocs": [
|
|
21450
22025
|
{
|
|
21451
22026
|
"path": "agents/customer-vendor-ui.md",
|
|
@@ -21455,13 +22030,18 @@
|
|
|
21455
22030
|
{
|
|
21456
22031
|
"path": "README.md",
|
|
21457
22032
|
"module": "README",
|
|
21458
|
-
"content": "# @happyvertical/smrt-commerce\n\nCommerce models for the s-m-r-t framework. Covers customers, vendors, contracts (10 STI types), invoices with ledger integration, payments, and fulfillment tracking.\n\n## Installation\n\n```bash\npnpm add @happyvertical/smrt-commerce\n```\n\n## Usage\n\n```typescript\nimport { withTenant } from '@happyvertical/smrt-tenancy';\nimport {\n CustomerCollection,\n ContractCollection,\n InvoiceCollection,\n InvoiceLineItemCollection,\n PaymentCollection,\n PaymentMethod,\n ensureCommerceTenant,\n} from '@happyvertical/smrt-commerce';\n\n// Once per tenant, after the host creates it: tax settings and standard codes.\nawait ensureCommerceTenant(db, tenantId);\n\n// Money documents (invoices, contracts, and their lines) belong to a tenant.\nawait withTenant({ tenantId }, async () => {\n // Create a customer linked to a profile\n const customers = await CustomerCollection.create({ db });\n const customer = await customers.create({\n profileId: 'profile-uuid',\n creditLimit: 1000000, // $10,000.00 — money is integer minor units\n paymentTerms: 'Net 30',\n });\n\n // Create an order (STI contract type)\n const contracts = await ContractCollection.create({ db });\n const order = await contracts.create({\n _meta_type: 'Order',\n customerId: customer.id,\n currency: 'CAD',\n });\n\n // Create an invoice. The server resolves each line's tax code and computes\n // line tax and the header totals; header amounts are never inputs.\n const invoices = await InvoiceCollection.create({ db });\n const invoice = await invoices.create({\n customerId: customer.id,\n contractId: order.id,\n invoiceNumber: await invoices.generateInvoiceNumber(),\n currency: 'CAD',\n });\n const lines = await InvoiceLineItemCollection.create({ db });\n await lines.create({\n invoiceId: invoice.id,\n description: 'Consulting',\n quantity: 10,\n unitPrice: 10000, // $100.00\n });\n\n // Issuing freezes the money snapshot; then recognize revenue (creates a\n // balanced journal in smrt-ledgers from the frozen facts).\n const issued = await invoices.get({ id: invoice.id }, { cache: false });\n issued.markSent();\n await issued.save();\n await issued.recognizeRevenue({\n arAccountId: 'ar-account-id',\n revenueAccountId: 'revenue-account-id',\n taxAccountId: 'tax-account-id',\n bookCurrency: 'CAD', // must equal the invoice currency\n });\n\n // Record a payment with ledger integration\n const payments = await PaymentCollection.create({ db });\n const payment = await payments.create({\n contractId: order.id,\n customerId: customer.id,\n amount: issued.totalAmount,\n method: PaymentMethod.CREDIT_CARD,\n });\n await payment.recordPayment({\n ledgerId: 'ledger-id',\n receivablesAccountId: 'ar-account-id',\n cashAccountId: 'bank-account-id',\n });\n});\n```\n\n## Commercial money calculator\n\nThe public `/money` entry is a pure calculator for line amounts, discounts,\ncomponent tax, document rounding, and totals. It consumes and returns integer\nminor-unit money values; tax and discount percentages are exact text (`'5'`\nmeans 5%). It does not persist or load documents.\n\n```ts\nimport { calculateMoneyDocument } from '@happyvertical/smrt-commerce/money';\n\nconst totals = calculateMoneyDocument({\n currency: 'CAD',\n lineAmountType: 'exclusive',\n roundingScope: 'line',\n lines: [{\n id: 'consulting', quantity: 1, unitPrice: 10000,\n discount: { kind: 'none' },\n taxCode: {\n id: 'gst', kind: 'taxable',\n components: [{ taxComponentId: 'gst', percent: '5', compound: false }],\n },\n }],\n});\n```\n\n`resolveLineTaxCode()` resolves caller-loaded codes without I/O, and the\nSvelte `InvoiceEditor` renders the money controls from its required `money`\nprop. Frozen editor documents render a supplied stored snapshot without\nrecalculation. See the [commerce money calculator guide](../../docs/content/commerce-money.md)\nfor pricing modes, rounding, input and result types, the native-form editor\ncontract, and the boundaries this package does not define.\n\n### Tenant tax catalogue and defaults\n\nThe package root also exports tenant-scoped `TaxComponent`, `TaxCode`, and\n`TaxSettings` models and their collections. Components hold exact percentage\nrates and optional liability accounts; codes combine compatible components; the\none settings row per tenant supplies sales/purchase defaults, amount mode,\nrounding scope, and labelled registration numbers. A referenced component rate\nis immutable: archive it and create a replacement for a rate change.\n\n`ensureTenantTaxCodes(db, tenantId)` idempotently creates the standard\nnon-taxable codes, `exempt`, `zero-rated`, and `out-of-scope`\n(`STANDARD_TAX_CODES`). `applyTaxCodeExamplePack(db, tenantId)` installs illustrative\nregional examples for review, and `applyCommerceCookbookSettings(db, tenantId,\ncookbook)` explicitly materializes a validated cookbook's defaults after schema\nsetup and authentication establish the tenant. See the [commerce money\ncalculator guide](../../docs/content/commerce-money.md#tenant-tax-catalogue) for\nthe supported kinds, seed behavior, and cookbook rate units.\n\n### Persisted Invoice money\n\nCommerce has one money model. Tax codes resolve each line's tax: the line's\noverride, else `exempt` for an exempt customer, else the product's code, the\ncustomer's code, then the tenant default. The server computes line\n`taxAmount` and the header totals and ignores any client total. Every Invoice\nand Contract money write needs a tenant; without one it fails with\n`MoneyValidationError` `INVALID_MONEY_INPUT`.\n`ensureCommerceTenant(db, tenantId, { cookbook })` is the one bootstrap call a\nhost makes after creating a tenant. It seeds the tenant's tax settings, the\nstandard codes, and the cookbook's defaults, and returns `{ tenantId }`.\n\nIssuing an Invoice (leaving `DRAFT`) or assigning `arJournalId` freezes its\nmoney snapshot. Header and line money changes then fail with `FROZEN_DOCUMENT`,\nwhile later lifecycle and payment updates retain the snapshot. Provider tax\nstays separate in `providerTaxAmount` and contributes to the Invoice headers\nwithout creating an application tax code.\n`InvoiceLineItemCollection.getTotalForInvoice()` sums line amounts for\ncompatibility; use persisted `Invoice.totalAmount` for the amount due.\n`recognizeRevenue()` and `cancelWithRevenueReversal()` require a `bookCurrency`\nequal to the invoice currency, and `toAccountingInput({ mapping })` requires\nexplicit QuickBooks realm mapping. The\n[commerce money calculator guide](../../docs/content/commerce-money.md#persisted-invoice-money)\nand [document-money contract](agents/document-money.md) cover persisted Invoice\nsemantics, credits, refunds, and posting. The materialized Invoice tax-report\ndescriptor and tenant-safe drilldown are documented in\n[tax-summary contract](agents/tax-summary.md). Contract money persistence,\nPurchaseContract tax contributors, and the materialized tax-summary host and\ndrilldown are available. Applications opt in by registering\n`createTaxSummaryMaterializedReportHost()` in their server-only report-host\nregistry; the route binds the authenticated tenant and read permission. See the\ntax-summary contract for registration details.\n\n### Upgrading money data from an earlier version\n\nCommerce refuses money data written by an earlier version with\n`COMMERCE_DATA_NEEDS_UPGRADE` instead of reading it. That is a schema that\nstill has the earlier money columns or the `commerce_money_migration_documents` table,\nan invoice or contract without a tenant, or a tenant with invoices or contracts but no tax settings row or with\ndocuments in the earlier format. `smrt db:status`\nreports it, so `smrt app doctor` reports `migration-required`. Run the normal\nschema migration, then upgrade once:\n\n```sh\nsmrt db:upgrade-commerce-money --check # report only\nsmrt db:upgrade-commerce-money # every tenant\n```\n\nEach tenant converts in its own transaction. Stored rates become tax codes,\nissued documents keep every stored total, and drafts are recomputed. The\nearlier columns are dropped once no tenant needs the upgrade. See\n[money data upgrade](agents/money-upgrade.md) and the\n[CLI guide](../cli/agents/commerce-money-upgrade.md).\n\n## API\n\n### Models\n\n#### Supplier quotations\n\n`SupplierQuote` extends `PurchaseContract`, requires `vendorId`, and has\n`ContractType.SUPPLIER_QUOTE`. Create it through `PurchaseContractCollection`\n(or `ContractCollection`) with `_meta_type:\n'@happyvertical/smrt-commerce:SupplierQuote'`. It inherits `expiryDate`,\n`reference`, `notes`, and `terms`; it has no customer relationship. Purchase\nbranch reads include quotes and sales branch reads exclude them.\n\nKeep received quote revisions and shared quote identity in your application.\nAwarding a quote creates a separate `PurchaseOrder`; accepting or saving a\nquote never converts its identity into an order. Quotes use purchase-side\nmoney calculation and frozen snapshots, but never contribute incurred purchase\ntax to reports, regardless of status. Existing Estimate and PurchaseOrder\nreport eligibility is unchanged.\n\n#### Upgrading contract parties (#3898)\n\n`Contract` no longer declares `customerId` or `vendorId`. Narrow a contract to\n`SalesContract` to read `customerId`, or to `PurchaseContract` to read `vendorId`.\nThe existing leaf qualified names and the `contracts` table stay the same; a\n`db:migrate --dry-run` against a v0.55.19 schema reports no DDL changes on\nSQLite or PostgreSQL. Sales leaves no longer accept `vendorId`; purchase leaves\nno longer accept `customerId`. `Order`, `Estimate`, `Agreement`, `Lease`, and\n`WholesaleOrder` require a customer on save. `PurchaseOrder` and `SupplierQuote`\nrequire a vendor.\n`Cart`, `LicenseSale`, and `ProductionOrder` retain optional parties on their side.\n\nBefore upgrading existing data, run the read-only\n`preflightCommercePartyLinks(db)` and inspect its stray party links, missing\nrequired parties, base-typed rows, discriminator mismatches, and invalid\ndocument references. Its `unqualifiedDiscriminatorRows` lists legacy simple\n`_meta_type` values such as `Order` and `PurchaseOrder`; retype them to the\nqualified class name before upgrading, since branch collections cannot read\nthem. The report checks invoice, payment, fulfillment, ad group,\nand expense links when those tables exist. A linked base-typed or wrong-branch\ncontract blocks the next save of its document even when the link is unchanged.\nRegistered custom descendants of either contract branch are accepted in link\nchecks; load those model classes before running the preflight.\nTenant-owned documents linked to global or other-tenant contracts are also\nreported because the save-time lookup is tenant-scoped. Retype the contract to\nthe correct leaf, assign it to the document's tenant, or repair the link before\ndeployment. The deprecated `ContractCollection` party finders now return the\nappropriate branch\nonly, so retype legacy base rows with party links before relying on those\nfinders. Clear stray party columns and fill required parties before deploying\nwriters that save affected rows. Missing required parties remain readable but\nfail their next save. Regenerate cached manifests, planner catalogs, and\ngenerated screens after upgrading.\nThis preflight never edits rows; an operator chooses the data correction.\n\n| Export | Description |\n|--------|------------|\n| `Customer` | Customer record with creditLimit, paymentTerms, tax exemption, addresses, contacts |\n| `Vendor` | Vendor/supplier with leadTimeDays, minimumOrderAmount, currency, contacts |\n| `Contract` | STI base class for all commercial agreements |\n| `SalesContract` | Abstract customer-side Contract branch; owns `customerId` |\n| `PurchaseContract` | Abstract supplier-side Contract branch; owns `vendorId` |\n| `Estimate` | Quote or proposal sent to a customer |\n| `Order` | Customer purchase order |\n| `Lease` | Rental or lease agreement |\n| `Agreement` | Service or maintenance agreement |\n| `SupplierQuote` | Quotation received from a vendor; distinct from an awarded purchase order |\n| `PurchaseOrder` | Order sent to a vendor/supplier |\n| `ContractLineItem` | Line item within a contract |\n| `Invoice` | Billing document with status lifecycle and ledger integration |\n| `InvoiceLineItem` | Individual invoice line item |\n| `Payment` | Payment record with method, status, and optional ledger journal |\n| `PaymentAllocation` | Payment-to-invoice allocation |\n| `Fulfillment` | Shipment/delivery tracking with carrier and address |\n| `FulfillmentLineItem` | Individual fulfillment line item |\n| `BillingAccount` | A payer's account with a seller: customer, provider customer, terms, standing |\n| `BillingPeriodClose` | One payer's invoice for one period and currency; also the billing job target |\n| `BillingLineSource` | The claim that a charge or plan period is billed on exactly one close |\n\n### Collections\n\n`CustomerCollection`, `VendorCollection`, `ContractCollection`, `SalesContractCollection`, `PurchaseContractCollection`, `InvoiceCollection`, `InvoiceLineItemCollection`, `PaymentCollection`, `PaymentAllocationCollection`, `FulfillmentCollection`\n\n`PaymentAllocationCollection.allocatePayment()` splits a payment across invoices\nin one transaction using stable allocation IDs for retries. Direct allocation\nsaves also enforce matching owners, customers and currencies and both balance\ncaps. Read [payment allocation integrity](agents/payment-allocations.md) for\nreplay, parent revision refresh and compatibility requirements.\n\n\n### Enums\n\n| Export | Values |\n|--------|--------|\n| `ContractType` | `estimate`, `order`, `lease`, `agreement`, `supplier_quote`, `purchase_order`, `wholesale_order`, `production_order`, `cart`, `license_sale` |\n| `ContractStatus` | `draft`, `sent`, `accepted`, `declined`, `completed`, `cancelled` |\n| `CustomerStatus` | `active`, `inactive`, `suspended` |\n| `VendorStatus` | `active`, `inactive`, `suspended` |\n| `InvoiceStatus` | `draft`, `sent`, `viewed`, `partial`, `paid`, `overdue`, `cancelled`, `written_off` |\n| `PaymentMethod` | `cash`, `check`, `credit_card`, `bank_transfer`, `crypto`, `other` |\n| `PaymentStatus` | `pending`, `completed`, `failed`, `refunded`, `cancelled` |\n| `FulfillmentStatus` | `pending`, `processing`, `shipped`, `delivered`, `cancelled` |\n| `FulfillmentType` | `shipment`, `delivery`, `pickup`, `digital`, `service` |\n\n### Types and Constants\n\n| Export | Description |\n|--------|------------|\n| `Address` | Shared address interface (street1, street2, city, state, postalCode, country) |\n| `RecognizeRevenueOptions` | Account IDs for invoice revenue recognition |\n| `RecordPaymentOptions` | Ledger and account IDs for payment recording |\n| `InvoiceNumberOptions` | Options for `generateInvoiceNumber()` (prefix, format) |\n| `UNPAID_STATUSES` | Array of invoice statuses considered unpaid (sent, viewed, partial, overdue) |\n| `COMMERCE_MODULE_META` | UI module metadata |\n| `COMMERCE_UI_SLOTS` | UI slot definitions |\n\n### Ledger Integration\n\nInvoice and Payment integrate with `@happyvertical/smrt-ledgers` via dynamic import (optional dependency). Invoice stores `arJournalId` and `revenueJournalId` as plain string references to ledger journals. `recognizeRevenue()` creates a balanced AR entry (DR: Accounts Receivable, CR: Revenue, CR: Tax Payable). `recordPayment()` creates a balanced cash receipt entry (DR: Cash, CR: Accounts Receivable). Both methods return null or throw if the ledgers package is not installed.\n\n`Payment.recordPayment()` claims the payment revision and commits the journal,\nentries, and completed payment in one database transaction. A failed completion\nrolls back the posting and restores the instance's settlement fields for retry.\nThe claimed persisted status is authoritative even if a caller locally resets\nstatus or marks the instance failed without saving. Concurrent or stale instances\nfail instead of posting twice; reload the payment\nand retry only if it is still pending. An already completed payment retains the\n`Payment already recorded` error. A caller's enclosing transaction remains the\nowner: if it rolls back after settlement returns, reload the payment before retry.\nThese guarantees cover new postings, not orphaned journals written by older\nversions. The returned journal uses the caller's database executor.\nBoth payment and refund journal lines retain the payment's explicit currency and\nexact amount; posting does not convert currencies or fall back to USD. When\nreporting accounts that contain different currencies, use the public\n`listPostedMovements` currency filter rather than combining their amounts.\n\n### Cross-Package References\n\nCustomer and Vendor link to `@happyvertical/smrt-profiles` via plain `profileId` string. Invoice and Payment reference `@happyvertical/smrt-ledgers` journals via plain string IDs (`arJournalId`, `revenueJournalId`, `journalId`). All models use `@TenantScoped({ mode: 'optional' })` with nullable `tenantId`.\n\n## Billing-period close\n\n`BillingRuntime` turns approved `@happyvertical/smrt-subscriptions` charges into\ninvoices for each billing owner, pushes them to a payment provider with\nprovider-calculated tax, posts revenue to the ledger, and applies the\nprovider's webhook events. Every provider call goes through\n`@happyvertical/accounting`; `createStripeBillingProvider()` adapts its Stripe\nprovider to the `BillingProvider` port. Money crossing the port is integer\nminor units.\n\n```ts\nimport { getAccountingProvider } from '@happyvertical/accounting';\nimport { SpendingPolicyEvaluator } from '@happyvertical/smrt-subscriptions';\nimport {\n BillingRuntime,\n createStripeBillingProvider,\n enqueueBillingPeriodClose,\n registerBillingRuntime,\n} from '@happyvertical/smrt-commerce';\n\nconst stripe = await getAccountingProvider({ type: 'stripe', secretKey });\nconst billing = await BillingRuntime.create({\n db,\n sellerTenantId: platformTenantId,\n kind: 'provider', // or 'reseller' to bill a reseller's children\n provider: createStripeBillingProvider({ stripe, webhookSecret }),\n billingRelationships, // smrt-tenancy BillingRelationshipService\n ledger: {\n bookCurrency: 'USD', // ISO 4217; must equal the billing currency\n arAccountId,\n revenueAccountId,\n taxAccountId,\n cashAccountId,\n prepaidCreditAccountId,\n },\n onPayerStanding: async ({ payerTenantId, standing, db }) => {\n // suspend or reinstate the payer; runs in the event transaction\n },\n});\n\n// One account per payer: its customer, tax location, and terms.\nawait billing.upsertAccount({\n payerTenantId,\n name: 'Network Co',\n email: 'billing@example.test',\n billingAddress: { country: 'CA', postalCode: 'T0L 0A0' },\n});\n\n// Optional: bill this payer from its activation date instead of calendar\n// months, prorating flat plans added mid-period (see \"Billing cycles\").\nawait billing.upsertAccount({\n payerTenantId,\n name: 'Network Co',\n billingAnchorAt: activatedAt,\n prorateFlatPlans: true,\n});\n\n// Close each payer's last ended period — safe to run on every schedule tick.\nregisterBillingRuntime('platform', billing);\nawait enqueueBillingPeriodClose({ runtime: 'platform' });\n\n// Webhook route: verify, enqueue, respond 2xx.\nconst intake = await billing.acceptWebhook(\n rawBody,\n request.headers.get('stripe-signature'),\n);\nif (intake.type?.endsWith(':unverified_credit_purchase')) {\n // alert: a paid checkout claimed to buy credit but failed verification\n}\nawait billing.processEvents(); // or enqueueBillingEvents({ runtime: 'platform' })\n\n// Prepaid credit through provider checkout: tax is added on top when the\n// payer's account is taxed; `savePaymentMethod` keeps the card for top-ups.\nconst { url } = await billing.createCreditCheckout({\n spendingPolicyId,\n amount: 5000,\n purchaseId: cartId,\n savePaymentMethod: true,\n successUrl,\n cancelUrl,\n});\n\n// Card on file at signup: a setup checkout that charges nothing. Its\n// completion event makes the card the payer's default and adopts the billing\n// address collected at checkout as the tax location.\nconst setup = await billing.createCardSetupCheckout({\n payerTenantId,\n currency: 'CAD',\n setupId: signupId,\n successUrl,\n cancelUrl,\n});\n\n// Automatic top-ups: charge the saved card when a balance would run out.\n// Accounts are taxed by default (`automaticTax`); a taxed account is charged\n// the credit plus provider-calculated tax (#3194).\nconst evaluator = await SpendingPolicyEvaluator.create({\n db,\n autoTopUp: billing.autoTopUpHook({ amount: () => 2500 }),\n});\n```\n\n- **What is billed.** A `provider` runtime bills `ClientCharge`s (and their\n `BillingAdjustment`s) to the payer each charge names, plus monthly flat plans\n it owns, to each subscriber's billing owner. A `reseller` runtime bills the\n `RetailCharge`s its children owe it plus its own flat plans. Charges approved\n before the period end are billed once, whatever period that is; flat plans\n are billed in arrears for the payer's closed period (trials and\n provider-managed subscriptions are skipped). One invoice per payer and\n currency carries both kinds of lines.\n- **Replay safety.** Close, invoice, and line ids are derived from the payer,\n currency, and period; each charge is claimed by exactly one close; the\n provider invoice uses the close id as its idempotency key; and every step is\n persisted before the next, so a retry resumes where it stopped. A lease stops\n two workers advancing the same close. A close without a period also resumes\n every unfinished close whose period has ended, even one the payer's current\n schedule no longer produces (#3116). A payer whose charges net to a credit\n gets no invoice; the credit is carried to its next invoice.\n- **Billing cycles (#3116).** A payer's periods follow its account's\n `billingAnchorAt`. Unset (the default) they are UTC calendar months. Set,\n they run monthly from the anchor at its UTC time of day; a day the month\n lacks is clamped to its last day, always from the anchor itself (an anchor\n on Jan 31 bills Jan 31 → Feb 28 → Mar 31). Before the anchor the schedule is\n calendar months, and the anchor's own month ends at the anchor (a *stub*).\n `closePeriod()` / `enqueueBillingPeriodClose()` with no period close each\n payer's **last ended period on its own schedule**, so payers on different\n anchors are closed by the same daily tick. An explicit\n `{ periodStart, periodEnd }` closes that period for calendar payers (flat\n plans only when it is a calendar month, as before) and for anchored payers\n whose schedule has exactly that period. `billingPeriodFor(payer, at)` returns\n a payer's current period; `billingPeriodContaining()`,\n `lastEndedBillingPeriod()`, and `prorateMinorUnits()` are exported.\n- **Proration.** A flat plan is priced `price × billed time / cycle length`,\n in integer minor units rounded half up (exact integer arithmetic; the\n cycle is the period's own month, so February prorates over 28 or 29 days).\n With `prorateFlatPlans` off (default) the billed time is the whole period\n whenever the subscription was active in it and out of trial at its start —\n the pre-#3116 behavior. With it on, the billed time is exactly the time the\n subscription was active and out of trial: a subscription starting, leaving\n trial, or canceled mid-period is billed pro rata. Proration happens before\n the account's flat discount. Split into parts, prorated amounts can differ\n from the full price by a minor unit.\n- **First period.** *Anchor at signup* (set `billingAnchorAt` to the\n signup/activation instant): the first period is a full period from signup;\n nothing before it is billed. *Bill from signup to the next anchor* (keep a\n fixed anchor, or calendar months, and set `prorateFlatPlans`): the first\n period is billed pro rata from signup. Items added later are prorated to the\n payer's anchor when `prorateFlatPlans` is on and billed a full period\n otherwise.\n- **Mid-period changes.** Billing is in arrears, so a cancellation needs no\n credit: with proration the canceled period is billed up to `canceledAt`,\n without it in full. A plan change is billed at the plan in effect at close\n for the whole window (no plan-change proration yet, #3119), and a cancellation\n back-dated into an already billed period is not credited — issue an\n adjustment.\n- **No double billing across schedule changes.** Every flat-plan claim records\n the exact time it bills, and a subscription's claims are numbered, so only\n one worker can write the next claim and two can never claim the same time.\n Moving an account from calendar months to an anchor, moving an anchor, or\n moving a subscription to a payer on another schedule bills only time not yet\n billed, prorated. For example, calendar months through February and then an\n anchor on Mar 20 bills the Mar 1 → Mar 20 stub pro rata, then Mar 20 → Apr\n 20. Time around an already billed window is billed as separate lines, each\n with its own service period. Stop workers running a pre-#3116 version\n before relying on this: they claim flat plans outside the numbering.\n- **Service periods.** Each flat-plan invoice line carries its billed window\n as `periodStart`/`periodEnd`, down to the provider port and on to Stripe's\n invoice item `period`.\n- **Upgrading to #3116.** Additive schema only: `_smrt_billing_accounts` gains\n `billing_anchor_at` (nullable) and `prorate_flat_plans` (default false), and\n `_smrt_billing_line_sources` gains `chain_sequence` (default 0) and an index\n on `(line_key, chain_sequence)`; run `smrt db:migrate`.\n Existing accounts stay on calendar months, and claims written before the\n upgrade (`<subscriptionId>:<periodStart>`, number 0) count as billed\n coverage, so nothing is billed again after it.\n- **Tax** comes from the provider (Stripe Tax) using the account customer's\n `defaultBillingAddress`; the invoice records it as `providerTaxAmount` in the\n save that issues it. Billing invoices are ordinary money-model invoices in\n the seller's tenant; period close seeds its standard codes if needed. Every\n billing line is an explicit override to the seller's `out-of-scope` code, so\n the seller's default code never taxes it a second time. Charges are written\n before credits, so the running subtotal never goes negative. Revenue posting\n uses `ledger.bookCurrency` and is idempotent.\n- **Events** are verified, stored in smrt-jobs' durable delivery inbox, and\n applied against current provider state: paid invoices record a payment and\n allocation; failures and overdue notices mark the payer `past_due` and its\n flat-plan subscriptions `past_due`; an invoice written off at the provider\n (`markInvoiceUncollectible(invoiceId)`, or in Stripe) is written off\n locally and marks the payer `uncollectible` (no bad-debt journal is\n posted; AR stays until it is paid or an operator journals it); payment\n reinstates them, including a late payment of the written-off invoice. Dunning is the provider's.\n Subscribe the webhook endpoint to `invoice.*`, `customer.subscription.*`,\n `checkout.session.completed`, `checkout.session.async_payment_succeeded`,\n and `payment_intent.*`.\n- **Prepaid credit** purchases credit a `period: 'balance'` spending policy\n once per checkout session, paid by the policy's tenant or, for a delegated\n balance, the parent that set it. Amounts are ISO 4217 minor units in any\n currency the provider supports. When the payer's account is taxed\n (`automaticTax`, not tax-exempt; override per call), provider tax is\n charged on top and collected at checkout with the buyer's address: the\n credit is the amount bought, the payment is the total collected, and the\n tax is booked to the tax account. For a payer with a provider customer but\n no local tax location, the address collected at checkout becomes its tax\n location. `savePaymentMethod` requires a provider that can re-read the\n checkout and set a default payment method, and is refused otherwise.\n- **Card on file (#3139).** `createCardSetupCheckout()` (or a credit purchase\n with `savePaymentMethod`) saves the payer's card without charging it. On\n completion the card becomes the provider customer's default and the\n payer's default `PaymentInstrument`. When the checkout collected the billing\n address (a taxed payer with no tax location), it becomes the payer's tax\n location. `cardOnFile(payerTenantId)` returns `{ onFile, savedAt? }` from\n the same check automatic charges use (a default card with the account's\n provider customer), for \"card on file\" displays and signup activation;\n the payer or a system context may call it (#3187).\n- **Automatically charged invoices.** With `autoChargeInvoices: true` on the\n runtime, a payer with a default card is invoiced `charge_automatically`:\n the provider charges the card after the invoice is sent, on its own\n schedule, and retries failures. Each pushed invoice records the method in\n `Invoice.collectionMethod` (`charge_automatically` or `send_invoice`; empty\n for invoices pushed before #3190). Do not offer another payment rail on a\n `charge_automatically` invoice the provider is still collecting.\n Upgrading adds the `collection_method` column to `invoices` (run\n `smrt db:migrate`). Activate service on the `current` standing\n from the `paid` event, not on send; a failure marks the payer `past_due`.\n- **Automatic top-ups.** Taxed accounts (the default: `automaticTax` on, not\n tax-exempt) are charged the credit plus provider-calculated tax (#3194):\n the balance is credited the credit, the payment records the credit, and\n the tax is booked to the tax account. `taxedAccounts: 'skip'` does not top\n them up, and `'charge_untaxed'` charges them without tax. A taxed payer\n without a tax location is not charged. A location Stripe rejects fails the\n attempt with `failureCode: 'customer_tax_location_invalid'`\n (`onAutoTopUpFailed`), and it is retried only after `retryAfterMs`. Pass\n `onAutoTopUpSkipped` to learn when a needed top-up will not be charged\n because of the payer's account. Its `reason` is `taxed_account`, `no_card`,\n `no_account`, or `tax_location_invalid`, and more may be added. Use it, for\n example, to ask the payer to top up by hand or to add a billing address; it is not called while a charge is processing, while a\n declined card waits out `retryAfterMs`, or when the top-up is no longer\n needed or `amount` declines it (#3189).\n `billing.autoTopUpHook()` is the `autoTopUp` hook for\n `SpendingPolicyEvaluator`: when a balance would run out it charges the\n payer's saved card off-session (the delegating parent's, for a delegated\n balance) and credits the balance only when the charge succeeds. A charge\n still processing is credited from its `payment_intent.succeeded` event; a\n decline or an authentication requirement credits nothing, closes the\n attempt, and calls the runtime's `onAutoTopUpFailed` (ask the payer to\n re-save their card with a setup checkout). One attempt runs per policy at a\n time, retries of a declined card wait `retryAfterMs` (default one hour),\n and the credit is granted exactly once however the outcome arrives.\n- **Product tax codes (#3194).** `taxCodes: { credit, invoice }` on the\n runtime classifies prepaid credit (checkout purchases and taxed top-ups)\n and billing-period invoice lines for Stripe Tax (`txcd_...`). Without it,\n the Stripe account's default product tax code applies.\n- **Upgrading to #3194.** Requires `@happyvertical/accounting` 0.96 or later.\n No schema change. Taxed accounts are now topped up with tax. To keep the\n previous behaviour, pass `taxedAccounts: 'skip'`.\n- **Upgrading to #3139.** Requires `@happyvertical/accounting` 0.92 or later.\n No schema change. Credit checkouts are now taxed for taxed accounts (pass\n `automaticTax: false` to keep them untaxed), and Stripe customers are\n created idempotently.\n\n### Payment rails: BTC and other crypto (#3138)\n\nA seller runtime can take payments on more than one **rail**. The issuing\n`provider` (Stripe) still issues, taxes, and emails every invoice; extra rails\nin `paymentProviders` let a payer choose, per payment, to pay an invoice or buy\nprepaid credit another way. The crypto rail runs short-lived, fiat-priced\ncheckouts through `@happyvertical/payments`' provider-neutral\n`CryptoCheckoutGateway` — BTCPay Server at launch — so another gateway can\nreplace BTCPay without changing this package or its consumers.\n\n```ts\nimport {\n BillingRuntime,\n createBtcPayBillingProvider,\n createStripeBillingProvider,\n} from '@happyvertical/smrt-commerce';\n\nconst btcpay = createBtcPayBillingProvider({\n baseUrl, apiKey, storeId, // one BTCPay store per seller entity\n webhookSecret, metadataSecret, // from the host's secret store, by name\n speedPolicy: 'LowMediumSpeed', // settle after 2 confirmations (BTCPay offers 0/1/2/6)\n rateSource: 'kraken',\n minimumAmount: { CAD: 500, USD: 500 },\n});\nconst billing = await BillingRuntime.create({\n db, sellerTenantId, kind: 'provider',\n provider: createStripeBillingProvider({ stripe, webhookSecret }),\n paymentProviders: [btcpay],\n paymentPolicy: { latePaymentPolicy: 'review', refundBasis: 'original_native' },\n billingRelationships,\n ledger: { ...ledger, cryptoHoldingsAccountId, fxGainLossAccountId, feesAccountId },\n});\n\n// Prepaid credit or an issued invoice, paid with BTC:\nawait billing.createCreditCheckout({ ...purchase, provider: 'btcpay' });\nawait billing.createInvoicePayment({ invoiceId, provider: 'btcpay', purchaseId, successUrl, cancelUrl });\n\n// Webhook route for the rail (BTCPay signs with its own header):\nawait billing.acceptWebhook(rawBody, '', { provider: 'btcpay', headers: request.headers });\nawait billing.processEvents();\n```\n\n- **Refusals are typed (#3185).** `createInvoicePayment()` and\n `createCreditCheckout()` throw `BillingPaymentRefusedError` for requests the\n payer can correct, with a stable `code`: `not_found`, `not_payable`,\n `payment_in_progress`, `nothing_due`, `invalid_amount`, `below_minimum`\n (under the rail's `minimumAmount`), or `no_account`. Show its message as a\n 4xx; any other error is a server failure, and `TenantIsolationError` still\n means the caller is not the payer. Treat an unknown code like\n `not_payable`.\n- **Settlement is the gateway's.** Credit is granted and invoices are paid\n only when the gateway reports `settled` under its own confirmation policy;\n this package never counts confirmations. BTCPay offers 0, 1, 2 or 6\n confirmations; the default is `LowMediumSpeed` (2 confirmations).\n- **Payment attempts.** Every rail checkout is a `BillingPaymentAttempt`\n recording the locked fiat price, fiat and native amounts received, rate and\n rate source, each payment's txid, rail and fee, a timeline of status changes,\n and what settlement produced. `listPaymentAttempts()` feeds \"payment\n confirming\" displays and operator queues; every filter applies before its\n `limit` and `offset`, so paging through `{ unresolved: true }` lists every\n open exception (flagged, not yet resolved) however old. `refreshPaymentAttempts()` is the polling\n fallback for missed webhooks.\n- **Dunning pauses** for an invoice while a payment for it is confirming\n (0-conf seen); if the payment then expires or is invalidated the payer's\n standing is re-applied.\n- **Invoices paid on a rail** are closed at the issuer with\n `markInvoicePaidOutOfBand` (Stripe `paid_out_of_band`), so Stripe stops\n collecting; Stripe's own `paid` event then records nothing more.\n `createInvoicePayment` refuses when the issuing provider cannot do that.\n A written-off (uncollectible) invoice with an amount due can be paid on a\n rail too, as it can by card at Stripe (#3188): the payment is recorded,\n Stripe's invoice is closed out of band, and the payer returns to `current`;\n the invoice keeps its `WRITTEN_OFF` status (its `amountPaid` shows the\n recovery).\n- **Exceptions never auto-refund.** Underpaid, overpaid, late, manually marked\n (at the gateway), invalidated-after-settlement, and paid-twice attempts are\n flagged for an operator (`resolvePaymentAttempt`). Per-seller\n `paymentPolicy` sets under/overpayment tolerances (basis points), late\n payments (`review` | `accept`), manually marked checkouts, hold vs convert\n (`conversionHandler`), and the refund basis.\n- **Ledger.** Receipts are booked at the locked fiat amount into\n `cryptoHoldingsAccountId` (default `cashAccountId`); money received above\n the price is booked as customer credit (prepaid-credit liability).\n `recordManualRefund()` refunds from that excess first, then from purchased\n credit (or an invoice payment kept as credit because the invoice was paid\n elsewhere) — capped cumulatively, once per reference; an invoice payment\n applied to its invoice is reversed at the issuer, not here.\n `recordCryptoConversion()` posts a conversion with FX gain/loss.\n- **Upgrading.** Additive schema: `_smrt_billing_payment_attempts` (run\n `smrt db:migrate`). A runtime without `paymentProviders` never reads it.\n- **One live payment per invoice.** The issuer's own `paid` event for an\n invoice a rail closed out of band records nothing (the rail records it); an\n out-of-band close made by someone else (wire, cheque) is recorded as an\n `OTHER`-method payment; a real card payment made while a rail payment\n confirms is recorded and the rail money is flagged `invoice_already_paid`.\n An issuer close whose rail payment never settles is flagged\n `out_of_band_without_settlement`: reopen the invoice at the issuer, then\n `resolvePaymentAttempt()` it, after which a later out-of-band close is\n recorded normally.\n- **Entities.** Each legal entity is its own seller runtime with its own\n store, keys, and ledger. Inter-entity resale is ordinary reseller billing\n between two sellers, payable on any rail the selling entity offers.\n\nSee [`AGENTS.md`](./AGENTS.md#billing-period-close-3060) for invariants and\nknown limits.\n\n## Dependencies\n\n- `@happyvertical/smrt-core` -- ORM and code generation\n- `@happyvertical/smrt-tenancy` -- multi-tenant scoping\n- `@happyvertical/smrt-subscriptions` -- charges, plans, and credit balances billed by period close\n- `@happyvertical/smrt-jobs` -- period-close jobs and the provider event inbox\n- `@happyvertical/accounting` -- payment-provider (Stripe) calls\n- `@happyvertical/smrt-types` -- shared type definitions\n- Peer: `@happyvertical/smrt-ledgers`, `@happyvertical/smrt-profiles`, `@happyvertical/smrt-svelte`\n\n## Contributor guide\n\nSee [`AGENTS.md`](./AGENTS.md) for package architecture, invariants, validation,\nand contributor guidance.\n\n## Pricing version UI\n\n`PricingVersionSummary` and `PricingVersionDecision` compose the quote revision\ncontract without adding a pricing ledger. Compare versions using\n`QuoteRevisionComparison`; the application owns every decision and native form\naction. See [pricing version presentation](agents/pricing-versions.md).\n\n## Commercial UI composition\n\nCustomer addresses and vendor/quote currencies use the shared localized code\nselectors while preserving native field names and rejected raw drafts. See\n[commercial UI contracts and backend boundaries](agents/commercial-ui.md) for\nnative forms, recovery, amount units and consumer adoption, plus the focused\n[customer/vendor](agents/customer-vendor-ui.md) and\n[quotation](agents/quote-ui.md) contracts for selector restrictions.\n\n## Customer and vendor contacts\n\n`Customer.contacts` and `Vendor.contacts` hold the people to reach at a party\n(`PartyContact`: `id`, `name`, `label`, `email`, `phone`, `address`) as a JSON\ncolumn; existing databases need `smrt db:migrate` for the new `contacts`\ncolumns. The form's contact rows are written with `savePartyContacts()`:\n\n```typescript\nimport {\n applyPartyContactIntent,\n PartyContactValidationError,\n readPartyContactRows,\n savePartyContacts,\n} from '@happyvertical/smrt-commerce';\n\nconst form = await request.formData();\n// Throws PartyContactPayloadError when the form's contactCount marker is absent\n// (block not rendered, or fieldNames differ), so a save never deletes by accident.\nconst rows = readPartyContactRows(form); // pass the form's fieldNames if overridden\nconst { contacts, handled } = applyPartyContactIntent(rows, String(form.get('intent')));\nif (handled) return { values: { ...values, contacts } }; // add/remove round trip, no save\ntry {\n await savePartyContacts(vendors, vendorId, rows, { expectedTenantId });\n} catch (error) {\n if (error instanceof PartyContactValidationError)\n return fail(400, { values: { ...values, contacts: rows }, errors: { contacts: toMessages(error.errors) } });\n throw error;\n}\n```\n\n`savePartyContacts` also accepts the `FormData` itself (with `fieldNames`) and\nreads it the same way. The rows replace the party's contacts: posted ids are kept, new rows get an\nid, unposted rows are removed, blank rows are dropped. See\n[customer/vendor UI](agents/customer-vendor-ui.md#contact-rows-3501).\n\n## Customer and vendor selectors\n\n`CustomerSelect` and `VendorSelect` from `@happyvertical/smrt-commerce/svelte`\nare searchable, field-sized pickers for a customer or vendor relation field.\nThey are registered in `COMMERCE_UI_SLOTS` with `selects:\n'@happyvertical/smrt-commerce:Customer'` / `':Vendor'`, so generic forms (via\n`findSelectorFor` over the manifest's `uiSelectors`) use them for any\n`@foreignKey` or `@crossPackageRef` field targeting those models.\n\nThey are presentation only. The caller owns the data: `search(query)` returns\nthe existing party DTOs (`CustomerDisplayData` / `VendorDisplayData`, whose\n`profile.name` is the label), and `resolve(id)` returns the record behind the\ncurrent value. Each row shows the profile name with status (and customer type).\nThe committed id, never the name, posts under `name`. Pass `onCreate` to offer a\n\"New customer\" / \"New vendor\" action; it receives the searched text, and a\nreturned record is selected.\n\n```svelte\n<CustomerSelect\n name=\"customerId\"\n bind:value={customerId}\n search={(query) => customers.search(query)}\n resolve={(id) => customers.byId(id)}\n onCreate={(query) => openCustomerDialog(query)}\n/>\n```\n\nThe same models carry `@field({ ui: { widget } })` hints in the manifest:\n`textarea` on `Contract` (and subclass) `terms` and `notes`, `Customer`/`Vendor`\n`notes`, and `Invoice` notes and terms; `currency` (an ISO 4217 code picker) on\n`Contract`, `Vendor`, `Invoice` and `Payment` `currency`; `email`/`phone` on\n`Vendor.defaultContactEmail`/`defaultContactPhone`. See\n[customer and vendor selectors](agents/customer-vendor-ui.md#selectors-3602).\n\n## Invoice preparation UI\n\n`InvoiceEditor` from `@happyvertical/smrt-commerce/svelte` is the invoice form,\nbuilt on the commercial money model, v2, which is the only invoice money model.\nIt requires a `money` prop (`MoneyEditorProps`): a lossless draft with\ndescriptions, fractional quantities, unit prices, percentage or amount\ndiscounts and a tax code per line (inherited or overridden), plus the\nauthorized tax codes. Its previews run the same exact calculator the server\nuses, and the server re-resolves tax codes and recomputes every stored amount.\nAn issued document renders only its frozen stored snapshot.\n`InvoiceAllocationFields` and `InvoiceReview` add caller-owned sources and\napprovals. See [the editor contract](agents/invoice-preparation.md), the\n[line calculation helper](agents/invoice-calculations.md), and the\n[existing-database storage migration](agents/invoice-quantity.md).\n\n## Exact cost billing\n\nThe independent `@happyvertical/smrt-commerce/cost-billing` entry point provides\nexact signed reimbursement/fee calculations, ordered generic markup provenance,\nand explicit line or invoice tax rounding. Its opt-in ordered tax calculator\nsupports subset compounding with caller-selected signed invoice allocation.\nSee [the cost billing contract](agents/cost-billing.md).\nIt does not alter Invoice persistence, editor defaults or settlement behavior.\n\n### Purchase-order header editing\n\nImport `PurchaseOrderHeaderForm` from `@happyvertical/smrt-commerce/svelte` for a\nnative POST of vendor, reference, notes, expected date and ship-to fields. Pass\n`action`, `vendors` and raw `values`; return canonical field `errors` and the same\n`hiddenFields` after server validation. This header-only form leaves purchase\nallocations and totals unchanged. `PurchaseOrder` stores `expectedDate` and\n`shipTo` in existing STI metadata. See [the purchasing contract](agents/purchasing-ui.md#purchase-order-header)\nfor validation, retained values and transport details.\n\n### Invoice retention and release\n\n`configureInvoiceRetention` freezes an explicit retained minor-unit amount on an\nunpaid draft. Supply tenant, invoice, currency, expected invoice revision,\nrequest identity, actor, reason, evidence reference, and versioned host policy.\nThe supported `creditPolicy: 'retained-first'` is an explicit caller choice:\nissued credits reduce unreleased retention first, then current payable. Hosts\nmust choose and authorize their regional policy; Commerce supplies no legal\npercentage or release date default. Configuration cannot be overwritten; replace\nan unissued draft to change the original agreement.\n\n`releaseInvoiceRetention` appends evidence under the invoice's transaction lock.\nIt cannot release more than the currently retained remainder. Exact request\nreplays are safe; changed payloads or stale revisions fail. Both commands retain\ncaller transaction affinity. Original gross, tax, line snapshots and approved\nconfiguration stay unchanged. Credits and credit cancellation derive remaining\nretention without rewriting release history.\n\n`invoice.getPayableFacts()` separates `grossBalanceMinor`,\n`retainedRemainingMinor`, `currentlyDueMinor`, and `allocationCapMinor`. The\nallocation API enforces the current cap. `getAmountDue()` returns current due;\n`getBalance()` remains the signed gross balance. An invoice with paid current\ndue and unreleased retention is not fully paid or overdue. Display gross and\ncurrent payable separately; pass explicit current due and remaining holdback to\nthe exact retained invoice UI props rather than treating gross as payable.\nStored money facts include `payable` for retained invoices; MCP projection honors\nfield visibility for every source of the payable alias.\n\nProvider-controlled imports/automatic collection cannot acquire retention.\nAccounting export refuses unreleased retention until the provider has an\nexplicit payable representation; it never silently requests collection of\ngross. Fully released invoices use the existing gross accounting contract.\n\n## Retained invoice accounting\n\n`loadRetainedInvoiceAccountingSnapshot` supplies a read-only frozen document for\nreviewing item/tax mapping before posting; `toRetainedInvoiceAccountingInput`\nvalidates that reviewed mapping against those frozen lines. Both preserve tenant and caller executor\nscope and performs no provider operation. Generic accounting snapshot/export still\nrefuses unreleased retention. A supplied host-managed release due date may precede\nthe release date: preserving original past-due terms makes the released amount\nimmediately overdue rather than silently extending payment terms.\n\n`loadRetainedCreditAccountingSnapshot` and `toRetainedCreditAccountingInput`\nprovide the equivalent review path for an issued calculated credit. The caller\nsupplies both original invoice and credit IDs; the owning API verifies tenant,\ncustomer, currency and exact original lineage, then retains the existing frozen\nreader's issue and provider-realm checks. Generic retained-credit export remains\nblocked. Review never creates a credit adjustment or contacts a provider.\n\nThe public `syncRetainedInvoiceToAccounting`, `releaseRetainedInvoiceToAccounting`,\n`syncRetainedCreditToAccounting`, and `syncRetainedRefundToAccounting` commands\nimplement an explicit QuickBooks accrual reclassification contract. They require\nSDK `receivableTransfers` from the published SDK0.104.1 cohort\n(happyvertical/sdk#1414 / #1415; absent from SDK0.104.0).\nThey are available from the Commerce root and `/invoices` entry points.\n\nThe tenant must review/version the realm, environment, customer, AR account,\nretained-asset account and optional bank account mapping. Supported choices are\n`accrual`, `invoice-tax-point` and `host-managed` release due dates. Verified home\ncurrency only; no inferred FX, tax timing, recovery percentage or release date.\nHosts authorize actors before these server commands and provide an active tenant.\n\nFor gross10500/retained1000, the original approved invoice remains10500. A\nwithholding journal debits retained assets and credits customer AR1000; a\nzero-cash application links that credit to the original invoice. A partial release\ncreates the opposite journal and an explicitly dated host collection obligation.\nThe original invoice and its tax snapshot are unchanged. Provider collections and\nprovider tax overrides are unsupported for this contract.\n\nInitial export requires an issued, unpaid, uncredited, unreleased host-owned\ninvoice with no prior provider identity. Historical source-owned invoices require\na separate reconciled ownership contract and are rejected: importing history must\nnot duplicate its original journal. Existing local retention commands continue to\nreject provider-controlled releases; use the provider release command instead.\n\nRetained-first credits cancel the applicable retained asset through a release\njournal, apply the exported credit memo to that journal, and apply any due portion\nto the original invoice. Unapplied credit remains available for a refund. Refund\nsync requires an already completed local refund Payment, its cash journal and one\nexact source-credit allocation. It records the cash journal and applies the\nexisting credit; it performs no bank action and makes no settlement-certification\nclaim. That source payment/allocation becomes immutable. Provider-managed invoice\nvoids/write-offs, credit cancellation/reallocation and refund deletion are rejected\nrather than silently erasing remote postings. Corrective reversal contracts are\nnot implemented by these commands.\n\n### Recovery and concurrency\n\nEach external write has persisted request bytes/hash, attempt owner, status and\nvalidated readback. Confirmed earlier steps survive a later failure. Unknown or\ninterrupted steps block new operations; `reconcileRetainedAccountingStep` requires\nthe observed request/attempt, a known remote ID for exact provider readback, or\nexplicit authoritative absence evidence. An interrupted in-flight owner must be\nproven stopped. Reconciliation evidence and previous attempts remain recorded.\n`reconcileRetainedBaseInvoice` wraps the existing frozen invoice recovery contract.\n`reconcileRetainedCreditAccounting` resolves the reserved credit-memo creation step. Ordinary credit export is blocked for provider-retained sources.\n\nThe descriptor hash is not remote idempotency. The SDK uses documented QuickBooks\nrequest-ID duplicate handling; never create a new request identity merely because\na response was lost. Exact local replays reuse confirmed steps. Competing commands\nserialize under the tenant/source invoice locks, and releases check an observed\ninvoice revision. Source credits cannot change during an unfinished retention\noperation. Commands reject caller-owned Commerce money transactions because a\nprovider write cannot be rolled back with local SQL.\n\n`readRetainedInvoiceAccountingState` exposes the persisted operation/readback\nhistory. Canonical `Invoice.getPayableFacts()` remains the source of local gross,\nretained, released, credited and currently due amounts. Provider invoice\n`TotalAmt - Balance` includes noncash applications and must never be labeled cash\nreceived. SDK readback separates noncash applications, recorded cash journals and\nunverified bank settlement.\n\nValidation uses native SQLite/PostgreSQL persistence with mocked public provider\noperations, plus the SDK's exact request/readback fixtures. No live provider call\nor sandbox certification has been performed. Primary contract evidence and\nprovider-field limits are documented in the SDK Accounting README.\n\n### Incomplete supplier price evidence\n\nUse `recordPurchasePriceObservation`, `readPurchasePriceObservation`, and\n`completePurchasePriceObservation` for source quotes/orders that lack complete\nnet/tax/gross facts. Unknown totals remain null and no calculated or reportable\nContract exists until explicit reviewed completion. See\n[observed purchase prices](agents/observed-purchase-prices.md).\n"
|
|
22033
|
+
"content": "# @happyvertical/smrt-commerce\n\nCommerce models for the s-m-r-t framework. Covers customers, vendors, contracts (10 STI types), invoices with ledger integration, payments, and fulfillment tracking.\n\n## Installation\n\n```bash\npnpm add @happyvertical/smrt-commerce\n```\n\n## Usage\n\n```typescript\nimport { withTenant } from '@happyvertical/smrt-tenancy';\nimport {\n CustomerCollection,\n ContractCollection,\n InvoiceCollection,\n InvoiceLineItemCollection,\n PaymentCollection,\n PaymentMethod,\n ensureCommerceTenant,\n} from '@happyvertical/smrt-commerce';\n\n// Once per tenant, after the host creates it: tax settings and standard codes.\nawait ensureCommerceTenant(db, tenantId);\n\n// Money documents (invoices, contracts, and their lines) belong to a tenant.\nawait withTenant({ tenantId }, async () => {\n // Create a customer linked to a profile\n const customers = await CustomerCollection.create({ db });\n const customer = await customers.create({\n profileId: 'profile-uuid',\n creditLimit: 1000000, // $10,000.00 — money is integer minor units\n paymentTerms: 'Net 30',\n });\n\n // Create an order (STI contract type)\n const contracts = await ContractCollection.create({ db });\n const order = await contracts.create({\n _meta_type: 'Order',\n customerId: customer.id,\n currency: 'CAD',\n });\n\n // Create an invoice. The server resolves each line's tax code and computes\n // line tax and the header totals; header amounts are never inputs.\n const invoices = await InvoiceCollection.create({ db });\n const invoice = await invoices.create({\n customerId: customer.id,\n contractId: order.id,\n invoiceNumber: await invoices.generateInvoiceNumber(),\n currency: 'CAD',\n });\n const lines = await InvoiceLineItemCollection.create({ db });\n await lines.create({\n invoiceId: invoice.id,\n description: 'Consulting',\n quantity: 10,\n unitPrice: 10000, // $100.00\n });\n\n // Issuing freezes the money snapshot; then recognize revenue (creates a\n // balanced journal in smrt-ledgers from the frozen facts).\n const issued = await invoices.get({ id: invoice.id }, { cache: false });\n issued.markSent();\n await issued.save();\n await issued.recognizeRevenue({\n arAccountId: 'ar-account-id',\n revenueAccountId: 'revenue-account-id',\n taxAccountId: 'tax-account-id',\n bookCurrency: 'CAD', // must equal the invoice currency\n });\n\n // Record a payment with ledger integration\n const payments = await PaymentCollection.create({ db });\n const payment = await payments.create({\n contractId: order.id,\n customerId: customer.id,\n amount: issued.totalAmount,\n method: PaymentMethod.CREDIT_CARD,\n });\n await payment.recordPayment({\n ledgerId: 'ledger-id',\n receivablesAccountId: 'ar-account-id',\n cashAccountId: 'bank-account-id',\n });\n});\n```\n\n## Commercial money calculator\n\nThe public `/money` entry is a pure calculator for line amounts, discounts,\ncomponent tax, document rounding, and totals. It consumes and returns integer\nminor-unit money values; tax and discount percentages are exact text (`'5'`\nmeans 5%). It does not persist or load documents.\n\n```ts\nimport { calculateMoneyDocument } from '@happyvertical/smrt-commerce/money';\n\nconst totals = calculateMoneyDocument({\n currency: 'CAD',\n lineAmountType: 'exclusive',\n roundingScope: 'line',\n lines: [{\n id: 'consulting', quantity: 1, unitPrice: 10000,\n discount: { kind: 'none' },\n taxCode: {\n id: 'gst', kind: 'taxable',\n components: [{ taxComponentId: 'gst', percent: '5', compound: false }],\n },\n }],\n});\n```\n\n`resolveLineTaxCode()` resolves caller-loaded codes without I/O, and the\nSvelte `InvoiceEditor` renders the money controls from its required `money`\nprop. Frozen editor documents render a supplied stored snapshot without\nrecalculation. See the [commerce money calculator guide](../../docs/content/commerce-money.md)\nfor pricing modes, rounding, input and result types, the native-form editor\ncontract, and the boundaries this package does not define.\n\n### Tenant tax catalogue and defaults\n\nThe package root also exports tenant-scoped `TaxComponent`, `TaxCode`, and\n`TaxSettings` models and their collections. Components hold exact percentage\nrates and optional liability accounts; codes combine compatible components; the\none settings row per tenant supplies sales/purchase defaults, amount mode,\nrounding scope, and labelled registration numbers. A referenced component rate\nis immutable: archive it and create a replacement for a rate change.\n\n`ensureTenantTaxCodes(db, tenantId)` idempotently creates the standard\nnon-taxable codes, `exempt`, `zero-rated`, and `out-of-scope`\n(`STANDARD_TAX_CODES`). `applyTaxCodeExamplePack(db, tenantId)` installs illustrative\nregional examples for review, and `applyCommerceCookbookSettings(db, tenantId,\ncookbook)` explicitly materializes a validated cookbook's defaults after schema\nsetup and authentication establish the tenant. See the [commerce money\ncalculator guide](../../docs/content/commerce-money.md#tenant-tax-catalogue) for\nthe supported kinds, seed behavior, and cookbook rate units.\n\n### Persisted Invoice money\n\nCommerce has one money model. Tax codes resolve each line's tax: the line's\noverride, else `exempt` for an exempt customer, else the product's code, the\ncustomer's code, then the tenant default. The server computes line\n`taxAmount` and the header totals and ignores any client total. Every Invoice\nand Contract money write needs a tenant; without one it fails with\n`MoneyValidationError` `INVALID_MONEY_INPUT`.\n`ensureCommerceTenant(db, tenantId, { cookbook })` is the one bootstrap call a\nhost makes after creating a tenant. It seeds the tenant's tax settings, the\nstandard codes, and the cookbook's defaults, and returns `{ tenantId }`.\n\nIssuing an Invoice (leaving `DRAFT`) or assigning `arJournalId` freezes its\nmoney snapshot. Header and line money changes then fail with `FROZEN_DOCUMENT`,\nwhile later lifecycle and payment updates retain the snapshot. Provider tax\nstays separate in `providerTaxAmount` and contributes to the Invoice headers\nwithout creating an application tax code.\n`InvoiceLineItemCollection.getTotalForInvoice()` sums line amounts for\ncompatibility; use persisted `Invoice.totalAmount` for the amount due.\n`recognizeRevenue()` and `cancelWithRevenueReversal()` require a `bookCurrency`\nequal to the invoice currency, and `toAccountingInput({ mapping })` requires\nexplicit QuickBooks realm mapping. The\n[commerce money calculator guide](../../docs/content/commerce-money.md#persisted-invoice-money)\nand [document-money contract](agents/document-money.md) cover persisted Invoice\nsemantics, credits, refunds, and posting. The materialized Invoice tax-report\ndescriptor and tenant-safe drilldown are documented in\n[tax-summary contract](agents/tax-summary.md). Contract money persistence,\nPurchaseContract tax contributors, and the materialized tax-summary host and\ndrilldown are available. Applications opt in by registering\n`createTaxSummaryMaterializedReportHost()` in their server-only report-host\nregistry; the route binds the authenticated tenant and read permission. See the\ntax-summary contract for registration details.\n\n### Upgrading money data from an earlier version\n\nCommerce refuses money data written by an earlier version with\n`COMMERCE_DATA_NEEDS_UPGRADE` instead of reading it. That is a schema that\nstill has the earlier money columns or the `commerce_money_migration_documents` table,\nan invoice or contract without a tenant, or a tenant with invoices or contracts but no tax settings row or with\ndocuments in the earlier format. `smrt db:status`\nreports it, so `smrt app doctor` reports `migration-required`. Run the normal\nschema migration, then upgrade once:\n\n```sh\nsmrt db:upgrade-commerce-money --check # report only\nsmrt db:upgrade-commerce-money # every tenant\n```\n\nEach tenant converts in its own transaction. Stored rates become tax codes,\nissued documents keep every stored total, and drafts are recomputed. The\nearlier columns are dropped once no tenant needs the upgrade. See\n[money data upgrade](agents/money-upgrade.md) and the\n[CLI guide](../cli/agents/commerce-money-upgrade.md).\n\n## API\n\n### Models\n\n#### Supplier quotations\n\n`SupplierQuote` extends `PurchaseContract`, requires `vendorId`, and has\n`ContractType.SUPPLIER_QUOTE`. Create it through `PurchaseContractCollection`\n(or `ContractCollection`) with `_meta_type:\n'@happyvertical/smrt-commerce:SupplierQuote'`. It inherits `expiryDate`,\n`reference`, `notes`, and `terms`; it has no customer relationship. Purchase\nbranch reads include quotes and sales branch reads exclude them.\n\nKeep received quote revisions and shared quote identity in your application.\nAwarding a quote creates a separate `PurchaseOrder`; accepting or saving a\nquote never converts its identity into an order. Quotes use purchase-side\nmoney calculation and frozen snapshots, but never contribute incurred purchase\ntax to reports, regardless of status. Existing Estimate and PurchaseOrder\nreport eligibility is unchanged.\n\n#### Upgrading contract parties (#3898)\n\n`Contract` no longer declares `customerId` or `vendorId`. Narrow a contract to\n`SalesContract` to read `customerId`, or to `PurchaseContract` to read `vendorId`.\nThe existing leaf qualified names and the `contracts` table stay the same; a\n`db:migrate --dry-run` against a v0.55.19 schema reports no DDL changes on\nSQLite or PostgreSQL. Sales leaves no longer accept `vendorId`; purchase leaves\nno longer accept `customerId`. `Order`, `Estimate`, `Agreement`, `Lease`, and\n`WholesaleOrder` require a customer on save. `PurchaseOrder` and `SupplierQuote`\nrequire a vendor.\n`Cart`, `LicenseSale`, and `ProductionOrder` retain optional parties on their side.\n\nBefore upgrading existing data, run the read-only\n`preflightCommercePartyLinks(db)` and inspect its stray party links, missing\nrequired parties, base-typed rows, discriminator mismatches, and invalid\ndocument references. Its `unqualifiedDiscriminatorRows` lists legacy simple\n`_meta_type` values such as `Order` and `PurchaseOrder`; retype them to the\nqualified class name before upgrading, since branch collections cannot read\nthem. The report checks invoice, payment, fulfillment, ad group,\nand expense links when those tables exist. A linked base-typed or wrong-branch\ncontract blocks the next save of its document even when the link is unchanged.\nRegistered custom descendants of either contract branch are accepted in link\nchecks; load those model classes before running the preflight.\nTenant-owned documents linked to global or other-tenant contracts are also\nreported because the save-time lookup is tenant-scoped. Retype the contract to\nthe correct leaf, assign it to the document's tenant, or repair the link before\ndeployment. The deprecated `ContractCollection` party finders now return the\nappropriate branch\nonly, so retype legacy base rows with party links before relying on those\nfinders. Clear stray party columns and fill required parties before deploying\nwriters that save affected rows. Missing required parties remain readable but\nfail their next save. Regenerate cached manifests, planner catalogs, and\ngenerated screens after upgrading.\nThis preflight never edits rows; an operator chooses the data correction.\n\n| Export | Description |\n|--------|------------|\n| `Customer` | Customer record with creditLimit, paymentTerms, tax exemption, addresses, contacts |\n| `Vendor` | Vendor/supplier with leadTimeDays, minimumOrderAmount, currency, contacts |\n| `Contract` | STI base class for all commercial agreements |\n| `SalesContract` | Abstract customer-side Contract branch; owns `customerId` |\n| `PurchaseContract` | Abstract supplier-side Contract branch; owns `vendorId` |\n| `Estimate` | Quote or proposal sent to a customer |\n| `Order` | Customer purchase order |\n| `Lease` | Rental or lease agreement |\n| `Agreement` | Service or maintenance agreement |\n| `SupplierQuote` | Quotation received from a vendor; distinct from an awarded purchase order |\n| `PurchaseOrder` | Order sent to a vendor/supplier |\n| `ContractLineItem` | Line item within a contract |\n| `Invoice` | Billing document with status lifecycle and ledger integration |\n| `InvoiceLineItem` | Individual invoice line item |\n| `Payment` | Payment record with method, status, and optional ledger journal |\n| `PaymentAllocation` | Payment-to-invoice allocation |\n| `Fulfillment` | Shipment/delivery tracking with carrier and address |\n| `FulfillmentLineItem` | Individual fulfillment line item |\n| `BillingAccount` | A payer's account with a seller: customer, provider customer, terms, standing |\n| `BillingPeriodClose` | One payer's invoice for one period and currency; also the billing job target |\n| `BillingLineSource` | The claim that a charge or plan period is billed on exactly one close |\n\n### Collections\n\n`CustomerCollection`, `VendorCollection`, `ContractCollection`, `SalesContractCollection`, `PurchaseContractCollection`, `InvoiceCollection`, `InvoiceLineItemCollection`, `PaymentCollection`, `PaymentAllocationCollection`, `FulfillmentCollection`\n\n`PaymentAllocationCollection.allocatePayment()` splits a payment across invoices\nin one transaction using stable allocation IDs for retries. Direct allocation\nsaves also enforce matching owners, customers and currencies and both balance\ncaps. Read [payment allocation integrity](agents/payment-allocations.md) for\nreplay, parent revision refresh and compatibility requirements.\n\n\n### Enums\n\n| Export | Values |\n|--------|--------|\n| `ContractType` | `estimate`, `order`, `lease`, `agreement`, `supplier_quote`, `purchase_order`, `wholesale_order`, `production_order`, `cart`, `license_sale` |\n| `ContractStatus` | `draft`, `sent`, `accepted`, `declined`, `completed`, `cancelled` |\n| `CustomerStatus` | `active`, `inactive`, `suspended` |\n| `VendorStatus` | `active`, `inactive`, `suspended` |\n| `InvoiceStatus` | `draft`, `sent`, `viewed`, `partial`, `paid`, `overdue`, `cancelled`, `written_off` |\n| `PaymentMethod` | `cash`, `check`, `credit_card`, `bank_transfer`, `crypto`, `other` |\n| `PaymentStatus` | `pending`, `completed`, `failed`, `refunded`, `cancelled` |\n| `FulfillmentStatus` | `pending`, `processing`, `shipped`, `delivered`, `cancelled` |\n| `FulfillmentType` | `shipment`, `delivery`, `pickup`, `digital`, `service` |\n\n### Types and Constants\n\n| Export | Description |\n|--------|------------|\n| `Address` | Shared address interface (street1, street2, city, state, postalCode, country) |\n| `RecognizeRevenueOptions` | Account IDs for invoice revenue recognition |\n| `RecordPaymentOptions` | Ledger and account IDs for payment recording |\n| `InvoiceNumberOptions` | Options for `generateInvoiceNumber()` (prefix, format) |\n| `UNPAID_STATUSES` | Array of invoice statuses considered unpaid (sent, viewed, partial, overdue) |\n| `COMMERCE_MODULE_META` | UI module metadata |\n| `COMMERCE_UI_SLOTS` | UI slot definitions |\n\n### Ledger Integration\n\nInvoice and Payment integrate with `@happyvertical/smrt-ledgers` via dynamic import (optional dependency). Invoice stores `arJournalId` and `revenueJournalId` as plain string references to ledger journals. `recognizeRevenue()` creates a balanced AR entry (DR: Accounts Receivable, CR: Revenue, CR: Tax Payable). `recordPayment()` creates a balanced cash receipt entry (DR: Cash, CR: Accounts Receivable). Both methods return null or throw if the ledgers package is not installed.\n\n`Payment.recordPayment()` claims the payment revision and commits the journal,\nentries, and completed payment in one database transaction. A failed completion\nrolls back the posting and restores the instance's settlement fields for retry.\nThe claimed persisted status is authoritative even if a caller locally resets\nstatus or marks the instance failed without saving. Concurrent or stale instances\nfail instead of posting twice; reload the payment\nand retry only if it is still pending. An already completed payment retains the\n`Payment already recorded` error. A caller's enclosing transaction remains the\nowner: if it rolls back after settlement returns, reload the payment before retry.\nThese guarantees cover new postings, not orphaned journals written by older\nversions. The returned journal uses the caller's database executor.\nBoth payment and refund journal lines retain the payment's explicit currency and\nexact amount; posting does not convert currencies or fall back to USD. When\nreporting accounts that contain different currencies, use the public\n`listPostedMovements` currency filter rather than combining their amounts.\n\n### Cross-Package References\n\nCustomer and Vendor link to `@happyvertical/smrt-profiles` via plain `profileId` string. Invoice and Payment reference `@happyvertical/smrt-ledgers` journals via plain string IDs (`arJournalId`, `revenueJournalId`, `journalId`). All models use `@TenantScoped({ mode: 'optional' })` with nullable `tenantId`.\n\n## Billing-period close\n\n`BillingRuntime` turns approved `@happyvertical/smrt-subscriptions` charges into\ninvoices for each billing owner, pushes them to a payment provider with\nprovider-calculated tax, posts revenue to the ledger, and applies the\nprovider's webhook events. Every provider call goes through\n`@happyvertical/accounting`; `createStripeBillingProvider()` adapts its Stripe\nprovider to the `BillingProvider` port. Money crossing the port is integer\nminor units.\n\n```ts\nimport { getAccountingProvider } from '@happyvertical/accounting';\nimport { SpendingPolicyEvaluator } from '@happyvertical/smrt-subscriptions';\nimport {\n BillingRuntime,\n createStripeBillingProvider,\n enqueueBillingPeriodClose,\n registerBillingRuntime,\n} from '@happyvertical/smrt-commerce';\n\nconst stripe = await getAccountingProvider({ type: 'stripe', secretKey });\nconst billing = await BillingRuntime.create({\n db,\n sellerTenantId: platformTenantId,\n kind: 'provider', // or 'reseller' to bill a reseller's children\n provider: createStripeBillingProvider({ stripe, webhookSecret }),\n billingRelationships, // smrt-tenancy BillingRelationshipService\n ledger: {\n bookCurrency: 'USD', // ISO 4217; must equal the billing currency\n arAccountId,\n revenueAccountId,\n taxAccountId,\n cashAccountId,\n prepaidCreditAccountId,\n },\n onPayerStanding: async ({ payerTenantId, standing, db }) => {\n // suspend or reinstate the payer; runs in the event transaction\n },\n});\n\n// One account per payer: its customer, tax location, and terms.\nawait billing.upsertAccount({\n payerTenantId,\n name: 'Network Co',\n email: 'billing@example.test',\n billingAddress: { country: 'CA', postalCode: 'T0L 0A0' },\n});\n\n// Optional: bill this payer from its activation date instead of calendar\n// months, prorating flat plans added mid-period (see \"Billing cycles\").\nawait billing.upsertAccount({\n payerTenantId,\n name: 'Network Co',\n billingAnchorAt: activatedAt,\n prorateFlatPlans: true,\n});\n\n// Close each payer's last ended period — safe to run on every schedule tick.\nregisterBillingRuntime('platform', billing);\nawait enqueueBillingPeriodClose({ runtime: 'platform' });\n\n// Webhook route: verify, enqueue, respond 2xx.\nconst intake = await billing.acceptWebhook(\n rawBody,\n request.headers.get('stripe-signature'),\n);\nif (intake.type?.endsWith(':unverified_credit_purchase')) {\n // alert: a paid checkout claimed to buy credit but failed verification\n}\nawait billing.processEvents(); // or enqueueBillingEvents({ runtime: 'platform' })\n\n// Prepaid credit through provider checkout: tax is added on top when the\n// payer's account is taxed; `savePaymentMethod` keeps the card for top-ups.\nconst { url } = await billing.createCreditCheckout({\n spendingPolicyId,\n amount: 5000,\n purchaseId: cartId,\n savePaymentMethod: true,\n successUrl,\n cancelUrl,\n});\n\n// Card on file at signup: a setup checkout that charges nothing. Its\n// completion event makes the card the payer's default and adopts the billing\n// address collected at checkout as the tax location.\nconst setup = await billing.createCardSetupCheckout({\n payerTenantId,\n currency: 'CAD',\n setupId: signupId,\n successUrl,\n cancelUrl,\n});\n\n// Automatic top-ups: charge the saved card when a balance would run out.\n// Accounts are taxed by default (`automaticTax`); a taxed account is charged\n// the credit plus provider-calculated tax (#3194).\nconst evaluator = await SpendingPolicyEvaluator.create({\n db,\n autoTopUp: billing.autoTopUpHook({ amount: () => 2500 }),\n});\n```\n\n- **What is billed.** A `provider` runtime bills `ClientCharge`s (and their\n `BillingAdjustment`s) to the payer each charge names, plus monthly flat plans\n it owns, to each subscriber's billing owner. A `reseller` runtime bills the\n `RetailCharge`s its children owe it plus its own flat plans. Charges approved\n before the period end are billed once, whatever period that is; flat plans\n are billed in arrears for the payer's closed period (trials and\n provider-managed subscriptions are skipped). One invoice per payer and\n currency carries both kinds of lines.\n- **Replay safety.** Close, invoice, and line ids are derived from the payer,\n currency, and period; each charge is claimed by exactly one close; the\n provider invoice uses the close id as its idempotency key; and every step is\n persisted before the next, so a retry resumes where it stopped. A lease stops\n two workers advancing the same close. A close without a period also resumes\n every unfinished close whose period has ended, even one the payer's current\n schedule no longer produces (#3116). A payer whose charges net to a credit\n gets no invoice; the credit is carried to its next invoice.\n- **Billing cycles (#3116).** A payer's periods follow its account's\n `billingAnchorAt`. Unset (the default) they are UTC calendar months. Set,\n they run monthly from the anchor at its UTC time of day; a day the month\n lacks is clamped to its last day, always from the anchor itself (an anchor\n on Jan 31 bills Jan 31 → Feb 28 → Mar 31). Before the anchor the schedule is\n calendar months, and the anchor's own month ends at the anchor (a *stub*).\n `closePeriod()` / `enqueueBillingPeriodClose()` with no period close each\n payer's **last ended period on its own schedule**, so payers on different\n anchors are closed by the same daily tick. An explicit\n `{ periodStart, periodEnd }` closes that period for calendar payers (flat\n plans only when it is a calendar month, as before) and for anchored payers\n whose schedule has exactly that period. `billingPeriodFor(payer, at)` returns\n a payer's current period; `billingPeriodContaining()`,\n `lastEndedBillingPeriod()`, and `prorateMinorUnits()` are exported.\n- **Proration.** A flat plan is priced `price × billed time / cycle length`,\n in integer minor units rounded half up (exact integer arithmetic; the\n cycle is the period's own month, so February prorates over 28 or 29 days).\n With `prorateFlatPlans` off (default) the billed time is the whole period\n whenever the subscription was active in it and out of trial at its start —\n the pre-#3116 behavior. With it on, the billed time is exactly the time the\n subscription was active and out of trial: a subscription starting, leaving\n trial, or canceled mid-period is billed pro rata. Proration happens before\n the account's flat discount. Split into parts, prorated amounts can differ\n from the full price by a minor unit.\n- **First period.** *Anchor at signup* (set `billingAnchorAt` to the\n signup/activation instant): the first period is a full period from signup;\n nothing before it is billed. *Bill from signup to the next anchor* (keep a\n fixed anchor, or calendar months, and set `prorateFlatPlans`): the first\n period is billed pro rata from signup. Items added later are prorated to the\n payer's anchor when `prorateFlatPlans` is on and billed a full period\n otherwise.\n- **Mid-period changes.** Billing is in arrears, so a cancellation needs no\n credit: with proration the canceled period is billed up to `canceledAt`,\n without it in full. A plan change is billed at the plan in effect at close\n for the whole window (no plan-change proration yet, #3119), and a cancellation\n back-dated into an already billed period is not credited — issue an\n adjustment.\n- **No double billing across schedule changes.** Every flat-plan claim records\n the exact time it bills, and a subscription's claims are numbered, so only\n one worker can write the next claim and two can never claim the same time.\n Moving an account from calendar months to an anchor, moving an anchor, or\n moving a subscription to a payer on another schedule bills only time not yet\n billed, prorated. For example, calendar months through February and then an\n anchor on Mar 20 bills the Mar 1 → Mar 20 stub pro rata, then Mar 20 → Apr\n 20. Time around an already billed window is billed as separate lines, each\n with its own service period. Stop workers running a pre-#3116 version\n before relying on this: they claim flat plans outside the numbering.\n- **Service periods.** Each flat-plan invoice line carries its billed window\n as `periodStart`/`periodEnd`, down to the provider port and on to Stripe's\n invoice item `period`.\n- **Upgrading to #3116.** Additive schema only: `_smrt_billing_accounts` gains\n `billing_anchor_at` (nullable) and `prorate_flat_plans` (default false), and\n `_smrt_billing_line_sources` gains `chain_sequence` (default 0) and an index\n on `(line_key, chain_sequence)`; run `smrt db:migrate`.\n Existing accounts stay on calendar months, and claims written before the\n upgrade (`<subscriptionId>:<periodStart>`, number 0) count as billed\n coverage, so nothing is billed again after it.\n- **Tax** comes from the provider (Stripe Tax) using the account customer's\n `defaultBillingAddress`; the invoice records it as `providerTaxAmount` in the\n save that issues it. Billing invoices are ordinary money-model invoices in\n the seller's tenant; period close seeds its standard codes if needed. Every\n billing line is an explicit override to the seller's `out-of-scope` code, so\n the seller's default code never taxes it a second time. Charges are written\n before credits, so the running subtotal never goes negative. Revenue posting\n uses `ledger.bookCurrency` and is idempotent.\n- **Events** are verified, stored in smrt-jobs' durable delivery inbox, and\n applied against current provider state: paid invoices record a payment and\n allocation; failures and overdue notices mark the payer `past_due` and its\n flat-plan subscriptions `past_due`; an invoice written off at the provider\n (`markInvoiceUncollectible(invoiceId)`, or in Stripe) is written off\n locally and marks the payer `uncollectible` (no bad-debt journal is\n posted; AR stays until it is paid or an operator journals it); payment\n reinstates them, including a late payment of the written-off invoice. Dunning is the provider's.\n Subscribe the webhook endpoint to `invoice.*`, `customer.subscription.*`,\n `checkout.session.completed`, `checkout.session.async_payment_succeeded`,\n and `payment_intent.*`.\n- **Prepaid credit** purchases credit a `period: 'balance'` spending policy\n once per checkout session, paid by the policy's tenant or, for a delegated\n balance, the parent that set it. Amounts are ISO 4217 minor units in any\n currency the provider supports. When the payer's account is taxed\n (`automaticTax`, not tax-exempt; override per call), provider tax is\n charged on top and collected at checkout with the buyer's address: the\n credit is the amount bought, the payment is the total collected, and the\n tax is booked to the tax account. For a payer with a provider customer but\n no local tax location, the address collected at checkout becomes its tax\n location. `savePaymentMethod` requires a provider that can re-read the\n checkout and set a default payment method, and is refused otherwise.\n- **Card on file (#3139).** `createCardSetupCheckout()` (or a credit purchase\n with `savePaymentMethod`) saves the payer's card without charging it. On\n completion the card becomes the provider customer's default and the\n payer's default `PaymentInstrument`. When the checkout collected the billing\n address (a taxed payer with no tax location), it becomes the payer's tax\n location. `cardOnFile(payerTenantId)` returns `{ onFile, savedAt? }` from\n the same check automatic charges use (a default card with the account's\n provider customer), for \"card on file\" displays and signup activation;\n the payer or a system context may call it (#3187).\n- **Automatically charged invoices.** With `autoChargeInvoices: true` on the\n runtime, a payer with a default card is invoiced `charge_automatically`:\n the provider charges the card after the invoice is sent, on its own\n schedule, and retries failures. Each pushed invoice records the method in\n `Invoice.collectionMethod` (`charge_automatically` or `send_invoice`; empty\n for invoices pushed before #3190). Do not offer another payment rail on a\n `charge_automatically` invoice the provider is still collecting.\n Upgrading adds the `collection_method` column to `invoices` (run\n `smrt db:migrate`). Activate service on the `current` standing\n from the `paid` event, not on send; a failure marks the payer `past_due`.\n- **Automatic top-ups.** Taxed accounts (the default: `automaticTax` on, not\n tax-exempt) are charged the credit plus provider-calculated tax (#3194):\n the balance is credited the credit, the payment records the credit, and\n the tax is booked to the tax account. `taxedAccounts: 'skip'` does not top\n them up, and `'charge_untaxed'` charges them without tax. A taxed payer\n without a tax location is not charged. A location Stripe rejects fails the\n attempt with `failureCode: 'customer_tax_location_invalid'`\n (`onAutoTopUpFailed`), and it is retried only after `retryAfterMs`. Pass\n `onAutoTopUpSkipped` to learn when a needed top-up will not be charged\n because of the payer's account. Its `reason` is `taxed_account`, `no_card`,\n `no_account`, or `tax_location_invalid`, and more may be added. Use it, for\n example, to ask the payer to top up by hand or to add a billing address; it is not called while a charge is processing, while a\n declined card waits out `retryAfterMs`, or when the top-up is no longer\n needed or `amount` declines it (#3189).\n `billing.autoTopUpHook()` is the `autoTopUp` hook for\n `SpendingPolicyEvaluator`: when a balance would run out it charges the\n payer's saved card off-session (the delegating parent's, for a delegated\n balance) and credits the balance only when the charge succeeds. A charge\n still processing is credited from its `payment_intent.succeeded` event; a\n decline or an authentication requirement credits nothing, closes the\n attempt, and calls the runtime's `onAutoTopUpFailed` (ask the payer to\n re-save their card with a setup checkout). One attempt runs per policy at a\n time, retries of a declined card wait `retryAfterMs` (default one hour),\n and the credit is granted exactly once however the outcome arrives.\n- **Product tax codes (#3194).** `taxCodes: { credit, invoice }` on the\n runtime classifies prepaid credit (checkout purchases and taxed top-ups)\n and billing-period invoice lines for Stripe Tax (`txcd_...`). Without it,\n the Stripe account's default product tax code applies.\n- **Upgrading to #3194.** Requires `@happyvertical/accounting` 0.96 or later.\n No schema change. Taxed accounts are now topped up with tax. To keep the\n previous behaviour, pass `taxedAccounts: 'skip'`.\n- **Upgrading to #3139.** Requires `@happyvertical/accounting` 0.92 or later.\n No schema change. Credit checkouts are now taxed for taxed accounts (pass\n `automaticTax: false` to keep them untaxed), and Stripe customers are\n created idempotently.\n\n### Payment rails: BTC and other crypto (#3138)\n\nA seller runtime can take payments on more than one **rail**. The issuing\n`provider` (Stripe) still issues, taxes, and emails every invoice; extra rails\nin `paymentProviders` let a payer choose, per payment, to pay an invoice or buy\nprepaid credit another way. The crypto rail runs short-lived, fiat-priced\ncheckouts through `@happyvertical/payments`' provider-neutral\n`CryptoCheckoutGateway` — BTCPay Server at launch — so another gateway can\nreplace BTCPay without changing this package or its consumers.\n\n```ts\nimport {\n BillingRuntime,\n createBtcPayBillingProvider,\n createStripeBillingProvider,\n} from '@happyvertical/smrt-commerce';\n\nconst btcpay = createBtcPayBillingProvider({\n baseUrl, apiKey, storeId, // one BTCPay store per seller entity\n webhookSecret, metadataSecret, // from the host's secret store, by name\n speedPolicy: 'LowMediumSpeed', // settle after 2 confirmations (BTCPay offers 0/1/2/6)\n rateSource: 'kraken',\n minimumAmount: { CAD: 500, USD: 500 },\n});\nconst billing = await BillingRuntime.create({\n db, sellerTenantId, kind: 'provider',\n provider: createStripeBillingProvider({ stripe, webhookSecret }),\n paymentProviders: [btcpay],\n paymentPolicy: { latePaymentPolicy: 'review', refundBasis: 'original_native' },\n billingRelationships,\n ledger: { ...ledger, cryptoHoldingsAccountId, fxGainLossAccountId, feesAccountId },\n});\n\n// Prepaid credit or an issued invoice, paid with BTC:\nawait billing.createCreditCheckout({ ...purchase, provider: 'btcpay' });\nawait billing.createInvoicePayment({ invoiceId, provider: 'btcpay', purchaseId, successUrl, cancelUrl });\n\n// Webhook route for the rail (BTCPay signs with its own header):\nawait billing.acceptWebhook(rawBody, '', { provider: 'btcpay', headers: request.headers });\nawait billing.processEvents();\n```\n\n- **Refusals are typed (#3185).** `createInvoicePayment()` and\n `createCreditCheckout()` throw `BillingPaymentRefusedError` for requests the\n payer can correct, with a stable `code`: `not_found`, `not_payable`,\n `payment_in_progress`, `nothing_due`, `invalid_amount`, `below_minimum`\n (under the rail's `minimumAmount`), or `no_account`. Show its message as a\n 4xx; any other error is a server failure, and `TenantIsolationError` still\n means the caller is not the payer. Treat an unknown code like\n `not_payable`.\n- **Settlement is the gateway's.** Credit is granted and invoices are paid\n only when the gateway reports `settled` under its own confirmation policy;\n this package never counts confirmations. BTCPay offers 0, 1, 2 or 6\n confirmations; the default is `LowMediumSpeed` (2 confirmations).\n- **Payment attempts.** Every rail checkout is a `BillingPaymentAttempt`\n recording the locked fiat price, fiat and native amounts received, rate and\n rate source, each payment's txid, rail and fee, a timeline of status changes,\n and what settlement produced. `listPaymentAttempts()` feeds \"payment\n confirming\" displays and operator queues; every filter applies before its\n `limit` and `offset`, so paging through `{ unresolved: true }` lists every\n open exception (flagged, not yet resolved) however old. `refreshPaymentAttempts()` is the polling\n fallback for missed webhooks.\n- **Dunning pauses** for an invoice while a payment for it is confirming\n (0-conf seen); if the payment then expires or is invalidated the payer's\n standing is re-applied.\n- **Invoices paid on a rail** are closed at the issuer with\n `markInvoicePaidOutOfBand` (Stripe `paid_out_of_band`), so Stripe stops\n collecting; Stripe's own `paid` event then records nothing more.\n `createInvoicePayment` refuses when the issuing provider cannot do that.\n A written-off (uncollectible) invoice with an amount due can be paid on a\n rail too, as it can by card at Stripe (#3188): the payment is recorded,\n Stripe's invoice is closed out of band, and the payer returns to `current`;\n the invoice keeps its `WRITTEN_OFF` status (its `amountPaid` shows the\n recovery).\n- **Exceptions never auto-refund.** Underpaid, overpaid, late, manually marked\n (at the gateway), invalidated-after-settlement, and paid-twice attempts are\n flagged for an operator (`resolvePaymentAttempt`). Per-seller\n `paymentPolicy` sets under/overpayment tolerances (basis points), late\n payments (`review` | `accept`), manually marked checkouts, hold vs convert\n (`conversionHandler`), and the refund basis.\n- **Ledger.** Receipts are booked at the locked fiat amount into\n `cryptoHoldingsAccountId` (default `cashAccountId`); money received above\n the price is booked as customer credit (prepaid-credit liability).\n `recordManualRefund()` refunds from that excess first, then from purchased\n credit (or an invoice payment kept as credit because the invoice was paid\n elsewhere) — capped cumulatively, once per reference; an invoice payment\n applied to its invoice is reversed at the issuer, not here.\n `recordCryptoConversion()` posts a conversion with FX gain/loss.\n- **Upgrading.** Additive schema: `_smrt_billing_payment_attempts` (run\n `smrt db:migrate`). A runtime without `paymentProviders` never reads it.\n- **One live payment per invoice.** The issuer's own `paid` event for an\n invoice a rail closed out of band records nothing (the rail records it); an\n out-of-band close made by someone else (wire, cheque) is recorded as an\n `OTHER`-method payment; a real card payment made while a rail payment\n confirms is recorded and the rail money is flagged `invoice_already_paid`.\n An issuer close whose rail payment never settles is flagged\n `out_of_band_without_settlement`: reopen the invoice at the issuer, then\n `resolvePaymentAttempt()` it, after which a later out-of-band close is\n recorded normally.\n- **Entities.** Each legal entity is its own seller runtime with its own\n store, keys, and ledger. Inter-entity resale is ordinary reseller billing\n between two sellers, payable on any rail the selling entity offers.\n\nSee [`AGENTS.md`](./AGENTS.md#billing-period-close-3060) for invariants and\nknown limits.\n\n## Tenant-owned connected payment accounts\n\nA tenant (for example a co-op) can be paid into its own Stripe Connect\nStandard account, so its customers' money never passes through the platform.\nCheckouts are direct charges on the tenant's account; the platform's share is\nan application fee. A tenant without an enabled account is refused — there is\nno fallback to the platform account.\n\n```ts\nimport { SecretService } from '@happyvertical/smrt-secrets';\nimport {\n createConnectedCheckout,\n createStripeConnectGateway,\n createTenantOnboardingLink,\n handleConnectedPaymentWebhook,\n onboardTenantPaymentAccount,\n refundConnectedPayment,\n} from '@happyvertical/smrt-commerce';\n\nconst gateway = await createStripeConnectGateway({\n secrets: await SecretService.create({ db }),\n secretsTenantId: installationTenantId,\n environment: 'live',\n adapterOptions: { successUrl, cancelUrl, defaultCurrency: 'CAD' },\n});\n\n// Onboarding: create the tenant's Standard account, then send its treasurer\n// to Stripe. The link is a bearer secret: redirect, never log or store.\nawait onboardTenantPaymentAccount({ db, gateway, tenantId, environment: 'live', country: 'CA' });\nconst { url } = await createTenantOnboardingLink({\n db, gateway, tenantId, environment: 'live', returnUrl, refreshUrl,\n});\n\n// Checkout: the fee comes from the tenant's fee policy, in integer minor units.\nconst { intent, checkoutUrl } = await createConnectedCheckout({\n db, gateway, tenantId, environment: 'live',\n offeringRef: 'membership-2027', licenseeEmail, idempotencyKey,\n amount: 10_000, currency: 'CAD', applicationFeeAmount: 500,\n});\n\n// Webhooks (Connect endpoint; use endpoint: 'platform' for application_fee.*).\nconst result = await handleConnectedPaymentWebhook({\n db, gateway, environment: 'live', payload: rawBody,\n signature: request.headers.get('Stripe-Signature') ?? undefined,\n resolveSettlementAccounts: ({ tenantId }) => ledgerAccountsFor(tenantId),\n});\n\n// Refunds go to the tenant's account; the fee-refund choice is explicit.\nawait refundConnectedPayment({\n db, gateway, tenantId, environment: 'live', paymentId, amount: 4_000,\n refundApplicationFee: true, idempotencyKey, resolveSettlementAccounts,\n});\n```\n\nThe settled `Payment` books cash net of the fee and the fee as an expense, so\nthe fee never becomes a payable to the platform. Webhook signatures are\nverified before routing, events are routed by connected account to the\nowning tenant, quotes are bound to that tenant before settling, and processed\nevent ids are stored. Secrets, endpoints, and limits:\n[connected payments](agents/connected-payments.md).\n\n## Dependencies\n\n- `@happyvertical/smrt-core` -- ORM and code generation\n- `@happyvertical/smrt-tenancy` -- multi-tenant scoping\n- `@happyvertical/smrt-subscriptions` -- charges, plans, and credit balances billed by period close\n- `@happyvertical/smrt-jobs` -- period-close jobs and the provider event inbox\n- `@happyvertical/accounting` -- payment-provider (Stripe) calls\n- `@happyvertical/smrt-types` -- shared type definitions\n- Peer: `@happyvertical/smrt-ledgers`, `@happyvertical/smrt-profiles`, `@happyvertical/smrt-svelte`\n\n## Contributor guide\n\nSee [`AGENTS.md`](./AGENTS.md) for package architecture, invariants, validation,\nand contributor guidance.\n\n## Pricing version UI\n\n`PricingVersionSummary` and `PricingVersionDecision` compose the quote revision\ncontract without adding a pricing ledger. Compare versions using\n`QuoteRevisionComparison`; the application owns every decision and native form\naction. See [pricing version presentation](agents/pricing-versions.md).\n\n## Commercial UI composition\n\nCustomer addresses and vendor/quote currencies use the shared localized code\nselectors while preserving native field names and rejected raw drafts. See\n[commercial UI contracts and backend boundaries](agents/commercial-ui.md) for\nnative forms, recovery, amount units and consumer adoption, plus the focused\n[customer/vendor](agents/customer-vendor-ui.md) and\n[quotation](agents/quote-ui.md) contracts for selector restrictions.\n\n## Customer and vendor contacts\n\n`Customer.contacts` and `Vendor.contacts` hold the people to reach at a party\n(`PartyContact`: `id`, `name`, `label`, `email`, `phone`, `address`) as a JSON\ncolumn; existing databases need `smrt db:migrate` for the new `contacts`\ncolumns. The form's contact rows are written with `savePartyContacts()`:\n\n```typescript\nimport {\n applyPartyContactIntent,\n PartyContactValidationError,\n readPartyContactRows,\n savePartyContacts,\n} from '@happyvertical/smrt-commerce';\n\nconst form = await request.formData();\n// Throws PartyContactPayloadError when the form's contactCount marker is absent\n// (block not rendered, or fieldNames differ), so a save never deletes by accident.\nconst rows = readPartyContactRows(form); // pass the form's fieldNames if overridden\nconst { contacts, handled } = applyPartyContactIntent(rows, String(form.get('intent')));\nif (handled) return { values: { ...values, contacts } }; // add/remove round trip, no save\ntry {\n await savePartyContacts(vendors, vendorId, rows, { expectedTenantId });\n} catch (error) {\n if (error instanceof PartyContactValidationError)\n return fail(400, { values: { ...values, contacts: rows }, errors: { contacts: toMessages(error.errors) } });\n throw error;\n}\n```\n\n`savePartyContacts` also accepts the `FormData` itself (with `fieldNames`) and\nreads it the same way. The rows replace the party's contacts: posted ids are kept, new rows get an\nid, unposted rows are removed, blank rows are dropped. See\n[customer/vendor UI](agents/customer-vendor-ui.md#contact-rows-3501).\n\n## Customer and vendor selectors\n\n`CustomerSelect` and `VendorSelect` from `@happyvertical/smrt-commerce/svelte`\nare searchable, field-sized pickers for a customer or vendor relation field.\nThey are registered in `COMMERCE_UI_SLOTS` with `selects:\n'@happyvertical/smrt-commerce:Customer'` / `':Vendor'`, so generic forms (via\n`findSelectorFor` over the manifest's `uiSelectors`) use them for any\n`@foreignKey` or `@crossPackageRef` field targeting those models.\n\nThey are presentation only. The caller owns the data: `search(query)` returns\nthe existing party DTOs (`CustomerDisplayData` / `VendorDisplayData`, whose\n`profile.name` is the label), and `resolve(id)` returns the record behind the\ncurrent value. Each row shows the profile name with status (and customer type).\nThe committed id, never the name, posts under `name`. Pass `onCreate` to offer a\n\"New customer\" / \"New vendor\" action; it receives the searched text, and a\nreturned record is selected.\n\n```svelte\n<CustomerSelect\n name=\"customerId\"\n bind:value={customerId}\n search={(query) => customers.search(query)}\n resolve={(id) => customers.byId(id)}\n onCreate={(query) => openCustomerDialog(query)}\n/>\n```\n\nThe same models carry `@field({ ui: { widget } })` hints in the manifest:\n`textarea` on `Contract` (and subclass) `terms` and `notes`, `Customer`/`Vendor`\n`notes`, and `Invoice` notes and terms; `currency` (an ISO 4217 code picker) on\n`Contract`, `Vendor`, `Invoice` and `Payment` `currency`; `email`/`phone` on\n`Vendor.defaultContactEmail`/`defaultContactPhone`. See\n[customer and vendor selectors](agents/customer-vendor-ui.md#selectors-3602).\n\n## Invoice preparation UI\n\n`InvoiceEditor` from `@happyvertical/smrt-commerce/svelte` is the invoice form,\nbuilt on the commercial money model, v2, which is the only invoice money model.\nIt requires a `money` prop (`MoneyEditorProps`): a lossless draft with\ndescriptions, fractional quantities, unit prices, percentage or amount\ndiscounts and a tax code per line (inherited or overridden), plus the\nauthorized tax codes. Its previews run the same exact calculator the server\nuses, and the server re-resolves tax codes and recomputes every stored amount.\nAn issued document renders only its frozen stored snapshot.\n`InvoiceAllocationFields` and `InvoiceReview` add caller-owned sources and\napprovals. See [the editor contract](agents/invoice-preparation.md), the\n[line calculation helper](agents/invoice-calculations.md), and the\n[existing-database storage migration](agents/invoice-quantity.md).\n\n## Exact cost billing\n\nThe independent `@happyvertical/smrt-commerce/cost-billing` entry point provides\nexact signed reimbursement/fee calculations, ordered generic markup provenance,\nand explicit line or invoice tax rounding. Its opt-in ordered tax calculator\nsupports subset compounding with caller-selected signed invoice allocation.\nSee [the cost billing contract](agents/cost-billing.md).\nIt does not alter Invoice persistence, editor defaults or settlement behavior.\n\n### Purchase-order header editing\n\nImport `PurchaseOrderHeaderForm` from `@happyvertical/smrt-commerce/svelte` for a\nnative POST of vendor, reference, notes, expected date and ship-to fields. Pass\n`action`, `vendors` and raw `values`; return canonical field `errors` and the same\n`hiddenFields` after server validation. This header-only form leaves purchase\nallocations and totals unchanged. `PurchaseOrder` stores `expectedDate` and\n`shipTo` in existing STI metadata. See [the purchasing contract](agents/purchasing-ui.md#purchase-order-header)\nfor validation, retained values and transport details.\n\n### Invoice retention and release\n\n`configureInvoiceRetention` freezes an explicit retained minor-unit amount on an\nunpaid draft. Supply tenant, invoice, currency, expected invoice revision,\nrequest identity, actor, reason, evidence reference, and versioned host policy.\nThe supported `creditPolicy: 'retained-first'` is an explicit caller choice:\nissued credits reduce unreleased retention first, then current payable. Hosts\nmust choose and authorize their regional policy; Commerce supplies no legal\npercentage or release date default. Configuration cannot be overwritten; replace\nan unissued draft to change the original agreement.\n\n`releaseInvoiceRetention` appends evidence under the invoice's transaction lock.\nIt cannot release more than the currently retained remainder. Exact request\nreplays are safe; changed payloads or stale revisions fail. Both commands retain\ncaller transaction affinity. Original gross, tax, line snapshots and approved\nconfiguration stay unchanged. Credits and credit cancellation derive remaining\nretention without rewriting release history.\n\n`invoice.getPayableFacts()` separates `grossBalanceMinor`,\n`retainedRemainingMinor`, `currentlyDueMinor`, and `allocationCapMinor`. The\nallocation API enforces the current cap. `getAmountDue()` returns current due;\n`getBalance()` remains the signed gross balance. An invoice with paid current\ndue and unreleased retention is not fully paid or overdue. Display gross and\ncurrent payable separately; pass explicit current due and remaining holdback to\nthe exact retained invoice UI props rather than treating gross as payable.\nStored money facts include `payable` for retained invoices; MCP projection honors\nfield visibility for every source of the payable alias.\n\nProvider-controlled imports/automatic collection cannot acquire retention.\nAccounting export refuses unreleased retention until the provider has an\nexplicit payable representation; it never silently requests collection of\ngross. Fully released invoices use the existing gross accounting contract.\n\n## Retained invoice accounting\n\n`loadRetainedInvoiceAccountingSnapshot` supplies a read-only frozen document for\nreviewing item/tax mapping before posting; `toRetainedInvoiceAccountingInput`\nvalidates that reviewed mapping against those frozen lines. Both preserve tenant and caller executor\nscope and performs no provider operation. Generic accounting snapshot/export still\nrefuses unreleased retention. A supplied host-managed release due date may precede\nthe release date: preserving original past-due terms makes the released amount\nimmediately overdue rather than silently extending payment terms.\n\n`loadRetainedCreditAccountingSnapshot` and `toRetainedCreditAccountingInput`\nprovide the equivalent review path for an issued calculated credit. The caller\nsupplies both original invoice and credit IDs; the owning API verifies tenant,\ncustomer, currency and exact original lineage, then retains the existing frozen\nreader's issue and provider-realm checks. Generic retained-credit export remains\nblocked. Review never creates a credit adjustment or contacts a provider.\n\nThe public `syncRetainedInvoiceToAccounting`, `releaseRetainedInvoiceToAccounting`,\n`syncRetainedCreditToAccounting`, and `syncRetainedRefundToAccounting` commands\nimplement an explicit QuickBooks accrual reclassification contract. They require\nSDK `receivableTransfers` from the published SDK0.104.1 cohort\n(happyvertical/sdk#1414 / #1415; absent from SDK0.104.0).\nThey are available from the Commerce root and `/invoices` entry points.\n\nThe tenant must review/version the realm, environment, customer, AR account,\nretained-asset account and optional bank account mapping. Supported choices are\n`accrual`, `invoice-tax-point` and `host-managed` release due dates. Verified home\ncurrency only; no inferred FX, tax timing, recovery percentage or release date.\nHosts authorize actors before these server commands and provide an active tenant.\n\nFor gross10500/retained1000, the original approved invoice remains10500. A\nwithholding journal debits retained assets and credits customer AR1000; a\nzero-cash application links that credit to the original invoice. A partial release\ncreates the opposite journal and an explicitly dated host collection obligation.\nThe original invoice and its tax snapshot are unchanged. Provider collections and\nprovider tax overrides are unsupported for this contract.\n\nInitial export requires an issued, unpaid, uncredited, unreleased host-owned\ninvoice with no prior provider identity. Historical source-owned invoices require\na separate reconciled ownership contract and are rejected: importing history must\nnot duplicate its original journal. Existing local retention commands continue to\nreject provider-controlled releases; use the provider release command instead.\n\nRetained-first credits cancel the applicable retained asset through a release\njournal, apply the exported credit memo to that journal, and apply any due portion\nto the original invoice. Unapplied credit remains available for a refund. Refund\nsync requires an already completed local refund Payment, its cash journal and one\nexact source-credit allocation. It records the cash journal and applies the\nexisting credit; it performs no bank action and makes no settlement-certification\nclaim. That source payment/allocation becomes immutable. Provider-managed invoice\nvoids/write-offs, credit cancellation/reallocation and refund deletion are rejected\nrather than silently erasing remote postings. Corrective reversal contracts are\nnot implemented by these commands.\n\n### Recovery and concurrency\n\nEach external write has persisted request bytes/hash, attempt owner, status and\nvalidated readback. Confirmed earlier steps survive a later failure. Unknown or\ninterrupted steps block new operations; `reconcileRetainedAccountingStep` requires\nthe observed request/attempt, a known remote ID for exact provider readback, or\nexplicit authoritative absence evidence. An interrupted in-flight owner must be\nproven stopped. Reconciliation evidence and previous attempts remain recorded.\n`reconcileRetainedBaseInvoice` wraps the existing frozen invoice recovery contract.\n`reconcileRetainedCreditAccounting` resolves the reserved credit-memo creation step. Ordinary credit export is blocked for provider-retained sources.\n\nThe descriptor hash is not remote idempotency. The SDK uses documented QuickBooks\nrequest-ID duplicate handling; never create a new request identity merely because\na response was lost. Exact local replays reuse confirmed steps. Competing commands\nserialize under the tenant/source invoice locks, and releases check an observed\ninvoice revision. Source credits cannot change during an unfinished retention\noperation. Commands reject caller-owned Commerce money transactions because a\nprovider write cannot be rolled back with local SQL.\n\n`readRetainedInvoiceAccountingState` exposes the persisted operation/readback\nhistory. Canonical `Invoice.getPayableFacts()` remains the source of local gross,\nretained, released, credited and currently due amounts. Provider invoice\n`TotalAmt - Balance` includes noncash applications and must never be labeled cash\nreceived. SDK readback separates noncash applications, recorded cash journals and\nunverified bank settlement.\n\nValidation uses native SQLite/PostgreSQL persistence with mocked public provider\noperations, plus the SDK's exact request/readback fixtures. No live provider call\nor sandbox certification has been performed. Primary contract evidence and\nprovider-field limits are documented in the SDK Accounting README.\n\n### Incomplete supplier price evidence\n\nUse `recordPurchasePriceObservation`, `readPurchasePriceObservation`, and\n`completePurchasePriceObservation` for source quotes/orders that lack complete\nnet/tax/gross facts. Unknown totals remain null and no calculated or reportable\nContract exists until explicit reviewed completion. See\n[observed purchase prices](agents/observed-purchase-prices.md).\n"
|
|
21459
22034
|
},
|
|
21460
22035
|
{
|
|
21461
22036
|
"path": "agents/invoice-quantity.md",
|
|
21462
22037
|
"module": "invoice-quantity",
|
|
21463
22038
|
"content": "# Invoice quantity persistence\n\nInvoiceLineItem accepts nonnegative decimal quantities with at most six decimal\nplaces. The `1.0` default declares a floating-point quantity column. Unit price,\ndiscount and amount stay safe integer minor units. Every save goes through the\ninvoice money aggregate, which recomputes the line and ignores\nany supplied amount. Tax comes from the line's resolved tax code, never from a\nstored rate. See the [commercial money model](money-model.md).\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\nmigration no longer adds an editor-state column. An `invoice_editor_state_json`\ncolumn that an earlier version added is left in place and unused. No schema\nmutation 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"
|
|
21464
22039
|
},
|
|
22040
|
+
{
|
|
22041
|
+
"path": "agents/connected-payments.md",
|
|
22042
|
+
"module": "connected-payments",
|
|
22043
|
+
"content": "# Tenant-owned connected payment accounts (#3831)\n\nA tenant (for example a co-op) is paid into its own Stripe Connect **Standard**\naccount. Checkouts are direct charges on that account, the platform's share is\nan application fee, and the platform never holds the tenant's money. Source:\n`src/connected-payments.ts`, `src/models/TenantPaymentAccount.ts`,\n`src/models/TenantPaymentWebhookEvent.ts`.\n\n## Model\n\n`TenantPaymentAccount { tenantId, backendId, environment, providerAccountId,\nstatus, chargesEnabled, detailsSubmitted, payoutsEnabled, capabilitiesJson,\ncapabilitiesObservedAt, feePolicyRef, disabledAt, disabledReason }`.\n\n- Tenant-scoped (`required`), native UUID tenant ids. Two unique indexes:\n `(tenantId, backendId, environment)` and\n `(backendId, environment, providerAccountId)`, so the database refuses a\n second owner of a provider account even if a command check were bypassed.\n- `status`: `pending` (details not submitted), `restricted` (details\n submitted, charges or payouts off), `enabled` (charges, details and payouts\n all on), `disabled` (operator; refreshes keep it off). Only `enabled` takes\n new charges.\n- Read-only generated surface (REST, MCP and CLI: `list`/`get` and read methods only); never deleted. `save()` refuses any write outside the\n commands, any change of tenant/backend/environment, and a provider account\n change outside the relink command.\n- Connected `Payment` rows and connected `PaymentIntent` creation/option\n writes need the commands' internal authority (`connected-payments-internal.ts`,\n per tenant), so no generated REST/MCP/CLI write can forge or advance them;\n connected payments and refunds are never deleted. A checkout's identity\n includes its environment (`PaymentIntent.connectedEnvironment`): the same\n `acct_` id exists in test and live.\n- `TenantPaymentWebhookEvent` (`_smrt_tenant_payment_webhook_events`, system\n table, unique `(backendId, environment, eventId)`) stores processed event ids;\n the SDK's dedupe is in memory only.\n- `PaymentIntent` and `Payment` gain server-managed `connectedAccountId` and\n `applicationFeeCents` (`Payment` also `refundOfPaymentId` and\n `connectedRefundState`), frozen once settled. A connected intent settles only\n with a Payment on the same account and fee, and its account, fee and payment\n options (which name the provider session settlement re-reads) are frozen\n from creation, so a generated update cannot repoint it. A connected\n `Payment` (charge or refund) keeps its kind, amounts, currency, provider\n references, account, fee and refunded payment from its first write, even\n while pending.\n\n## Commands\n\nAll take an explicit `tenantId` and check ownership against it, so a\nprivileged or system caller is refused the same way. There is no fallback to\nthe platform account.\n\n| Command | Does |\n|---|---|\n| `onboardTenantPaymentAccount` | Create the Standard account (per-tenant idempotency key) and store it; returns the existing one on retry. |\n| `createTenantOnboardingLink` | Single-use onboarding URL. A bearer secret: redirect, never log or store. |\n| `linkTenantPaymentAccount` | Link an existing account; `replace: true` relinks. Refused (`ACCOUNT_IN_USE`) when another tenant holds it or its `smrt_tenant_id` metadata names another tenant. An account whose metadata names no tenant needs `ownershipVerified: true` (the host proved control, for example Stripe OAuth). |\n| `refreshTenantPaymentAccount` | Snapshot from `getAccountCapabilities`, re-derive status. |\n| `setTenantPaymentAccountDisabled`, `setTenantPaymentFeePolicy` | Operator switches. |\n| `requireEnabledTenantPaymentAccount` | Throws `NO_ENABLED_ACCOUNT` unless enabled. |\n| `createConnectedCheckout` | Checkout on the tenant's account with `stripeAccount` + `applicationFeeAmount` (integer minor units from the caller's fee policy; `0` for none; below the amount). The intent (account, environment, fee, expiry) is recorded before the provider call and the option naming the provider session is added once, so a retry replays the same request (the expiry is part of the provider idempotency key) and recovers a lost answer; once the recorded expiry is under 31 minutes away the retry asks for a fresh session with a renewed expiry (the lost URL never reached the buyer). The requested amount and currency are recorded too: a retry with a different price, environment, account or fee is refused; a retry of a paid checkout returns it with `settled: true` (no URL; never charge again); a retry of an unpaid closed (expired, cancelled or lapsed) quote gets `QUOTE_CLOSED`; an explicit `expiresAt` too close to replay must be replaced by the caller. |\n| `handleConnectedPaymentWebhook` | See below. |\n| `refundConnectedPayment` | Refund on the tenant's account; `refundApplicationFee` is required; partial and full. The refund row is written (`requested`, holding its amount) before the provider is asked; only a provider-`succeeded` refund is journaled, `pending` waits for the refund update, `failed` (including a first request the SDK refused before sending) releases the amount; a re-sent request must repeat its `refundApplicationFee` (stored in the frozen `applicationFeeRefunded`). A retry with the same key returns a recorded result, or re-sends a lost request with the same provider idempotency key within 23 hours (older lost requests wait for the refund update). Once booked refunds cover the payment it is marked `REFUNDED` and its intent retired. |\n| `createStripeConnectGateway` | Platform `StripeAdapter` from smrt-secrets. |\n\n## Webhooks\n\n`handleConnectedPaymentWebhook({ db, gateway, environment, endpoint, payload,\nsignature, resolveSettlementAccounts })`:\n\n1. The SDK verifies the signature **before** routing, then maps the event's\n connected account to its `TenantPaymentAccount` (cross-tenant read under\n `withSystemContext`, the one reviewed cross-tenant path). Unknown accounts\n throw the SDK's `PaymentNotOwnedError`.\n2. The event's `livemode` must match `environment` (`ENVIRONMENT_MISMATCH`).\n3. A stored event id returns `{ duplicate: true }` with its first outcome (and the `application_fee` details, so a host can still book them).\n4. `account.updated` applies the snapshot unless an earlier second than the\n stored one (`stale_account_update`). `application_fee.*` (platform endpoint,\n `endpoint: 'platform'`) is routed to the tenant and reported, not booked.\n A payment event loads the quote **in the resolved tenant** and refuses\n (`QUOTE_NOT_OWNED`) unless it belongs to that tenant and account, then\n re-reads the Checkout Session from the provider, so delivery order never\n matters. A provider-confirmed checkout settles even when its notice\n arrives after the price lock (the session expires with the lock); a\n payment confirmed for a quote already expired or cancelled is reported\n `late_payment` and posts nothing. `account.updated` reloads the row and\n refuses an event for a provider account the row no longer holds; a\n concurrent operator write fails the revision-guarded save (retried by the\n provider). Refund updates (`charge.refund.updated`, `refund.*`) book or fail\n a pending refund recorded by `refundConnectedPayment` on that account;\n terminal states never move back. Refunds made by the command carry\n `metadata.smrt_refund_id` and are matched by it (an unknown one throws\n `REFUND_NOT_RECORDED`; an amount differing from the row throws\n `AMOUNT_MISMATCH`). Refunds made elsewhere are reported. Settlement records a `Payment` (deterministic id from the intent)\n through `recordPayment()` and marks the intent paid. `refunded` events are\n reported (`refund_reported`), never booked automatically.\n5. The event id is stored only after processing succeeds; a refusal throws,\n so respond non-2xx and nothing unprocessed is acknowledged.\n\n## Accounting\n\nThe direct charge arrives net of the fee, which the provider withheld at\nsource: `recordPayment()` debits cash `amount - fee`, debits\n`applicationFeeAccountId` `fee`, and credits receivables `amount`. The fee is\nan expense already paid, never a payable to the platform. A payment with a fee\nand no `applicationFeeAccountId` is refused. Refund journals reverse the\nrefunded amount; a refunded application fee is reported by\n`application_fee.refunded` for the host to book.\n\n## Operator setup\n\n- smrt-secrets (held by the installation tenant, `secretsTenantId`):\n `stripe-connect-<env>-secret-key` (`sk_`/`rk_` key of that environment),\n `stripe-connect-<env>-webhook-secret` (platform endpoint),\n `stripe-connect-<env>-connect-webhook-secret` (Connect endpoint). Values\n never appear in errors.\n- Stripe: one platform endpoint (`application_fee.created`,\n `application_fee.refunded`) and one Connect endpoint (`account.updated`,\n `checkout.session.completed`, `checkout.session.async_payment_succeeded`,\n `checkout.session.async_payment_failed`, `checkout.session.expired`,\n `charge.refunded`, `charge.refund.updated`), per environment.\n- Each tenant needs ledger accounts for cash, receivables and the platform fee\n expense, returned by `resolveSettlementAccounts`.\n\n## Known limits\n\nA `requested` refund the provider never made (a request lost in transport\nand not retried within 23 hours, or one the provider rejected — the SDK does\nnot expose the HTTP status, so a rejection cannot be told from an unreadable\nsuccess) holds its amount and books nothing until an operator resolves it.\nAutomatic reconciliation needs an SDK refund read and error status\n([sdk#1430](https://github.com/happyvertical/sdk/issues/1430)).\n\nRelinking strands open checkouts on the old account and refuses refunds of\nits payments (`ACCOUNT_NOT_OWNED`). Concurrent refunds with different keys are\ncapped by the provider, not locally. Payments confirmed after an intent\nexpired or was cancelled are reported `ignored` for an operator.\n"
|
|
22044
|
+
},
|
|
21465
22045
|
{
|
|
21466
22046
|
"path": "agents/historical-sales.md",
|
|
21467
22047
|
"module": "historical-sales",
|
|
@@ -21485,7 +22065,7 @@
|
|
|
21485
22065
|
{
|
|
21486
22066
|
"path": "agents/money-upgrade.md",
|
|
21487
22067
|
"module": "money-upgrade",
|
|
21488
|
-
"content": "# Money data upgrade\n\nCommerce has one money model. It does not read money data written by an\nearlier version, and it does not convert that data on the fly. It refuses it\nand names the upgrade command. The operator steps are in the CLI guide,\n[commerce money upgrade command](../../cli/agents/commerce-money-upgrade.md).\n\n## Detection\n\n`src/upgrade/detect.ts` owns detection. Four shapes count as old data:\n\n- **Old schema.** The database still has `tax_settings.money_model`,\n `tax_settings.money_migration_id`, `*_line_items.legacy_amount`, or the\n `commerce_money_migration_documents` table. This refuses the whole database.\n- **Documents without a tenant.** An Invoice or Contract row, header or line,\n with no tenant. The current model writes none, and the upgrade cannot convert\n one: assign it to a tenant or delete it. This refuses the whole database and\n keeps the old schema in place until it is resolved.\n- **Tenant without settings.** A tenant has Invoice or Contract rows but no\n `tax_settings` row. This refuses that tenant only.\n- **Old-format documents.** An issued Invoice or Contract without a money\n snapshot, or a line with neither a tax code nor a component snapshot. The\n current model never writes either, so these stay detectable after `db:migrate --drop-columns`\n removed the old columns. This refuses that tenant only.\n\n`assertCommerceMoneySchemaCurrent(db)` checks the schema.\n`assertTenantMoneyCurrent(db, tenantId)` checks the schema, then the tenant.\nOn PostgreSQL every cross-tenant scan runs on a connection of its own (opened\nfrom the database URL), in a transaction that raises `smrt.system_context`,\nso the caller's connection and row policies are never touched; concurrent\nchecks for one database and tenant share a single scan. Both cache a passing\nresult per database (keyed by its connection URL, so\nrequest executors share it) and tenant, so the check runs once per process.\nInvoice, Contract and line models also run the check from `initialize()` on\nevery row hydrated from the database, which covers unscoped reads and every\ncollection class, including generated STI subtype collections. A\ntenantless collection read still checks the schema, and a tenant's first\n`TaxSettings` row is refused while that tenant holds old documents. Every runtime money read and write, the money MCP tools, the tax\nsummary, and `ensureCommerceTenant()` call them first.\n\nA refusal is a `CommerceDataNeedsUpgradeError` with `code`\n`COMMERCE_DATA_NEEDS_UPGRADE`. Its message ends with the command, for example\n`Run smrt db:upgrade-commerce-money --tenant-id <id> to upgrade it.`\n`commerceMoneyUpgradeCommand(tenantId?)` builds that command text.\n\n`findCommerceMoneyUpgradeNeeds(db)` scans the whole database. It is\noperator-only: it ignores the caller's row permissions, so never wire it into\na principal-facing surface. It returns `oldSchema`, `tenantlessDocuments`\n(rows to assign or delete) and each tenant that needs the upgrade, with a\nreason (`legacy-marker`, `interrupted-migration`, `missing-settings`, or\n`old-format-documents`).\n`smrt db:status` uses it to report the `commerce-money-upgrade` error\nprecondition, so `smrt app doctor` reports `migration-required`.\n\n## Upgrade\n\n`upgradeCommerceMoney(db, { tenantId? })` in `src/upgrade/money-v2.ts` is the\none-shot conversion behind `smrt db:upgrade-commerce-money`. Run the normal\nschema migration first. Each tenant converts in its own transaction:\n\n1. Its tax settings row and standard codes are seeded.\n2. Each stored fractional rate maps to a code. A zero rate becomes `exempt`\n for an exempt customer, otherwise `out-of-scope`. Any other rate becomes a\n one-component `rate-<percent>` code, created once.\n3. Issued documents are backfilled from their stored values: line net, line\n tax, tax-code snapshot, `DocumentTax` rows, and a money snapshot. No stored\n total changes.\n4. Drafts are recomputed through the runtime from their prices. Only\n old-format lines (no tax code and no component snapshot) are mapped; a\n draft header keeps its amount type and rounding. The result lists each\n draft whose total changed.\n\nAn issued document whose stored lines do not add up exactly to its stored\nheader fails its tenant. So do an invalid stored rate, an invoice posted under\nthe earlier version (its revenue journal carries that version's provenance,\nwhich current posting and reversal refuse; void it and clear the link, or reset\nthe data), an existing `rate-<percent>` code or component that does not\nmean exactly that rate, and an edited standard `exempt` or `out-of-scope`\ncode. Nothing in that tenant changes, and the result lists\neach failing document. A tenant whose earlier\nmigration never finished also fails; roll it back with the version that started\nit, or reset its data. Converted tenants are skipped on a rerun.\n\nOnce no tenant needs the upgrade, the old columns and table are dropped. The\nupgrade leaves no mode flag. Read `failures` and `remaining` on the result; the\nCLI exits nonzero on either. New `rate-<percent>` components have no liability\naccount and are listed in `componentsWithoutLiabilityAccount`; posting falls\nback to the caller's `taxAccountId` until one is assigned.\n\nThe runtime never imports `money-v2.ts`. Delete the `upgrade/` directory, the\nCLI command, and the detection call sites together once no supported version\ncan have written old-format data.\n"
|
|
22068
|
+
"content": "# Money data upgrade\n\nCommerce has one money model. It does not read money data written by an\nearlier version, and it does not convert that data on the fly. It refuses it\nand names the upgrade command. The operator steps are in the CLI guide,\n[commerce money upgrade command](../../cli/agents/commerce-money-upgrade.md).\n\n## Detection\n\n`src/upgrade/detect.ts` owns detection. Four shapes count as old data:\n\n- **Old schema.** The database still has `tax_settings.money_model`,\n `tax_settings.money_migration_id`, `*_line_items.legacy_amount`, or the\n `commerce_money_migration_documents` table. This refuses the whole database.\n- **Documents without a tenant.** An Invoice or Contract row, header or line,\n with no tenant. The current model writes none, and the upgrade cannot convert\n one: assign it to a tenant or delete it. This refuses the whole database and\n keeps the old schema in place until it is resolved.\n- **Tenant without settings.** A tenant has Invoice or Contract rows but no\n `tax_settings` row. This refuses that tenant only.\n- **Old-format documents.** An issued Invoice or Contract without a money\n snapshot, or a line with neither a tax code nor a component snapshot. The\n current model never writes either, so these stay detectable after `db:migrate --drop-columns`\n removed the old columns. This refuses that tenant only.\n\n`assertCommerceMoneySchemaCurrent(db)` checks the schema.\n`assertTenantMoneyCurrent(db, tenantId)` checks the schema, then the tenant.\nOn PostgreSQL every cross-tenant scan runs on a connection of its own (opened\nfrom the database URL), in a transaction that raises `smrt.system_context`,\nso the caller's connection and row policies are never touched; concurrent\nchecks for one database and tenant share a single scan. Both cache a passing\nresult per database (keyed by its connection URL, so\nrequest executors share it) and tenant, so the check runs once per process.\nInvoice, Contract and line models also run the check from `initialize()` on\nevery row hydrated from the database, which covers unscoped reads and every\ncollection class, including generated STI subtype collections. A\ntenantless collection read still checks the schema, and a tenant's first\n`TaxSettings` row is refused while that tenant holds old documents. Every runtime money read and write, the money MCP tools, the tax\nsummary, and `ensureCommerceTenant()` call them first.\n\nA refusal is a `CommerceDataNeedsUpgradeError` with `code`\n`COMMERCE_DATA_NEEDS_UPGRADE`. Its message ends with the command, for example\n`Run smrt db:upgrade-commerce-money --tenant-id <id> to upgrade it.`\n`commerceMoneyUpgradeCommand(tenantId?)` builds that command text.\n\n`findCommerceMoneyUpgradeNeeds(db)` scans the whole database. It is\noperator-only: it ignores the caller's row permissions, so never wire it into\na principal-facing surface. It returns `oldSchema`, `tenantlessDocuments`\n(rows to assign or delete) and each tenant that needs the upgrade, with a\nreason (`legacy-marker`, `interrupted-migration`, `missing-settings`, or\n`old-format-documents`).\n`smrt db:status` uses it to report the `commerce-money-upgrade` error\nprecondition, so `smrt app doctor` reports `migration-required`.\n\n## Upgrade\n\n`upgradeCommerceMoney(db, { tenantId? })` in `src/upgrade/money-v2.ts` is the\none-shot conversion behind `smrt db:upgrade-commerce-money`. Run the normal\nschema migration first. Each tenant converts in its own transaction:\n\n1. Its tax settings row and standard codes are seeded.\n2. Each stored fractional rate maps to a code. A zero rate becomes `exempt`\n for an exempt customer, otherwise `out-of-scope`. Any other rate becomes a\n one-component `rate-<percent>` code, created once.\n3. Issued documents are backfilled from their stored values: line net, line\n tax, tax-code snapshot, `DocumentTax` rows, and a money snapshot. No stored\n total changes.\n4. Drafts are recomputed through the runtime from their prices. Only\n old-format lines (no tax code and no component snapshot) are mapped; a\n draft header keeps its amount type and rounding. The result lists each\n draft whose total changed.\n\nAn issued document whose stored lines do not add up exactly to its stored\nheader fails its tenant. So do an invalid stored rate, an invoice posted under\nthe earlier version (its revenue journal carries that version's provenance,\nwhich current posting and reversal refuse; void it and clear the link, or reset\nthe data), an existing `rate-<percent>` code or component that does not\nmean exactly that rate, and an edited standard `exempt` or `out-of-scope`\ncode. Nothing in that tenant changes, and the result lists\neach failing document. A tenant whose earlier\nmigration never finished also fails; roll it back with the version that started\nit, or reset its data. Converted tenants are skipped on a rerun.\n\nEvery run also stores `''` for each Contract whose `billing_schedule_json` is\nNULL (only that tenant's with `tenantId`) and reports the count in\n`billingSchedulesBackfilled` (#4155). The runtime already reads NULL as no\nschedule, so this is tidying, not a prerequisite.\n\nOnce no tenant needs the upgrade, the old columns and table are dropped. The\nupgrade leaves no mode flag. Read `failures` and `remaining` on the result; the\nCLI exits nonzero on either. New `rate-<percent>` components have no liability\naccount and are listed in `componentsWithoutLiabilityAccount`; posting falls\nback to the caller's `taxAccountId` until one is assigned.\n\nThe runtime never imports `money-v2.ts`. Delete the `upgrade/` directory, the\nCLI command, and the detection call sites together once no supported version\ncan have written old-format data.\n"
|
|
21489
22069
|
},
|
|
21490
22070
|
{
|
|
21491
22071
|
"path": "agents/contract-money.md",
|
|
@@ -21525,7 +22105,7 @@
|
|
|
21525
22105
|
{
|
|
21526
22106
|
"path": "agents/contract-billing-schedules.md",
|
|
21527
22107
|
"module": "contract-billing-schedules",
|
|
21528
|
-
"content": "# Contract milestone billing (#4074)\n\nUse the public Commerce `approveContractBillingSchedule`,\n`issueContractMilestoneInvoice`, `getContractBillingSchedule` and\n`allocateContractMilestones` exports for fixed-value milestone billing. These\ncommands own monetary capacity. The host owns construction scope, estimated cost,\napproval permissions, signed evidence, regional policy and UI.\n\nStart with an accepted, frozen customer `Order` or `Agreement`. The first schedule\nrequires `amendmentMinor: 0`, `expectedRevision: 0`, currency, explicit `basis:\n'net' | 'gross'`, `approvalEvidenceRef`, `taxPolicyEvidenceRef` and exact milestone\n`{ key, label, amountMinor }` entries. Their sum must equal the original Contract\nsubtotal for net basis or totalAmount for gross basis. There is no editable\nschedule override of the accepted Contract value.\n\nEach amendment supplies the current expected revision, a new request identity,\nan explicit signed `amendmentMinor`, approval evidence and the complete next\nschedule. Its ceiling is derived from the original frozen value plus all approved\namendments. Currency/basis cannot change. Already issued milestone keys, labels\nand amounts must remain unchanged. Prior revisions and original Contract money\nremain immutable. Unissued milestones can be rescheduled through an explicitly\napproved revision. Net capacity is not a gross ceiling: each invoice's tax must\nalso be explicitly approved under the versioned policy.\n\n`allocateContractMilestones(totalMinor, weights)` accepts positive integer\nrelative weights. All nonfinal shares truncate exact rational arithmetic; the\nfinal entry receives the exact remainder. For example 100 with weights [1,1,1]\nreturns [33,33,34]. Zero amounts are allowed; negative/unsafe money and nonpositive\nweights reject. The helper chooses no tax policy or commercial percentages.\n\nPrepare a normal canonical draft Invoice and real line items through the public\nCommerce collections. Draft preparation consumes no capacity; an unissued draft may be cancelled\nwithout a claim. Then call:\n\n```ts\nconst claim = await issueContractMilestoneInvoice({\n db, tenantId, contractId, requestId: 'issue:start:v1',\n revision: 1, milestoneKey: 'start', invoiceId,\n approvedMoney: { netMinor: 4000, taxMinor: 200, grossMinor: 4200 },\n invoiceApprovalEvidenceRef: 'approved-invoice:123',\n});\n```\n\nThe command checks same tenant/customer/currency/Contract, derives current line\nmoney, verifies the exact approved net/tax/gross and schedule amount, freezes the\ninvoice and records the claim atomically. Tax-policy evidence is a host-owned\nreviewed version reference, not an instruction to infer tax rates. Preserve the\napproval's policy and original evidence in the host. Ordinary issue against a\nscheduled Contract is refused, including direct creation in a non-draft state.\nExisting issued contract invoices prevent adopting a schedule retroactively.\n\nThe explicit capacity policy is `issued-consumed`: cancellation, credits,\nwrite-offs, payments, retention and refunds never reopen a claimed milestone.\nCredits correct the canonical receivable and settlement continues through existing\nPayment/PaymentAllocation facilities. Unapplied payments remain the deposit cash\nauthority. A replacement billing entitlement requires an explicit approved\namendment; this API does not infer that a credit authorizes re-billing. Applications\nmust explain that policy rather than present a hidden re-bill action. Issued\nclaims remain readable after later amendments and cancellations.\n\nEvery mutation requires the active tenant context and uses the tenant money\nserialization barrier on the exact parent transaction executor. Identical request\nreplay returns the original evidence; a changed payload, stale revision or second\nclaim rejects. A parent rollback reverses both invoice issue and schedule claim.\nA detached read DTO cannot modify persisted state. Ordinary Contract writes and\ndeletes cannot erase schedule history. Host authorization precedes these commands;\nnone is exposed as an unauthenticated generated mutation.\n"
|
|
22108
|
+
"content": "# Contract milestone billing (#4074)\n\nUse the public Commerce `approveContractBillingSchedule`,\n`issueContractMilestoneInvoice`, `getContractBillingSchedule` and\n`allocateContractMilestones` exports for fixed-value milestone billing. These\ncommands own monetary capacity. The host owns construction scope, estimated cost,\napproval permissions, signed evidence, regional policy and UI.\n\nStart with an accepted, frozen customer `Order` or `Agreement`. The first schedule\nrequires `amendmentMinor: 0`, `expectedRevision: 0`, currency, explicit `basis:\n'net' | 'gross'`, `approvalEvidenceRef`, `taxPolicyEvidenceRef` and exact milestone\n`{ key, label, amountMinor }` entries. Their sum must equal the original Contract\nsubtotal for net basis or totalAmount for gross basis. There is no editable\nschedule override of the accepted Contract value.\n\nEach amendment supplies the current expected revision, a new request identity,\nan explicit signed `amendmentMinor`, approval evidence and the complete next\nschedule. Its ceiling is derived from the original frozen value plus all approved\namendments. Currency/basis cannot change. Already issued milestone keys, labels\nand amounts must remain unchanged. Prior revisions and original Contract money\nremain immutable. Unissued milestones can be rescheduled through an explicitly\napproved revision. Net capacity is not a gross ceiling: each invoice's tax must\nalso be explicitly approved under the versioned policy.\n\n`allocateContractMilestones(totalMinor, weights)` accepts positive integer\nrelative weights. All nonfinal shares truncate exact rational arithmetic; the\nfinal entry receives the exact remainder. For example 100 with weights [1,1,1]\nreturns [33,33,34]. Zero amounts are allowed; negative/unsafe money and nonpositive\nweights reject. The helper chooses no tax policy or commercial percentages.\n\nPrepare a normal canonical draft Invoice and real line items through the public\nCommerce collections. Draft preparation consumes no capacity; an unissued draft may be cancelled\nwithout a claim. Then call:\n\n```ts\nconst claim = await issueContractMilestoneInvoice({\n db, tenantId, contractId, requestId: 'issue:start:v1',\n revision: 1, milestoneKey: 'start', invoiceId,\n approvedMoney: { netMinor: 4000, taxMinor: 200, grossMinor: 4200 },\n invoiceApprovalEvidenceRef: 'approved-invoice:123',\n});\n```\n\nThe command checks same tenant/customer/currency/Contract, derives current line\nmoney, verifies the exact approved net/tax/gross and schedule amount, freezes the\ninvoice and records the claim atomically. Tax-policy evidence is a host-owned\nreviewed version reference, not an instruction to infer tax rates. Preserve the\napproval's policy and original evidence in the host. Ordinary issue against a\nscheduled Contract is refused, including direct creation in a non-draft state.\nExisting issued contract invoices prevent adopting a schedule retroactively.\n\nThe explicit capacity policy is `issued-consumed`: cancellation, credits,\nwrite-offs, payments, retention and refunds never reopen a claimed milestone.\nCredits correct the canonical receivable and settlement continues through existing\nPayment/PaymentAllocation facilities. Unapplied payments remain the deposit cash\nauthority. A replacement billing entitlement requires an explicit approved\namendment; this API does not infer that a credit authorizes re-billing. Applications\nmust explain that policy rather than present a hidden re-bill action. Issued\nclaims remain readable after later amendments and cancellations.\n\nEvery mutation requires the active tenant context and uses the tenant money\nserialization barrier on the exact parent transaction executor. Identical request\nreplay returns the original evidence; a changed payload, stale revision or second\nclaim rejects. A parent rollback reverses both invoice issue and schedule claim.\nA detached read DTO cannot modify persisted state. Ordinary Contract writes and\ndeletes cannot erase schedule history. Host authorization precedes these commands;\nnone is exposed as an unauthenticated generated mutation.\n\n`billingScheduleJson` is `''` when a Contract has no schedule, and the\ncolumn defaults to `''`. A row written before that default holds NULL, which\nhydrates and saves as `''`, so an ordinary save of such a Contract succeeds;\nany other change to the field through `save()` is still refused (#4155).\n`smrt db:upgrade-commerce-money` stores `''` for those NULL rows.\n"
|
|
21529
22109
|
},
|
|
21530
22110
|
{
|
|
21531
22111
|
"path": "agents/customer-funds.md",
|
|
@@ -22054,6 +22634,21 @@
|
|
|
22054
22634
|
},
|
|
22055
22635
|
"transactionId": {
|
|
22056
22636
|
"visibility": "hidden"
|
|
22637
|
+
},
|
|
22638
|
+
"connectedAccountId": {
|
|
22639
|
+
"visibility": "hidden"
|
|
22640
|
+
},
|
|
22641
|
+
"applicationFeeCents": {
|
|
22642
|
+
"visibility": "hidden"
|
|
22643
|
+
},
|
|
22644
|
+
"refundOfPaymentId": {
|
|
22645
|
+
"visibility": "hidden"
|
|
22646
|
+
},
|
|
22647
|
+
"connectedRefundState": {
|
|
22648
|
+
"visibility": "hidden"
|
|
22649
|
+
},
|
|
22650
|
+
"applicationFeeRefunded": {
|
|
22651
|
+
"visibility": "hidden"
|
|
22057
22652
|
}
|
|
22058
22653
|
}
|
|
22059
22654
|
},
|