@happyvertical/smrt-commerce 0.55.24 → 0.56.0

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.
@@ -3,19 +3,20 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-commerce",
6
- "packageVersion": "0.55.24",
6
+ "packageVersion": "0.56.0",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "5fecd03ae3b2033ab017298d7beef3d4d38b2069d60f169e372c1f7e14ca8338",
11
- "packageJson": "ba52e7d3b68131242e425cd105318d8ea47f9371d464e56c7c633ed03ad89719",
12
- "agents": "1ec9ceb9d4380f1f239051a6726afbb2afe31ce0485498b8e40afc28aeb76356",
10
+ "manifest": "7b16744246ab3b23324644c7ceabbbebc4bea86c068ef7d89fe22976f6afde98",
11
+ "packageJson": "b5f10112111ac6b147dc07796d44b84372ea1f3766bae49b6b127da3bcf8f027",
12
+ "agents": "856b53999561b37edadaf7510d3eeadd56c95e5c5db092e255e603c831aa52a9",
13
13
  "moduleDoc:agents/customer-vendor-ui.md": "744fb88c23ad05e7fd953edc2598e7c13f15f0ffd69c9cba70dd3e0668ae17f8",
14
+ "moduleDoc:README.md": "eee4fdd5600e46b152bb5a3ae09fcb4256a9e9ebbde2000cfea90f060ed8914e",
14
15
  "moduleDoc:agents/invoice-quantity.md": "bc3b617d4fb8cc87511763b281283107557ba64795e64522193ef967a3715d32",
15
16
  "moduleDoc:agents/payment-allocations.md": "46c0b921f0e40060cd1caec2dcefb01ed6612e31e4bf220a1a0b61e4e2a58212",
16
17
  "moduleDoc:agents/commercial-ui.md": "308729a481c82903d06837e7052c04509a92cf17d17345f71dce88d8d42d0431",
17
- "moduleDoc:agents/quote-ui.md": "514568e4e7815c47113627ac044a1f31a6933377b3f6c70f5d54b2508d94200c",
18
- "moduleDoc:agents/purchasing-ui.md": "c7d633c5ba45078c51f4066f68d90112cd748ac089ac34d209f9bf025739589f",
18
+ "moduleDoc:agents/quote-ui.md": "9d1faaa12f39e330294693f5309d9766c593dc0f7ced9e8b1dcaf5c81b066205",
19
+ "moduleDoc:agents/purchasing-ui.md": "d2b413009d466f3320934adf1e30ffbb7278138b859df69635e7c1f77efbbd1c",
19
20
  "moduleDoc:agents/cost-billing.md": "e60e6b7285962464e9bbc35ce259f7e99a81f6c9c39ae5855cdd0a34093d8d47"
20
21
  },
21
22
  "exports": [
@@ -3815,6 +3816,84 @@
3815
3816
  "tags": [],
3816
3817
  "risks": []
3817
3818
  },
3819
+ {
3820
+ "name": "PurchaseContractCollection",
3821
+ "qualifiedName": "@happyvertical/smrt-commerce:PurchaseContractCollection",
3822
+ "collection": "contracts",
3823
+ "description": "Read supplier-side contracts across every purchase STI subtype.",
3824
+ "tableName": "contracts",
3825
+ "packageName": "@happyvertical/smrt-commerce",
3826
+ "extends": "SmrtCollection",
3827
+ "fields": [],
3828
+ "relationships": [],
3829
+ "methods": [
3830
+ "findByVendor"
3831
+ ],
3832
+ "methodSignatures": [
3833
+ {
3834
+ "name": "findByVendor",
3835
+ "async": true,
3836
+ "params": [
3837
+ "vendorId: string"
3838
+ ],
3839
+ "returns": "Promise<PurchaseContract[]>"
3840
+ }
3841
+ ],
3842
+ "surfaces": [],
3843
+ "withheldSurfaces": [
3844
+ {
3845
+ "kind": "api",
3846
+ "operation": "findByVendor",
3847
+ "code": "excluded",
3848
+ "reason": "listed in api.exclude",
3849
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContractCollection"
3850
+ }
3851
+ ],
3852
+ "relationshipFeatures": [
3853
+ "uuidColumns"
3854
+ ],
3855
+ "tags": [],
3856
+ "risks": []
3857
+ },
3858
+ {
3859
+ "name": "SalesContractCollection",
3860
+ "qualifiedName": "@happyvertical/smrt-commerce:SalesContractCollection",
3861
+ "collection": "contracts",
3862
+ "description": "Read customer-side contracts across every sales STI subtype.",
3863
+ "tableName": "contracts",
3864
+ "packageName": "@happyvertical/smrt-commerce",
3865
+ "extends": "SmrtCollection",
3866
+ "fields": [],
3867
+ "relationships": [],
3868
+ "methods": [
3869
+ "findByCustomer"
3870
+ ],
3871
+ "methodSignatures": [
3872
+ {
3873
+ "name": "findByCustomer",
3874
+ "async": true,
3875
+ "params": [
3876
+ "customerId: string"
3877
+ ],
3878
+ "returns": "Promise<SalesContract[]>"
3879
+ }
3880
+ ],
3881
+ "surfaces": [],
3882
+ "withheldSurfaces": [
3883
+ {
3884
+ "kind": "api",
3885
+ "operation": "findByCustomer",
3886
+ "code": "excluded",
3887
+ "reason": "listed in api.exclude",
3888
+ "objectName": "@happyvertical/smrt-commerce:SalesContractCollection"
3889
+ }
3890
+ ],
3891
+ "relationshipFeatures": [
3892
+ "uuidColumns"
3893
+ ],
3894
+ "tags": [],
3895
+ "risks": []
3896
+ },
3818
3897
  {
3819
3898
  "name": "VendorCollection",
3820
3899
  "qualifiedName": "@happyvertical/smrt-commerce:VendorCollection",
@@ -4075,22 +4154,6 @@
4075
4154
  "columnType": "TEXT",
4076
4155
  "description": "Where the order or agreement stands, for example draft, sent, accepted or completed."
4077
4156
  },
4078
- {
4079
- "name": "customerId",
4080
- "type": "foreignKey",
4081
- "required": false,
4082
- "related": "Customer",
4083
- "columnType": "UUID",
4084
- "description": "The customer a sales order or agreement is for."
4085
- },
4086
- {
4087
- "name": "vendorId",
4088
- "type": "foreignKey",
4089
- "required": false,
4090
- "related": "Vendor",
4091
- "columnType": "UUID",
4092
- "description": "The vendor a purchase order is placed with."
4093
- },
4094
4157
  {
4095
4158
  "name": "subtotal",
4096
4159
  "type": "integer",
@@ -4177,22 +4240,7 @@
4177
4240
  "description": "Where it came from, for example your shop, a marketplace or a phone call."
4178
4241
  }
4179
4242
  ],
4180
- "relationships": [
4181
- {
4182
- "name": "customerId",
4183
- "type": "foreignKey",
4184
- "required": false,
4185
- "related": "Customer",
4186
- "columnType": "UUID"
4187
- },
4188
- {
4189
- "name": "vendorId",
4190
- "type": "foreignKey",
4191
- "required": false,
4192
- "related": "Vendor",
4193
- "columnType": "UUID"
4194
- }
4195
- ],
4243
+ "relationships": [],
4196
4244
  "methods": [
4197
4245
  "initialize",
4198
4246
  "isAccepted",
@@ -4304,64 +4352,671 @@
4304
4352
  "objectName": "@happyvertical/smrt-commerce:Contract"
4305
4353
  },
4306
4354
  {
4307
- "kind": "cli",
4308
- "name": "contract_update",
4309
- "operation": "update",
4310
- "objectName": "@happyvertical/smrt-commerce:Contract"
4355
+ "kind": "cli",
4356
+ "name": "contract_update",
4357
+ "operation": "update",
4358
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4359
+ },
4360
+ {
4361
+ "kind": "cli",
4362
+ "name": "contract_delete",
4363
+ "operation": "delete",
4364
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4365
+ },
4366
+ {
4367
+ "kind": "cli",
4368
+ "name": "contract_isDraft",
4369
+ "operation": "isDraft",
4370
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4371
+ },
4372
+ {
4373
+ "kind": "cli",
4374
+ "name": "contract_isAccepted",
4375
+ "operation": "isAccepted",
4376
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4377
+ },
4378
+ {
4379
+ "kind": "cli",
4380
+ "name": "contract_isCompleted",
4381
+ "operation": "isCompleted",
4382
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4383
+ },
4384
+ {
4385
+ "kind": "cli",
4386
+ "name": "contract_isExpired",
4387
+ "operation": "isExpired",
4388
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4389
+ },
4390
+ {
4391
+ "kind": "cli",
4392
+ "name": "contract_isOverdue",
4393
+ "operation": "isOverdue",
4394
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4395
+ },
4396
+ {
4397
+ "kind": "cli",
4398
+ "name": "contract_recalculateTotals",
4399
+ "operation": "recalculateTotals",
4400
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4401
+ },
4402
+ {
4403
+ "kind": "mcp",
4404
+ "name": "contract_list",
4405
+ "operation": "list",
4406
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4407
+ },
4408
+ {
4409
+ "kind": "mcp",
4410
+ "name": "contract_get",
4411
+ "operation": "get",
4412
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4413
+ }
4414
+ ],
4415
+ "withheldSurfaces": [
4416
+ {
4417
+ "kind": "api",
4418
+ "operation": "initialize",
4419
+ "code": "lifecycle-method",
4420
+ "reason": "`initialize` is a framework lifecycle method, not a distinct operation",
4421
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4422
+ },
4423
+ {
4424
+ "kind": "api",
4425
+ "operation": "isAccepted",
4426
+ "code": "not-included",
4427
+ "reason": "not listed in api.include",
4428
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4429
+ },
4430
+ {
4431
+ "kind": "api",
4432
+ "operation": "isCompleted",
4433
+ "code": "not-included",
4434
+ "reason": "not listed in api.include",
4435
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4436
+ },
4437
+ {
4438
+ "kind": "api",
4439
+ "operation": "isDraft",
4440
+ "code": "not-included",
4441
+ "reason": "not listed in api.include",
4442
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4443
+ },
4444
+ {
4445
+ "kind": "api",
4446
+ "operation": "isExpired",
4447
+ "code": "not-included",
4448
+ "reason": "not listed in api.include",
4449
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4450
+ },
4451
+ {
4452
+ "kind": "api",
4453
+ "operation": "isOverdue",
4454
+ "code": "not-included",
4455
+ "reason": "not listed in api.include",
4456
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4457
+ },
4458
+ {
4459
+ "kind": "api",
4460
+ "operation": "recalculateTotals",
4461
+ "code": "not-included",
4462
+ "reason": "not listed in api.include",
4463
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4464
+ },
4465
+ {
4466
+ "kind": "api",
4467
+ "operation": "save",
4468
+ "code": "lifecycle-method",
4469
+ "reason": "`save` is a framework lifecycle method, not a distinct operation",
4470
+ "objectName": "@happyvertical/smrt-commerce:Contract"
4471
+ }
4472
+ ],
4473
+ "relationshipFeatures": [
4474
+ "uuidColumns"
4475
+ ],
4476
+ "tags": [],
4477
+ "risks": []
4478
+ },
4479
+ {
4480
+ "name": "SalesContract",
4481
+ "qualifiedName": "@happyvertical/smrt-commerce:SalesContract",
4482
+ "collection": "contracts",
4483
+ "description": "Customer-side contracts, including orders, estimates, and licenses.",
4484
+ "tableName": "contracts",
4485
+ "packageName": "@happyvertical/smrt-commerce",
4486
+ "extends": "Contract",
4487
+ "fields": [
4488
+ {
4489
+ "name": "created_at",
4490
+ "type": "datetime",
4491
+ "required": false,
4492
+ "columnType": "TIMESTAMP"
4493
+ },
4494
+ {
4495
+ "name": "updated_at",
4496
+ "type": "datetime",
4497
+ "required": false,
4498
+ "columnType": "TIMESTAMP"
4499
+ },
4500
+ {
4501
+ "name": "tenantId",
4502
+ "type": "text",
4503
+ "required": false,
4504
+ "columnType": "UUID"
4505
+ },
4506
+ {
4507
+ "name": "contractType",
4508
+ "type": "text",
4509
+ "required": false,
4510
+ "columnType": "TEXT",
4511
+ "description": "The kind of contract: order, estimate, lease, agreement and so on."
4512
+ },
4513
+ {
4514
+ "name": "status",
4515
+ "type": "text",
4516
+ "required": false,
4517
+ "columnType": "TEXT",
4518
+ "description": "Where the order or agreement stands, for example draft, sent, accepted or completed."
4519
+ },
4520
+ {
4521
+ "name": "subtotal",
4522
+ "type": "integer",
4523
+ "required": false,
4524
+ "columnType": "INTEGER",
4525
+ "default": 0,
4526
+ "description": "The amount before tax."
4527
+ },
4528
+ {
4529
+ "name": "taxAmount",
4530
+ "type": "integer",
4531
+ "required": false,
4532
+ "columnType": "INTEGER",
4533
+ "default": 0,
4534
+ "description": "The tax charged."
4535
+ },
4536
+ {
4537
+ "name": "totalAmount",
4538
+ "type": "integer",
4539
+ "required": false,
4540
+ "columnType": "INTEGER",
4541
+ "default": 0,
4542
+ "description": "The full amount, tax included."
4543
+ },
4544
+ {
4545
+ "name": "currency",
4546
+ "type": "text",
4547
+ "required": false,
4548
+ "columnType": "TEXT",
4549
+ "default": "USD",
4550
+ "description": "The currency of the amounts."
4551
+ },
4552
+ {
4553
+ "name": "issueDate",
4554
+ "type": "datetime",
4555
+ "required": false,
4556
+ "columnType": "TIMESTAMP",
4557
+ "description": "The date it was issued or placed."
4558
+ },
4559
+ {
4560
+ "name": "dueDate",
4561
+ "type": "datetime",
4562
+ "required": false,
4563
+ "columnType": "TIMESTAMP",
4564
+ "description": "The date payment or delivery is due."
4565
+ },
4566
+ {
4567
+ "name": "expiryDate",
4568
+ "type": "datetime",
4569
+ "required": false,
4570
+ "columnType": "TIMESTAMP",
4571
+ "description": "The last day the price or offer is good for."
4572
+ },
4573
+ {
4574
+ "name": "reference",
4575
+ "type": "text",
4576
+ "required": false,
4577
+ "columnType": "TEXT",
4578
+ "default": "",
4579
+ "description": "Your own number or note for finding this later, such as a purchase order or quote number."
4580
+ },
4581
+ {
4582
+ "name": "notes",
4583
+ "type": "text",
4584
+ "required": false,
4585
+ "columnType": "TEXT",
4586
+ "default": "",
4587
+ "description": "Anything worth remembering about this."
4588
+ },
4589
+ {
4590
+ "name": "terms",
4591
+ "type": "text",
4592
+ "required": false,
4593
+ "columnType": "TEXT",
4594
+ "default": "",
4595
+ "description": "The conditions agreed, such as deposits, payment or delivery conditions."
4596
+ },
4597
+ {
4598
+ "name": "channelId",
4599
+ "type": "text",
4600
+ "required": false,
4601
+ "columnType": "TEXT",
4602
+ "default": "",
4603
+ "description": "Where it came from, for example your shop, a marketplace or a phone call."
4604
+ },
4605
+ {
4606
+ "name": "customerId",
4607
+ "type": "foreignKey",
4608
+ "required": false,
4609
+ "related": "Customer",
4610
+ "columnType": "UUID",
4611
+ "description": "The customer a sales order or agreement is for."
4612
+ }
4613
+ ],
4614
+ "relationships": [
4615
+ {
4616
+ "name": "customerId",
4617
+ "type": "foreignKey",
4618
+ "required": false,
4619
+ "related": "Customer",
4620
+ "columnType": "UUID"
4621
+ }
4622
+ ],
4623
+ "methods": [
4624
+ "initialize",
4625
+ "isAccepted",
4626
+ "isCompleted",
4627
+ "isDraft",
4628
+ "isExpired",
4629
+ "isOverdue",
4630
+ "recalculateTotals",
4631
+ "save"
4632
+ ],
4633
+ "methodSignatures": [
4634
+ {
4635
+ "name": "initialize",
4636
+ "async": true,
4637
+ "returns": "Promise<this>"
4638
+ },
4639
+ {
4640
+ "name": "isAccepted",
4641
+ "returns": "boolean"
4642
+ },
4643
+ {
4644
+ "name": "isCompleted",
4645
+ "returns": "boolean"
4646
+ },
4647
+ {
4648
+ "name": "isDraft",
4649
+ "returns": "boolean"
4650
+ },
4651
+ {
4652
+ "name": "isExpired",
4653
+ "returns": "boolean"
4654
+ },
4655
+ {
4656
+ "name": "isOverdue",
4657
+ "returns": "boolean"
4658
+ },
4659
+ {
4660
+ "name": "recalculateTotals",
4661
+ "async": true,
4662
+ "returns": "Promise<void>"
4663
+ },
4664
+ {
4665
+ "name": "save",
4666
+ "async": true,
4667
+ "returns": "Promise<this>"
4668
+ }
4669
+ ],
4670
+ "tenant": {
4671
+ "scoped": true,
4672
+ "mode": "optional",
4673
+ "field": "tenantId"
4674
+ },
4675
+ "tableStrategy": "sti",
4676
+ "surfaces": [
4677
+ {
4678
+ "kind": "api",
4679
+ "name": "contracts.list",
4680
+ "operation": "list",
4681
+ "objectName": "@happyvertical/smrt-commerce:SalesContract",
4682
+ "path": "/contracts",
4683
+ "method": "GET"
4684
+ },
4685
+ {
4686
+ "kind": "api",
4687
+ "name": "contracts.get",
4688
+ "operation": "get",
4689
+ "objectName": "@happyvertical/smrt-commerce:SalesContract",
4690
+ "path": "/contracts/[id]",
4691
+ "method": "GET"
4692
+ },
4693
+ {
4694
+ "kind": "cli",
4695
+ "name": "salescontract_list",
4696
+ "operation": "list",
4697
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
4698
+ },
4699
+ {
4700
+ "kind": "cli",
4701
+ "name": "salescontract_get",
4702
+ "operation": "get",
4703
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
4704
+ },
4705
+ {
4706
+ "kind": "mcp",
4707
+ "name": "salescontract_list",
4708
+ "operation": "list",
4709
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
4710
+ },
4711
+ {
4712
+ "kind": "mcp",
4713
+ "name": "salescontract_get",
4714
+ "operation": "get",
4715
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
4716
+ }
4717
+ ],
4718
+ "withheldSurfaces": [
4719
+ {
4720
+ "kind": "api",
4721
+ "operation": "initialize",
4722
+ "code": "lifecycle-method",
4723
+ "reason": "`initialize` is a framework lifecycle method, not a distinct operation",
4724
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
4725
+ },
4726
+ {
4727
+ "kind": "api",
4728
+ "operation": "isAccepted",
4729
+ "code": "excluded",
4730
+ "reason": "listed in api.exclude",
4731
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
4732
+ },
4733
+ {
4734
+ "kind": "api",
4735
+ "operation": "isCompleted",
4736
+ "code": "excluded",
4737
+ "reason": "listed in api.exclude",
4738
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
4739
+ },
4740
+ {
4741
+ "kind": "api",
4742
+ "operation": "isDraft",
4743
+ "code": "excluded",
4744
+ "reason": "listed in api.exclude",
4745
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
4746
+ },
4747
+ {
4748
+ "kind": "api",
4749
+ "operation": "isExpired",
4750
+ "code": "excluded",
4751
+ "reason": "listed in api.exclude",
4752
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
4753
+ },
4754
+ {
4755
+ "kind": "api",
4756
+ "operation": "isOverdue",
4757
+ "code": "excluded",
4758
+ "reason": "listed in api.exclude",
4759
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
4760
+ },
4761
+ {
4762
+ "kind": "api",
4763
+ "operation": "recalculateTotals",
4764
+ "code": "excluded",
4765
+ "reason": "listed in api.exclude",
4766
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
4767
+ },
4768
+ {
4769
+ "kind": "api",
4770
+ "operation": "save",
4771
+ "code": "lifecycle-method",
4772
+ "reason": "`save` is a framework lifecycle method, not a distinct operation",
4773
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
4774
+ }
4775
+ ],
4776
+ "relationshipFeatures": [
4777
+ "foreignKey",
4778
+ "uuidColumns"
4779
+ ],
4780
+ "tags": [],
4781
+ "risks": []
4782
+ },
4783
+ {
4784
+ "name": "PurchaseContract",
4785
+ "qualifiedName": "@happyvertical/smrt-commerce:PurchaseContract",
4786
+ "collection": "contracts",
4787
+ "description": "Supplier-side contracts, including purchase and production orders.",
4788
+ "tableName": "contracts",
4789
+ "packageName": "@happyvertical/smrt-commerce",
4790
+ "extends": "Contract",
4791
+ "fields": [
4792
+ {
4793
+ "name": "created_at",
4794
+ "type": "datetime",
4795
+ "required": false,
4796
+ "columnType": "TIMESTAMP"
4797
+ },
4798
+ {
4799
+ "name": "updated_at",
4800
+ "type": "datetime",
4801
+ "required": false,
4802
+ "columnType": "TIMESTAMP"
4803
+ },
4804
+ {
4805
+ "name": "tenantId",
4806
+ "type": "text",
4807
+ "required": false,
4808
+ "columnType": "UUID"
4809
+ },
4810
+ {
4811
+ "name": "contractType",
4812
+ "type": "text",
4813
+ "required": false,
4814
+ "columnType": "TEXT",
4815
+ "description": "The kind of contract: order, estimate, lease, agreement and so on."
4816
+ },
4817
+ {
4818
+ "name": "status",
4819
+ "type": "text",
4820
+ "required": false,
4821
+ "columnType": "TEXT",
4822
+ "description": "Where the order or agreement stands, for example draft, sent, accepted or completed."
4823
+ },
4824
+ {
4825
+ "name": "subtotal",
4826
+ "type": "integer",
4827
+ "required": false,
4828
+ "columnType": "INTEGER",
4829
+ "default": 0,
4830
+ "description": "The amount before tax."
4831
+ },
4832
+ {
4833
+ "name": "taxAmount",
4834
+ "type": "integer",
4835
+ "required": false,
4836
+ "columnType": "INTEGER",
4837
+ "default": 0,
4838
+ "description": "The tax charged."
4839
+ },
4840
+ {
4841
+ "name": "totalAmount",
4842
+ "type": "integer",
4843
+ "required": false,
4844
+ "columnType": "INTEGER",
4845
+ "default": 0,
4846
+ "description": "The full amount, tax included."
4847
+ },
4848
+ {
4849
+ "name": "currency",
4850
+ "type": "text",
4851
+ "required": false,
4852
+ "columnType": "TEXT",
4853
+ "default": "USD",
4854
+ "description": "The currency of the amounts."
4855
+ },
4856
+ {
4857
+ "name": "issueDate",
4858
+ "type": "datetime",
4859
+ "required": false,
4860
+ "columnType": "TIMESTAMP",
4861
+ "description": "The date it was issued or placed."
4862
+ },
4863
+ {
4864
+ "name": "dueDate",
4865
+ "type": "datetime",
4866
+ "required": false,
4867
+ "columnType": "TIMESTAMP",
4868
+ "description": "The date payment or delivery is due."
4869
+ },
4870
+ {
4871
+ "name": "expiryDate",
4872
+ "type": "datetime",
4873
+ "required": false,
4874
+ "columnType": "TIMESTAMP",
4875
+ "description": "The last day the price or offer is good for."
4876
+ },
4877
+ {
4878
+ "name": "reference",
4879
+ "type": "text",
4880
+ "required": false,
4881
+ "columnType": "TEXT",
4882
+ "default": "",
4883
+ "description": "Your own number or note for finding this later, such as a purchase order or quote number."
4884
+ },
4885
+ {
4886
+ "name": "notes",
4887
+ "type": "text",
4888
+ "required": false,
4889
+ "columnType": "TEXT",
4890
+ "default": "",
4891
+ "description": "Anything worth remembering about this."
4892
+ },
4893
+ {
4894
+ "name": "terms",
4895
+ "type": "text",
4896
+ "required": false,
4897
+ "columnType": "TEXT",
4898
+ "default": "",
4899
+ "description": "The conditions agreed, such as deposits, payment or delivery conditions."
4900
+ },
4901
+ {
4902
+ "name": "channelId",
4903
+ "type": "text",
4904
+ "required": false,
4905
+ "columnType": "TEXT",
4906
+ "default": "",
4907
+ "description": "Where it came from, for example your shop, a marketplace or a phone call."
4908
+ },
4909
+ {
4910
+ "name": "vendorId",
4911
+ "type": "foreignKey",
4912
+ "required": false,
4913
+ "related": "Vendor",
4914
+ "columnType": "UUID",
4915
+ "description": "The vendor a purchase order is placed with."
4916
+ }
4917
+ ],
4918
+ "relationships": [
4919
+ {
4920
+ "name": "vendorId",
4921
+ "type": "foreignKey",
4922
+ "required": false,
4923
+ "related": "Vendor",
4924
+ "columnType": "UUID"
4925
+ }
4926
+ ],
4927
+ "methods": [
4928
+ "initialize",
4929
+ "isAccepted",
4930
+ "isCompleted",
4931
+ "isDraft",
4932
+ "isExpired",
4933
+ "isOverdue",
4934
+ "recalculateTotals",
4935
+ "save"
4936
+ ],
4937
+ "methodSignatures": [
4938
+ {
4939
+ "name": "initialize",
4940
+ "async": true,
4941
+ "returns": "Promise<this>"
4942
+ },
4943
+ {
4944
+ "name": "isAccepted",
4945
+ "returns": "boolean"
4946
+ },
4947
+ {
4948
+ "name": "isCompleted",
4949
+ "returns": "boolean"
4950
+ },
4951
+ {
4952
+ "name": "isDraft",
4953
+ "returns": "boolean"
4311
4954
  },
4312
4955
  {
4313
- "kind": "cli",
4314
- "name": "contract_delete",
4315
- "operation": "delete",
4316
- "objectName": "@happyvertical/smrt-commerce:Contract"
4956
+ "name": "isExpired",
4957
+ "returns": "boolean"
4317
4958
  },
4318
4959
  {
4319
- "kind": "cli",
4320
- "name": "contract_isDraft",
4321
- "operation": "isDraft",
4322
- "objectName": "@happyvertical/smrt-commerce:Contract"
4960
+ "name": "isOverdue",
4961
+ "returns": "boolean"
4323
4962
  },
4324
4963
  {
4325
- "kind": "cli",
4326
- "name": "contract_isAccepted",
4327
- "operation": "isAccepted",
4328
- "objectName": "@happyvertical/smrt-commerce:Contract"
4964
+ "name": "recalculateTotals",
4965
+ "async": true,
4966
+ "returns": "Promise<void>"
4329
4967
  },
4330
4968
  {
4331
- "kind": "cli",
4332
- "name": "contract_isCompleted",
4333
- "operation": "isCompleted",
4334
- "objectName": "@happyvertical/smrt-commerce:Contract"
4969
+ "name": "save",
4970
+ "async": true,
4971
+ "returns": "Promise<this>"
4972
+ }
4973
+ ],
4974
+ "tenant": {
4975
+ "scoped": true,
4976
+ "mode": "optional",
4977
+ "field": "tenantId"
4978
+ },
4979
+ "tableStrategy": "sti",
4980
+ "surfaces": [
4981
+ {
4982
+ "kind": "api",
4983
+ "name": "contracts.list",
4984
+ "operation": "list",
4985
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract",
4986
+ "path": "/contracts",
4987
+ "method": "GET"
4335
4988
  },
4336
4989
  {
4337
- "kind": "cli",
4338
- "name": "contract_isExpired",
4339
- "operation": "isExpired",
4340
- "objectName": "@happyvertical/smrt-commerce:Contract"
4990
+ "kind": "api",
4991
+ "name": "contracts.get",
4992
+ "operation": "get",
4993
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract",
4994
+ "path": "/contracts/[id]",
4995
+ "method": "GET"
4341
4996
  },
4342
4997
  {
4343
4998
  "kind": "cli",
4344
- "name": "contract_isOverdue",
4345
- "operation": "isOverdue",
4346
- "objectName": "@happyvertical/smrt-commerce:Contract"
4999
+ "name": "purchasecontract_list",
5000
+ "operation": "list",
5001
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
4347
5002
  },
4348
5003
  {
4349
5004
  "kind": "cli",
4350
- "name": "contract_recalculateTotals",
4351
- "operation": "recalculateTotals",
4352
- "objectName": "@happyvertical/smrt-commerce:Contract"
5005
+ "name": "purchasecontract_get",
5006
+ "operation": "get",
5007
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
4353
5008
  },
4354
5009
  {
4355
5010
  "kind": "mcp",
4356
- "name": "contract_list",
5011
+ "name": "purchasecontract_list",
4357
5012
  "operation": "list",
4358
- "objectName": "@happyvertical/smrt-commerce:Contract"
5013
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
4359
5014
  },
4360
5015
  {
4361
5016
  "kind": "mcp",
4362
- "name": "contract_get",
5017
+ "name": "purchasecontract_get",
4363
5018
  "operation": "get",
4364
- "objectName": "@happyvertical/smrt-commerce:Contract"
5019
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
4365
5020
  }
4366
5021
  ],
4367
5022
  "withheldSurfaces": [
@@ -4370,56 +5025,56 @@
4370
5025
  "operation": "initialize",
4371
5026
  "code": "lifecycle-method",
4372
5027
  "reason": "`initialize` is a framework lifecycle method, not a distinct operation",
4373
- "objectName": "@happyvertical/smrt-commerce:Contract"
5028
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
4374
5029
  },
4375
5030
  {
4376
5031
  "kind": "api",
4377
5032
  "operation": "isAccepted",
4378
- "code": "not-included",
4379
- "reason": "not listed in api.include",
4380
- "objectName": "@happyvertical/smrt-commerce:Contract"
5033
+ "code": "excluded",
5034
+ "reason": "listed in api.exclude",
5035
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
4381
5036
  },
4382
5037
  {
4383
5038
  "kind": "api",
4384
5039
  "operation": "isCompleted",
4385
- "code": "not-included",
4386
- "reason": "not listed in api.include",
4387
- "objectName": "@happyvertical/smrt-commerce:Contract"
5040
+ "code": "excluded",
5041
+ "reason": "listed in api.exclude",
5042
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
4388
5043
  },
4389
5044
  {
4390
5045
  "kind": "api",
4391
5046
  "operation": "isDraft",
4392
- "code": "not-included",
4393
- "reason": "not listed in api.include",
4394
- "objectName": "@happyvertical/smrt-commerce:Contract"
5047
+ "code": "excluded",
5048
+ "reason": "listed in api.exclude",
5049
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
4395
5050
  },
4396
5051
  {
4397
5052
  "kind": "api",
4398
5053
  "operation": "isExpired",
4399
- "code": "not-included",
4400
- "reason": "not listed in api.include",
4401
- "objectName": "@happyvertical/smrt-commerce:Contract"
5054
+ "code": "excluded",
5055
+ "reason": "listed in api.exclude",
5056
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
4402
5057
  },
4403
5058
  {
4404
5059
  "kind": "api",
4405
5060
  "operation": "isOverdue",
4406
- "code": "not-included",
4407
- "reason": "not listed in api.include",
4408
- "objectName": "@happyvertical/smrt-commerce:Contract"
5061
+ "code": "excluded",
5062
+ "reason": "listed in api.exclude",
5063
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
4409
5064
  },
4410
5065
  {
4411
5066
  "kind": "api",
4412
5067
  "operation": "recalculateTotals",
4413
- "code": "not-included",
4414
- "reason": "not listed in api.include",
4415
- "objectName": "@happyvertical/smrt-commerce:Contract"
5068
+ "code": "excluded",
5069
+ "reason": "listed in api.exclude",
5070
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
4416
5071
  },
4417
5072
  {
4418
5073
  "kind": "api",
4419
5074
  "operation": "save",
4420
5075
  "code": "lifecycle-method",
4421
5076
  "reason": "`save` is a framework lifecycle method, not a distinct operation",
4422
- "objectName": "@happyvertical/smrt-commerce:Contract"
5077
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
4423
5078
  }
4424
5079
  ],
4425
5080
  "relationshipFeatures": [
@@ -4436,7 +5091,7 @@
4436
5091
  "description": "Quote or proposal for a customer",
4437
5092
  "tableName": "contracts",
4438
5093
  "packageName": "@happyvertical/smrt-commerce",
4439
- "extends": "Contract",
5094
+ "extends": "SalesContract",
4440
5095
  "fields": [
4441
5096
  {
4442
5097
  "name": "created_at",
@@ -4460,8 +5115,7 @@
4460
5115
  "name": "contractType",
4461
5116
  "type": "text",
4462
5117
  "required": false,
4463
- "columnType": "TEXT",
4464
- "description": "The kind of contract: order, estimate, lease, agreement and so on."
5118
+ "columnType": "TEXT"
4465
5119
  },
4466
5120
  {
4467
5121
  "name": "status",
@@ -4470,22 +5124,6 @@
4470
5124
  "columnType": "TEXT",
4471
5125
  "description": "Where the order or agreement stands, for example draft, sent, accepted or completed."
4472
5126
  },
4473
- {
4474
- "name": "customerId",
4475
- "type": "foreignKey",
4476
- "required": false,
4477
- "related": "Customer",
4478
- "columnType": "UUID",
4479
- "description": "The customer a sales order or agreement is for."
4480
- },
4481
- {
4482
- "name": "vendorId",
4483
- "type": "foreignKey",
4484
- "required": false,
4485
- "related": "Vendor",
4486
- "columnType": "UUID",
4487
- "description": "The vendor a purchase order is placed with."
4488
- },
4489
5127
  {
4490
5128
  "name": "subtotal",
4491
5129
  "type": "integer",
@@ -4570,21 +5208,21 @@
4570
5208
  "columnType": "TEXT",
4571
5209
  "default": "",
4572
5210
  "description": "Where it came from, for example your shop, a marketplace or a phone call."
4573
- }
4574
- ],
4575
- "relationships": [
5211
+ },
4576
5212
  {
4577
5213
  "name": "customerId",
4578
5214
  "type": "foreignKey",
4579
- "required": false,
5215
+ "required": true,
4580
5216
  "related": "Customer",
4581
5217
  "columnType": "UUID"
4582
- },
5218
+ }
5219
+ ],
5220
+ "relationships": [
4583
5221
  {
4584
- "name": "vendorId",
5222
+ "name": "customerId",
4585
5223
  "type": "foreignKey",
4586
- "required": false,
4587
- "related": "Vendor",
5224
+ "required": true,
5225
+ "related": "Customer",
4588
5226
  "columnType": "UUID"
4589
5227
  }
4590
5228
  ],
@@ -4893,7 +5531,7 @@
4893
5531
  "description": "Customer purchase order",
4894
5532
  "tableName": "contracts",
4895
5533
  "packageName": "@happyvertical/smrt-commerce",
4896
- "extends": "Contract",
5534
+ "extends": "SalesContract",
4897
5535
  "fields": [
4898
5536
  {
4899
5537
  "name": "created_at",
@@ -4917,8 +5555,7 @@
4917
5555
  "name": "contractType",
4918
5556
  "type": "text",
4919
5557
  "required": false,
4920
- "columnType": "TEXT",
4921
- "description": "The kind of contract: order, estimate, lease, agreement and so on."
5558
+ "columnType": "TEXT"
4922
5559
  },
4923
5560
  {
4924
5561
  "name": "status",
@@ -4927,22 +5564,6 @@
4927
5564
  "columnType": "TEXT",
4928
5565
  "description": "Where the order or agreement stands, for example draft, sent, accepted or completed."
4929
5566
  },
4930
- {
4931
- "name": "customerId",
4932
- "type": "foreignKey",
4933
- "required": false,
4934
- "related": "Customer",
4935
- "columnType": "UUID",
4936
- "description": "The customer a sales order or agreement is for."
4937
- },
4938
- {
4939
- "name": "vendorId",
4940
- "type": "foreignKey",
4941
- "required": false,
4942
- "related": "Vendor",
4943
- "columnType": "UUID",
4944
- "description": "The vendor a purchase order is placed with."
4945
- },
4946
5567
  {
4947
5568
  "name": "subtotal",
4948
5569
  "type": "integer",
@@ -5027,21 +5648,21 @@
5027
5648
  "columnType": "TEXT",
5028
5649
  "default": "",
5029
5650
  "description": "Where it came from, for example your shop, a marketplace or a phone call."
5030
- }
5031
- ],
5032
- "relationships": [
5651
+ },
5033
5652
  {
5034
5653
  "name": "customerId",
5035
5654
  "type": "foreignKey",
5036
- "required": false,
5655
+ "required": true,
5037
5656
  "related": "Customer",
5038
5657
  "columnType": "UUID"
5039
- },
5658
+ }
5659
+ ],
5660
+ "relationships": [
5040
5661
  {
5041
- "name": "vendorId",
5662
+ "name": "customerId",
5042
5663
  "type": "foreignKey",
5043
- "required": false,
5044
- "related": "Vendor",
5664
+ "required": true,
5665
+ "related": "Customer",
5045
5666
  "columnType": "UUID"
5046
5667
  }
5047
5668
  ],
@@ -5350,7 +5971,7 @@
5350
5971
  "description": "Rental or lease agreement",
5351
5972
  "tableName": "contracts",
5352
5973
  "packageName": "@happyvertical/smrt-commerce",
5353
- "extends": "Contract",
5974
+ "extends": "SalesContract",
5354
5975
  "fields": [
5355
5976
  {
5356
5977
  "name": "created_at",
@@ -5374,8 +5995,7 @@
5374
5995
  "name": "contractType",
5375
5996
  "type": "text",
5376
5997
  "required": false,
5377
- "columnType": "TEXT",
5378
- "description": "The kind of contract: order, estimate, lease, agreement and so on."
5998
+ "columnType": "TEXT"
5379
5999
  },
5380
6000
  {
5381
6001
  "name": "status",
@@ -5384,22 +6004,6 @@
5384
6004
  "columnType": "TEXT",
5385
6005
  "description": "Where the order or agreement stands, for example draft, sent, accepted or completed."
5386
6006
  },
5387
- {
5388
- "name": "customerId",
5389
- "type": "foreignKey",
5390
- "required": false,
5391
- "related": "Customer",
5392
- "columnType": "UUID",
5393
- "description": "The customer a sales order or agreement is for."
5394
- },
5395
- {
5396
- "name": "vendorId",
5397
- "type": "foreignKey",
5398
- "required": false,
5399
- "related": "Vendor",
5400
- "columnType": "UUID",
5401
- "description": "The vendor a purchase order is placed with."
5402
- },
5403
6007
  {
5404
6008
  "name": "subtotal",
5405
6009
  "type": "integer",
@@ -5484,21 +6088,21 @@
5484
6088
  "columnType": "TEXT",
5485
6089
  "default": "",
5486
6090
  "description": "Where it came from, for example your shop, a marketplace or a phone call."
5487
- }
5488
- ],
5489
- "relationships": [
6091
+ },
5490
6092
  {
5491
6093
  "name": "customerId",
5492
6094
  "type": "foreignKey",
5493
- "required": false,
6095
+ "required": true,
5494
6096
  "related": "Customer",
5495
6097
  "columnType": "UUID"
5496
- },
6098
+ }
6099
+ ],
6100
+ "relationships": [
5497
6101
  {
5498
- "name": "vendorId",
6102
+ "name": "customerId",
5499
6103
  "type": "foreignKey",
5500
- "required": false,
5501
- "related": "Vendor",
6104
+ "required": true,
6105
+ "related": "Customer",
5502
6106
  "columnType": "UUID"
5503
6107
  }
5504
6108
  ],
@@ -5807,7 +6411,7 @@
5807
6411
  "description": "Service or maintenance agreement",
5808
6412
  "tableName": "contracts",
5809
6413
  "packageName": "@happyvertical/smrt-commerce",
5810
- "extends": "Contract",
6414
+ "extends": "SalesContract",
5811
6415
  "fields": [
5812
6416
  {
5813
6417
  "name": "created_at",
@@ -5831,8 +6435,7 @@
5831
6435
  "name": "contractType",
5832
6436
  "type": "text",
5833
6437
  "required": false,
5834
- "columnType": "TEXT",
5835
- "description": "The kind of contract: order, estimate, lease, agreement and so on."
6438
+ "columnType": "TEXT"
5836
6439
  },
5837
6440
  {
5838
6441
  "name": "status",
@@ -5841,22 +6444,6 @@
5841
6444
  "columnType": "TEXT",
5842
6445
  "description": "Where the order or agreement stands, for example draft, sent, accepted or completed."
5843
6446
  },
5844
- {
5845
- "name": "customerId",
5846
- "type": "foreignKey",
5847
- "required": false,
5848
- "related": "Customer",
5849
- "columnType": "UUID",
5850
- "description": "The customer a sales order or agreement is for."
5851
- },
5852
- {
5853
- "name": "vendorId",
5854
- "type": "foreignKey",
5855
- "required": false,
5856
- "related": "Vendor",
5857
- "columnType": "UUID",
5858
- "description": "The vendor a purchase order is placed with."
5859
- },
5860
6447
  {
5861
6448
  "name": "subtotal",
5862
6449
  "type": "integer",
@@ -5941,21 +6528,21 @@
5941
6528
  "columnType": "TEXT",
5942
6529
  "default": "",
5943
6530
  "description": "Where it came from, for example your shop, a marketplace or a phone call."
5944
- }
5945
- ],
5946
- "relationships": [
6531
+ },
5947
6532
  {
5948
6533
  "name": "customerId",
5949
6534
  "type": "foreignKey",
5950
- "required": false,
6535
+ "required": true,
5951
6536
  "related": "Customer",
5952
6537
  "columnType": "UUID"
5953
- },
6538
+ }
6539
+ ],
6540
+ "relationships": [
5954
6541
  {
5955
- "name": "vendorId",
6542
+ "name": "customerId",
5956
6543
  "type": "foreignKey",
5957
- "required": false,
5958
- "related": "Vendor",
6544
+ "required": true,
6545
+ "related": "Customer",
5959
6546
  "columnType": "UUID"
5960
6547
  }
5961
6548
  ],
@@ -6264,7 +6851,7 @@
6264
6851
  "description": "Order sent to vendor/supplier",
6265
6852
  "tableName": "contracts",
6266
6853
  "packageName": "@happyvertical/smrt-commerce",
6267
- "extends": "Contract",
6854
+ "extends": "PurchaseContract",
6268
6855
  "fields": [
6269
6856
  {
6270
6857
  "name": "created_at",
@@ -6288,8 +6875,7 @@
6288
6875
  "name": "contractType",
6289
6876
  "type": "text",
6290
6877
  "required": false,
6291
- "columnType": "TEXT",
6292
- "description": "The kind of contract: order, estimate, lease, agreement and so on."
6878
+ "columnType": "TEXT"
6293
6879
  },
6294
6880
  {
6295
6881
  "name": "status",
@@ -6298,22 +6884,6 @@
6298
6884
  "columnType": "TEXT",
6299
6885
  "description": "Where the order or agreement stands, for example draft, sent, accepted or completed."
6300
6886
  },
6301
- {
6302
- "name": "customerId",
6303
- "type": "foreignKey",
6304
- "required": false,
6305
- "related": "Customer",
6306
- "columnType": "UUID",
6307
- "description": "The customer a sales order or agreement is for."
6308
- },
6309
- {
6310
- "name": "vendorId",
6311
- "type": "foreignKey",
6312
- "required": false,
6313
- "related": "Vendor",
6314
- "columnType": "UUID",
6315
- "description": "The vendor a purchase order is placed with."
6316
- },
6317
6887
  {
6318
6888
  "name": "subtotal",
6319
6889
  "type": "integer",
@@ -6399,6 +6969,13 @@
6399
6969
  "default": "",
6400
6970
  "description": "Where it came from, for example your shop, a marketplace or a phone call."
6401
6971
  },
6972
+ {
6973
+ "name": "vendorId",
6974
+ "type": "foreignKey",
6975
+ "required": true,
6976
+ "related": "Vendor",
6977
+ "columnType": "UUID"
6978
+ },
6402
6979
  {
6403
6980
  "name": "expectedDate",
6404
6981
  "type": "meta",
@@ -6415,17 +6992,10 @@
6415
6992
  }
6416
6993
  ],
6417
6994
  "relationships": [
6418
- {
6419
- "name": "customerId",
6420
- "type": "foreignKey",
6421
- "required": false,
6422
- "related": "Customer",
6423
- "columnType": "UUID"
6424
- },
6425
6995
  {
6426
6996
  "name": "vendorId",
6427
6997
  "type": "foreignKey",
6428
- "required": false,
6998
+ "required": true,
6429
6999
  "related": "Vendor",
6430
7000
  "columnType": "UUID"
6431
7001
  }
@@ -6735,7 +7305,7 @@
6735
7305
  "description": "B2B order placed by a wholesale customer.",
6736
7306
  "tableName": "contracts",
6737
7307
  "packageName": "@happyvertical/smrt-commerce",
6738
- "extends": "Contract",
7308
+ "extends": "SalesContract",
6739
7309
  "fields": [
6740
7310
  {
6741
7311
  "name": "created_at",
@@ -6759,8 +7329,7 @@
6759
7329
  "name": "contractType",
6760
7330
  "type": "text",
6761
7331
  "required": false,
6762
- "columnType": "TEXT",
6763
- "description": "The kind of contract: order, estimate, lease, agreement and so on."
7332
+ "columnType": "TEXT"
6764
7333
  },
6765
7334
  {
6766
7335
  "name": "status",
@@ -6769,22 +7338,6 @@
6769
7338
  "columnType": "TEXT",
6770
7339
  "description": "Where the order or agreement stands, for example draft, sent, accepted or completed."
6771
7340
  },
6772
- {
6773
- "name": "customerId",
6774
- "type": "foreignKey",
6775
- "required": false,
6776
- "related": "Customer",
6777
- "columnType": "UUID",
6778
- "description": "The customer a sales order or agreement is for."
6779
- },
6780
- {
6781
- "name": "vendorId",
6782
- "type": "foreignKey",
6783
- "required": false,
6784
- "related": "Vendor",
6785
- "columnType": "UUID",
6786
- "description": "The vendor a purchase order is placed with."
6787
- },
6788
7341
  {
6789
7342
  "name": "subtotal",
6790
7343
  "type": "integer",
@@ -6869,21 +7422,21 @@
6869
7422
  "columnType": "TEXT",
6870
7423
  "default": "",
6871
7424
  "description": "Where it came from, for example your shop, a marketplace or a phone call."
6872
- }
6873
- ],
6874
- "relationships": [
7425
+ },
6875
7426
  {
6876
7427
  "name": "customerId",
6877
7428
  "type": "foreignKey",
6878
- "required": false,
7429
+ "required": true,
6879
7430
  "related": "Customer",
6880
7431
  "columnType": "UUID"
6881
- },
7432
+ }
7433
+ ],
7434
+ "relationships": [
6882
7435
  {
6883
- "name": "vendorId",
7436
+ "name": "customerId",
6884
7437
  "type": "foreignKey",
6885
- "required": false,
6886
- "related": "Vendor",
7438
+ "required": true,
7439
+ "related": "Customer",
6887
7440
  "columnType": "UUID"
6888
7441
  }
6889
7442
  ],
@@ -7192,7 +7745,7 @@
7192
7745
  "description": "Instruction to commission goods from an internal or external factory.",
7193
7746
  "tableName": "contracts",
7194
7747
  "packageName": "@happyvertical/smrt-commerce",
7195
- "extends": "Contract",
7748
+ "extends": "PurchaseContract",
7196
7749
  "fields": [
7197
7750
  {
7198
7751
  "name": "created_at",
@@ -7216,8 +7769,7 @@
7216
7769
  "name": "contractType",
7217
7770
  "type": "text",
7218
7771
  "required": false,
7219
- "columnType": "TEXT",
7220
- "description": "The kind of contract: order, estimate, lease, agreement and so on."
7772
+ "columnType": "TEXT"
7221
7773
  },
7222
7774
  {
7223
7775
  "name": "status",
@@ -7226,22 +7778,6 @@
7226
7778
  "columnType": "TEXT",
7227
7779
  "description": "Where the order or agreement stands, for example draft, sent, accepted or completed."
7228
7780
  },
7229
- {
7230
- "name": "customerId",
7231
- "type": "foreignKey",
7232
- "required": false,
7233
- "related": "Customer",
7234
- "columnType": "UUID",
7235
- "description": "The customer a sales order or agreement is for."
7236
- },
7237
- {
7238
- "name": "vendorId",
7239
- "type": "foreignKey",
7240
- "required": false,
7241
- "related": "Vendor",
7242
- "columnType": "UUID",
7243
- "description": "The vendor a purchase order is placed with."
7244
- },
7245
7781
  {
7246
7782
  "name": "subtotal",
7247
7783
  "type": "integer",
@@ -7327,6 +7863,14 @@
7327
7863
  "default": "",
7328
7864
  "description": "Where it came from, for example your shop, a marketplace or a phone call."
7329
7865
  },
7866
+ {
7867
+ "name": "vendorId",
7868
+ "type": "foreignKey",
7869
+ "required": false,
7870
+ "related": "Vendor",
7871
+ "columnType": "UUID",
7872
+ "description": "The vendor a purchase order is placed with."
7873
+ },
7330
7874
  {
7331
7875
  "name": "productId",
7332
7876
  "type": "meta",
@@ -7341,13 +7885,6 @@
7341
7885
  }
7342
7886
  ],
7343
7887
  "relationships": [
7344
- {
7345
- "name": "customerId",
7346
- "type": "foreignKey",
7347
- "required": false,
7348
- "related": "Customer",
7349
- "columnType": "UUID"
7350
- },
7351
7888
  {
7352
7889
  "name": "vendorId",
7353
7890
  "type": "foreignKey",
@@ -7661,7 +8198,7 @@
7661
8198
  "description": "Transient order-in-progress for a shopper.",
7662
8199
  "tableName": "contracts",
7663
8200
  "packageName": "@happyvertical/smrt-commerce",
7664
- "extends": "Contract",
8201
+ "extends": "SalesContract",
7665
8202
  "fields": [
7666
8203
  {
7667
8204
  "name": "created_at",
@@ -7685,8 +8222,7 @@
7685
8222
  "name": "contractType",
7686
8223
  "type": "text",
7687
8224
  "required": false,
7688
- "columnType": "TEXT",
7689
- "description": "The kind of contract: order, estimate, lease, agreement and so on."
8225
+ "columnType": "TEXT"
7690
8226
  },
7691
8227
  {
7692
8228
  "name": "status",
@@ -7695,22 +8231,6 @@
7695
8231
  "columnType": "TEXT",
7696
8232
  "description": "Where the order or agreement stands, for example draft, sent, accepted or completed."
7697
8233
  },
7698
- {
7699
- "name": "customerId",
7700
- "type": "foreignKey",
7701
- "required": false,
7702
- "related": "Customer",
7703
- "columnType": "UUID",
7704
- "description": "The customer a sales order or agreement is for."
7705
- },
7706
- {
7707
- "name": "vendorId",
7708
- "type": "foreignKey",
7709
- "required": false,
7710
- "related": "Vendor",
7711
- "columnType": "UUID",
7712
- "description": "The vendor a purchase order is placed with."
7713
- },
7714
8234
  {
7715
8235
  "name": "subtotal",
7716
8236
  "type": "integer",
@@ -7795,21 +8315,22 @@
7795
8315
  "columnType": "TEXT",
7796
8316
  "default": "",
7797
8317
  "description": "Where it came from, for example your shop, a marketplace or a phone call."
7798
- }
7799
- ],
7800
- "relationships": [
8318
+ },
7801
8319
  {
7802
8320
  "name": "customerId",
7803
8321
  "type": "foreignKey",
7804
8322
  "required": false,
7805
8323
  "related": "Customer",
7806
- "columnType": "UUID"
7807
- },
8324
+ "columnType": "UUID",
8325
+ "description": "The customer a sales order or agreement is for."
8326
+ }
8327
+ ],
8328
+ "relationships": [
7808
8329
  {
7809
- "name": "vendorId",
8330
+ "name": "customerId",
7810
8331
  "type": "foreignKey",
7811
8332
  "required": false,
7812
- "related": "Vendor",
8333
+ "related": "Customer",
7813
8334
  "columnType": "UUID"
7814
8335
  }
7815
8336
  ],
@@ -8118,7 +8639,7 @@
8118
8639
  "description": "Industry-neutral licensing primitive.",
8119
8640
  "tableName": "contracts",
8120
8641
  "packageName": "@happyvertical/smrt-commerce",
8121
- "extends": "Contract",
8642
+ "extends": "SalesContract",
8122
8643
  "fields": [
8123
8644
  {
8124
8645
  "name": "created_at",
@@ -8142,8 +8663,7 @@
8142
8663
  "name": "contractType",
8143
8664
  "type": "text",
8144
8665
  "required": false,
8145
- "columnType": "TEXT",
8146
- "description": "The kind of contract: order, estimate, lease, agreement and so on."
8666
+ "columnType": "TEXT"
8147
8667
  },
8148
8668
  {
8149
8669
  "name": "status",
@@ -8152,22 +8672,6 @@
8152
8672
  "columnType": "TEXT",
8153
8673
  "description": "Where the order or agreement stands, for example draft, sent, accepted or completed."
8154
8674
  },
8155
- {
8156
- "name": "customerId",
8157
- "type": "foreignKey",
8158
- "required": false,
8159
- "related": "Customer",
8160
- "columnType": "UUID",
8161
- "description": "The customer a sales order or agreement is for."
8162
- },
8163
- {
8164
- "name": "vendorId",
8165
- "type": "foreignKey",
8166
- "required": false,
8167
- "related": "Vendor",
8168
- "columnType": "UUID",
8169
- "description": "The vendor a purchase order is placed with."
8170
- },
8171
8675
  {
8172
8676
  "name": "subtotal",
8173
8677
  "type": "integer",
@@ -8253,6 +8757,14 @@
8253
8757
  "default": "",
8254
8758
  "description": "Where it came from, for example your shop, a marketplace or a phone call."
8255
8759
  },
8760
+ {
8761
+ "name": "customerId",
8762
+ "type": "foreignKey",
8763
+ "required": false,
8764
+ "related": "Customer",
8765
+ "columnType": "UUID",
8766
+ "description": "The customer a sales order or agreement is for."
8767
+ },
8256
8768
  {
8257
8769
  "name": "skuId",
8258
8770
  "type": "meta",
@@ -8351,13 +8863,6 @@
8351
8863
  "required": false,
8352
8864
  "related": "Customer",
8353
8865
  "columnType": "UUID"
8354
- },
8355
- {
8356
- "name": "vendorId",
8357
- "type": "foreignKey",
8358
- "required": false,
8359
- "related": "Vendor",
8360
- "columnType": "UUID"
8361
8866
  }
8362
8867
  ],
8363
8868
  "methods": [
@@ -9332,7 +9837,7 @@
9332
9837
  "name": "contractId",
9333
9838
  "type": "foreignKey",
9334
9839
  "required": false,
9335
- "related": "Contract",
9840
+ "related": "SalesContract",
9336
9841
  "columnType": "UUID",
9337
9842
  "description": "The order being fulfilled."
9338
9843
  },
@@ -9406,7 +9911,7 @@
9406
9911
  "name": "contractId",
9407
9912
  "type": "foreignKey",
9408
9913
  "required": false,
9409
- "related": "Contract",
9914
+ "related": "SalesContract",
9410
9915
  "columnType": "UUID"
9411
9916
  }
9412
9917
  ],
@@ -9857,7 +10362,7 @@
9857
10362
  "name": "contractId",
9858
10363
  "type": "foreignKey",
9859
10364
  "required": false,
9860
- "related": "Contract",
10365
+ "related": "SalesContract",
9861
10366
  "columnType": "UUID",
9862
10367
  "description": "The order or agreement this invoice bills for, if any."
9863
10368
  },
@@ -10061,7 +10566,7 @@
10061
10566
  "name": "contractId",
10062
10567
  "type": "foreignKey",
10063
10568
  "required": false,
10064
- "related": "Contract",
10569
+ "related": "SalesContract",
10065
10570
  "columnType": "UUID"
10066
10571
  },
10067
10572
  {
@@ -10901,7 +11406,7 @@
10901
11406
  "name": "contractId",
10902
11407
  "type": "foreignKey",
10903
11408
  "required": false,
10904
- "related": "Contract",
11409
+ "related": "SalesContract",
10905
11410
  "columnType": "UUID",
10906
11411
  "description": "The order or agreement this payment is for, if any."
10907
11412
  },
@@ -11048,7 +11553,7 @@
11048
11553
  "name": "contractId",
11049
11554
  "type": "foreignKey",
11050
11555
  "required": false,
11051
- "related": "Contract",
11556
+ "related": "SalesContract",
11052
11557
  "columnType": "UUID"
11053
11558
  },
11054
11559
  {
@@ -16581,6 +17086,86 @@
16581
17086
  "operation": "get",
16582
17087
  "objectName": "@happyvertical/smrt-commerce:Contract"
16583
17088
  },
17089
+ {
17090
+ "kind": "api",
17091
+ "name": "contracts.list",
17092
+ "operation": "list",
17093
+ "objectName": "@happyvertical/smrt-commerce:SalesContract",
17094
+ "path": "/contracts",
17095
+ "method": "GET"
17096
+ },
17097
+ {
17098
+ "kind": "api",
17099
+ "name": "contracts.get",
17100
+ "operation": "get",
17101
+ "objectName": "@happyvertical/smrt-commerce:SalesContract",
17102
+ "path": "/contracts/[id]",
17103
+ "method": "GET"
17104
+ },
17105
+ {
17106
+ "kind": "cli",
17107
+ "name": "salescontract_list",
17108
+ "operation": "list",
17109
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
17110
+ },
17111
+ {
17112
+ "kind": "cli",
17113
+ "name": "salescontract_get",
17114
+ "operation": "get",
17115
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
17116
+ },
17117
+ {
17118
+ "kind": "mcp",
17119
+ "name": "salescontract_list",
17120
+ "operation": "list",
17121
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
17122
+ },
17123
+ {
17124
+ "kind": "mcp",
17125
+ "name": "salescontract_get",
17126
+ "operation": "get",
17127
+ "objectName": "@happyvertical/smrt-commerce:SalesContract"
17128
+ },
17129
+ {
17130
+ "kind": "api",
17131
+ "name": "contracts.list",
17132
+ "operation": "list",
17133
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract",
17134
+ "path": "/contracts",
17135
+ "method": "GET"
17136
+ },
17137
+ {
17138
+ "kind": "api",
17139
+ "name": "contracts.get",
17140
+ "operation": "get",
17141
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract",
17142
+ "path": "/contracts/[id]",
17143
+ "method": "GET"
17144
+ },
17145
+ {
17146
+ "kind": "cli",
17147
+ "name": "purchasecontract_list",
17148
+ "operation": "list",
17149
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
17150
+ },
17151
+ {
17152
+ "kind": "cli",
17153
+ "name": "purchasecontract_get",
17154
+ "operation": "get",
17155
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
17156
+ },
17157
+ {
17158
+ "kind": "mcp",
17159
+ "name": "purchasecontract_list",
17160
+ "operation": "list",
17161
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
17162
+ },
17163
+ {
17164
+ "kind": "mcp",
17165
+ "name": "purchasecontract_get",
17166
+ "operation": "get",
17167
+ "objectName": "@happyvertical/smrt-commerce:PurchaseContract"
17168
+ },
16584
17169
  {
16585
17170
  "kind": "api",
16586
17171
  "name": "contracts.list",
@@ -19868,20 +20453,25 @@
19868
20453
  ],
19869
20454
  "prompts": [],
19870
20455
  "relationshipsV2": {
19871
- "foreignKeyFields": 37,
20456
+ "foreignKeyFields": 28,
19872
20457
  "crossPackageRefFields": 14,
19873
20458
  "junctionCollections": 0,
19874
20459
  "hierarchicalObjects": 0,
19875
20460
  "polymorphicAssociations": 0,
19876
- "uuidColumns": 162
20461
+ "uuidColumns": 178
19877
20462
  },
19878
- "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** (STI base → Estimate, Order, Lease, Agreement, PurchaseOrder, WholesaleOrder, ProductionOrder, Cart, LicenseSale): 9 contract types sharing one table. Carries `channelId` (open-ended string — `dtc-web`, `wholesale-b2b`, `pos-store-N`, etc.) so the same model serves DTC checkout, B2B portals, and POS.\n- **WholesaleOrder**: B2B order. Conventional pairing — customer has `customerType: 'wholesale'`, NET-30/60 terms, delivered via wholesale-portal channel.\n- **ProductionOrder**: manufacturing equivalent of a PurchaseOrder — commission your factory to make finished goods. Consumes raw materials per BOM (`@happyvertical/smrt-manufacturing`) and produces SKU stock (`@happyvertical/smrt-inventory`).\n- **Cart**: transient order-in-progress. Same shape as Order; the application promotes the row from `_meta_type: Cart` to `_meta_type: Order` at checkout instead of copying data between tables.\n- **LicenseSale**: industry-neutral licensing primitive. Carries an *immutable* rights snapshot (`rightsMedium`, `rightsDistributionScope`, `rightsExclusivity`, `rightsDuration`, `rightsTerritory`, `rightsSublicensing`, `rightsDerivatives` — typed `Meta<T>` fields), licensee identity (`licenseeEmail`, optional `licenseeLegalEntity` / `licenseeJurisdiction`), and a signed-PDF reference (`pdfUrl`, `pdfHash`, optional `onChainHashRegistryRef`). Once saved at `ContractStatus.ACCEPTED`, the rights snapshot is frozen — mutating any of the seven rights fields and re-saving throws. The only legal transition out of ACCEPTED is `revoke()` (moves to CANCELLED without touching rights). Useful for stock media, music licensing, code-asset marketplaces, license keys, anywhere rights are sold for a fee.\n- **ContractLineItem**: items on contracts.\n- **Invoice**: status machine `DRAFT → SENT → VIEWED → PARTIAL → PAID` (also OVERDUE, CANCELLED, WRITTEN_OFF). `recognizeRevenue()` creates balanced AR journal entry (DR: Accounts Receivable, CR: Revenue, CR: Tax Payable).\n- **InvoiceLineItem**: fractional-quantity line items with safe minor-unit validation/recalculation on save; editor-mode recovery and existing-column migration are documented in [invoice quantity persistence](agents/invoice-quantity.md).\n- **Payment** / **PaymentAllocation**: tracks payments against invoices (status: 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 [payment allocation integrity](agents/payment-allocations.md) for atomic splits,\nretry identity, parent corrections and supported dialects.\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- **`Invoice.providerTaxAmount`** is added to line-item tax; it is\n server-managed (not API-writable). `toAccountingInput()` emits major units.\n- **Payment rails (#3138).** A runtime has one issuing `provider` and any\n number of `paymentProviders` (unique names, one inbox namespace each; a\n provider with `capabilities.issuesInvoices === false` can never issue).\n `observe()` resolves the provider from the delivery namespace, never the\n payload. Crypto rails implement the port over `@happyvertical/payments`'\n `CryptoCheckoutGateway`; never call BTCPay (or any gateway) from here.\n Settlement is decided by the pure `decideAttempt()` from gateway state and\n `paymentPolicy` — the gateway's `settled` is authoritative and nothing here\n counts confirmations. A `BillingPaymentAttempt` (`(provider, checkout_id)`\n deterministic id) records every state and settles once; grants and payments\n are keyed by checkout id. Invoice payments close the issuer's invoice out of\n band in `observe()` (idempotent) before `project()` records the payment, and\n `settlePaidInvoice` skips (records nothing for) a `paidOutOfBand` invoice\n the rail latched with `outOfBandRequestedAt`; other out-of-band closes are\n recorded as `OTHER` payments. Rails never save cards or charge tax: a taxed\n credit purchase on a rail needs an explicit `automaticTax: false`. Exceptions are flags for operators;\n nothing reverses or refunds automatically.\n- **Known limits**: write-offs post no bad-debt journal (AR stays until paid\n or journaled by an operator); discounts still reach Stripe as negative lines (the SDK's\n line `discount` would change the lines a replayed push reconciles, so it\n needs its own migration); `RetailCharge` has no adjustment ledger. Flat\n plans are monthly, in arrears; plan changes are not prorated (#3119).\n\n## Cross-Package References\n\n- `customerId` → `@foreignKey('Customer')` (hard reference within package)\n- `profileId` → plain string to smrt-profiles\n- `arJournalId`, `revenueJournalId` → plain string to smrt-ledgers\n- `skuId` (on `PaymentIntent`, `LicenseSale`) → plain string to smrt-products\n- `paymentId` (on `PaymentIntent`, `Payout`, `LicenseSale`) → plain string to `Payment` (same package, but kept as plain string for cross-model consistency)\n- `vendorId` on `Payout` → `@foreignKey(Vendor)` (hard reference within package)\n- Billing records reference tenants with `@crossPackageRef('@happyvertical/smrt-users:Tenant')` and charges, plan periods, and adjustments by plain `sourceId`\n\n## Gotchas\n\n- **Optional tenancy**: all models `@TenantScoped({ mode: 'optional' })` + nullable tenantId\n- **Currency is integer minor units** (cents, satoshis) — `$19.99` is `1999`, like affiliates. Money is exact, so money fields initialize `= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER columns (BIGINT on fresh PostgreSQL/DuckDB databases). Writing a fractional major-unit value is the bug — the model rejects it via `assertIntegerMinorUnits` on `Invoice`/`Payment`/`PaymentAllocation`/`Payout`/`Contract` before the database sees it, because PostgreSQL would reject it with `22P02` while SQLite's affinity silently stores it. `pnpm --filter @happyvertical/smrt-commerce test:postgres` is the lane that holds the line (#2361, #2401, #2373).\n- **Rates are the opposite**: `InvoiceLineItem.taxRate` and `ContractLineItem.taxRate` are inherently fractional and must initialize `= 0.0`. INTEGER would truncate every rate to 0. `ContractLineItem.quantity` is decimal for the same reason (contracts price fractional hours/weight/bandwidth); `InvoiceLineItem.quantity` is also decimal (six fractional places); existing integer columns require the explicit [quantity migration](agents/invoice-quantity.md).\n- **Rounding happens where a rate meets money, and nowhere else**: `InvoiceLineItem` uses exact shared invoice arithmetic to round fractional quantity × price and then tax to minor units; `ContractLineItem.calculateAmount()` uses `Math.round()` for tax. Everything downstream is exact integer arithmetic, which is why **there are no `EPSILON` tolerances left on any money path** (#2401). `Invoice`, `PaymentAllocation`, `Payout` and `PaymentIntent` compare with `===` / `>` / `>=`. Do not reintroduce a tolerance: against integers it forgives a real one-cent discrepancy rather than float fuzz. (`FulfillmentLineItem`'s quantity epsilon is unrelated — quantities are genuinely decimal there.)\n- **Migrating an existing database to minor units**: `preflightCommerceMoneyMinorUnits(db)` reports, per column, whether it still holds major units and which rows would be rounded or exceed JavaScript's safe-integer range; `migrateCommerceMoneyToMinorUnits(db)` converts them. Both are exported from the package root. The rescale is idempotent via a `_smrt_backfills` marker, changes the column type in place on PostgreSQL/DuckDB (`ALTER … TYPE BIGINT USING round(col * 100)`), and on SQLite rescales values only — SQLite cannot alter a declared type in place, so those columns come back in `declaredTypeChangePending` and need the table-rebuild path (#2370).\n- **`Payment.nativeAmount` is NOT in that migration, and must not be.** Its minor unit is a property of the *row's asset*: satoshis on a BTC rail (×1e8), cents on a fiat or stablecoin rail (×1e2), in one column discriminated only by `nativeCurrency`. A blanket ×100 turns 0.00713 BTC into `1` instead of `713000` sats, and a round 0.01 BTC even passes the preflight's integrality check while being wrong by six orders of magnitude. Convert it in two deliberate steps — normalise each row to its own asset's minor units while the column is still floating-point, then `rescaleMoneyColumnsToMinorUnits(db, COMMERCE_NATIVE_UNIT_COLUMNS, { scale: 1, … })` for the type change. `COMMERCE_NATIVE_UNIT_COLUMNS` is exported with a worked example; step one is deliberately the deployment's to write, because only it knows which `nativeCurrency` values it has used.\n- **`PaymentIntent.paymentOptions[].nativeAmount` has the same per-asset problem inside a JSON column and is likewise not migrated.** An intent's price lock defaults to 15 minutes, so the supported answer is to let open intents expire and be re-quoted: an unconverted option fails the exact `!==` reconciliation against the payment that arrives, which is a loud failure rather than a silently mis-scaled quote.\n- **Range and existing deployments**: fresh PostgreSQL/DuckDB INTEGER columns are BIGINT, and the runtime rejects values outside JavaScript's safe-integer range rather than rounding them. Existing PostgreSQL `int4` columns do not self-repair; widen them through the explicit migration tracked in #2424.\n- **Journals posted from commerce carry minor units**: `Payment.recordPayment()` and `Invoice.recognizeRevenue()` write `amount` / `totalAmount` straight into smrt-ledgers entries. smrt-ledgers is unit-agnostic (its `BALANCE_EPSILON` only checks debits === credits), and integer entries balance exactly, so this is strictly tighter than the major-unit journals commerce used to post.\n- **Invoice controls payment status**: not the Payment model — use `Invoice.updatePaymentStatus()`\n- **Tax rate is external**: no tax rate field on Invoice — rate must be calculated externally\n- **Profile linking**: separate `ProfileCollection.create()` needed to fetch actual Profile object\n- **PaymentIntent natural key**: `conflictColumns: ['tenant_id', 'offering_ref', 'licensee_email', 'idempotency_key']` — a retried `create` with the same tuple upserts the existing row. Use `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",
20463
+ "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; editor-mode recovery and existing-column migration are documented in [invoice quantity persistence](agents/invoice-quantity.md).\n- **Payment** / **PaymentAllocation**: tracks payments against invoices (status: 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 [payment allocation integrity](agents/payment-allocations.md) for atomic splits,\nretry identity, parent corrections and supported dialects.\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- **`Invoice.providerTaxAmount`** is added to line-item tax; it is\n server-managed (not API-writable). `toAccountingInput()` emits major units.\n- **Payment rails (#3138).** A runtime has one issuing `provider` and any\n number of `paymentProviders` (unique names, one inbox namespace each; a\n provider with `capabilities.issuesInvoices === false` can never issue).\n `observe()` resolves the provider from the delivery namespace, never the\n payload. Crypto rails implement the port over `@happyvertical/payments`'\n `CryptoCheckoutGateway`; never call BTCPay (or any gateway) from here.\n Settlement is decided by the pure `decideAttempt()` from gateway state and\n `paymentPolicy` — the gateway's `settled` is authoritative and nothing here\n counts confirmations. A `BillingPaymentAttempt` (`(provider, checkout_id)`\n deterministic id) records every state and settles once; grants and payments\n are keyed by checkout id. Invoice payments close the issuer's invoice out of\n band in `observe()` (idempotent) before `project()` records the payment, and\n `settlePaidInvoice` skips (records nothing for) a `paidOutOfBand` invoice\n the rail latched with `outOfBandRequestedAt`; other out-of-band closes are\n recorded as `OTHER` payments. Rails never save cards or charge tax: a taxed\n credit purchase on a rail needs an explicit `automaticTax: false`. Exceptions are flags for operators;\n nothing reverses or refunds automatically.\n- **Known limits**: write-offs post no bad-debt journal (AR stays until paid\n or journaled by an operator); discounts still reach Stripe as negative lines (the SDK's\n line `discount` would change the lines a replayed push reconciles, so it\n needs its own migration); `RetailCharge` has no adjustment ledger. Flat\n plans are monthly, in arrears; plan changes are not prorated (#3119).\n\n## Cross-Package References\n\n- `customerId` → `@foreignKey('Customer')` (hard reference within package)\n- `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- **Currency is integer minor units** (cents, satoshis) — `$19.99` is `1999`, like affiliates. Money is exact, so money fields initialize `= 0`, never `= 0.0`: the integer literal is what maps them to INTEGER columns (BIGINT on fresh PostgreSQL/DuckDB databases). Writing a fractional major-unit value is the bug — the model rejects it via `assertIntegerMinorUnits` on `Invoice`/`Payment`/`PaymentAllocation`/`Payout`/`Contract` before the database sees it, because PostgreSQL would reject it with `22P02` while SQLite's affinity silently stores it. `pnpm --filter @happyvertical/smrt-commerce test:postgres` is the lane that holds the line (#2361, #2401, #2373).\n- **Rates are the opposite**: `InvoiceLineItem.taxRate` and `ContractLineItem.taxRate` are inherently fractional and must initialize `= 0.0`. INTEGER would truncate every rate to 0. `ContractLineItem.quantity` is decimal for the same reason (contracts price fractional hours/weight/bandwidth); `InvoiceLineItem.quantity` is also decimal (six fractional places); existing integer columns require the explicit [quantity migration](agents/invoice-quantity.md).\n- **Rounding happens where a rate meets money, and nowhere else**: `InvoiceLineItem` uses exact shared invoice arithmetic to round fractional quantity × price and then tax to minor units; `ContractLineItem.calculateAmount()` uses `Math.round()` for tax. Everything downstream is exact integer arithmetic, which is why **there are no `EPSILON` tolerances left on any money path** (#2401). `Invoice`, `PaymentAllocation`, `Payout` and `PaymentIntent` compare with `===` / `>` / `>=`. Do not reintroduce a tolerance: against integers it forgives a real one-cent discrepancy rather than float fuzz. (`FulfillmentLineItem`'s quantity epsilon is unrelated — quantities are genuinely decimal there.)\n- **Migrating an existing database to minor units**: `preflightCommerceMoneyMinorUnits(db)` reports, per column, whether it still holds major units and which rows would be rounded or exceed JavaScript's safe-integer range; `migrateCommerceMoneyToMinorUnits(db)` converts them. Both are exported from the package root. The rescale is idempotent via a `_smrt_backfills` marker, changes the column type in place on PostgreSQL/DuckDB (`ALTER … TYPE BIGINT USING round(col * 100)`), and on SQLite rescales values only — SQLite cannot alter a declared type in place, so those columns come back in `declaredTypeChangePending` and need the table-rebuild path (#2370).\n- **`Payment.nativeAmount` is NOT in that migration, and must not be.** Its minor unit is a property of the *row's asset*: satoshis on a BTC rail (×1e8), cents on a fiat or stablecoin rail (×1e2), in one column discriminated only by `nativeCurrency`. A blanket ×100 turns 0.00713 BTC into `1` instead of `713000` sats, and a round 0.01 BTC even passes the preflight's integrality check while being wrong by six orders of magnitude. Convert it in two deliberate steps — normalise each row to its own asset's minor units while the column is still floating-point, then `rescaleMoneyColumnsToMinorUnits(db, COMMERCE_NATIVE_UNIT_COLUMNS, { scale: 1, … })` for the type change. `COMMERCE_NATIVE_UNIT_COLUMNS` is exported with a worked example; step one is deliberately the deployment's to write, because only it knows which `nativeCurrency` values it has used.\n- **`PaymentIntent.paymentOptions[].nativeAmount` has the same per-asset problem inside a JSON column and is likewise not migrated.** An intent's price lock defaults to 15 minutes, so the supported answer is to let open intents expire and be re-quoted: an unconverted option fails the exact `!==` reconciliation against the payment that arrives, which is a loud failure rather than a silently mis-scaled quote.\n- **Range and existing deployments**: fresh PostgreSQL/DuckDB INTEGER columns are BIGINT, and the runtime rejects values outside JavaScript's safe-integer range rather than rounding them. Existing PostgreSQL `int4` columns do not self-repair; widen them through the explicit migration tracked in #2424.\n- **Journals posted from commerce carry minor units**: `Payment.recordPayment()` and `Invoice.recognizeRevenue()` write `amount` / `totalAmount` straight into smrt-ledgers entries. smrt-ledgers is unit-agnostic (its `BALANCE_EPSILON` only checks debits === credits), and integer entries balance exactly, so this is strictly tighter than the major-unit journals commerce used to post.\n- **Invoice controls payment status**: not the Payment model — use `Invoice.updatePaymentStatus()`\n- **Tax rate is external**: no tax rate field on Invoice — rate must be calculated externally\n- **Profile linking**: separate `ProfileCollection.create()` needed to fetch actual Profile object\n- **PaymentIntent natural key**: `conflictColumns: ['tenant_id', 'offering_ref', 'licensee_email', 'idempotency_key']` — a retried `create` with the same tuple upserts the existing row. Use `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",
19879
20464
  "moduleDocs": [
19880
20465
  {
19881
20466
  "path": "agents/customer-vendor-ui.md",
19882
20467
  "module": "customer-vendor-ui",
19883
20468
  "content": "# Customer and vendor UI\n\nImport the provider-free surfaces from `@happyvertical/smrt-commerce/svelte`:\n\n```svelte\n<script lang=\"ts\">\n import {\n CustomerDirectory,\n CustomerDetail,\n CustomerForm,\n VendorDirectory,\n VendorDetail,\n VendorForm,\n type CustomerDisplayData,\n } from '@happyvertical/smrt-commerce/svelte';\n\n let { data, form } = $props();\n</script>\n\n<CustomerForm\n mode=\"edit\"\n values={form?.values ?? data.values}\n errors={form?.errors}\n transport={{\n action: '?/update',\n method: 'post',\n hiddenFields: {\n requestId: form?.values.requestId ?? data.requestId,\n expectedTenantId: data.tenantId,\n },\n }}\n labels={{ singular: 'Client', plural: 'Clients' }}\n cancelHref={`/clients/${data.customer.id}`}\n/>\n```\n\nThe caller owns action URLs, server authorization, tenancy checks, request tokens, persistence, and retry behavior. Capability props only decide whether an affordance is rendered. The components never create a Profile, Customer, Vendor, or contact relationship and never generate request tokens; the server writes posted contact rows with `savePartyContacts()` ([contact rows](#contact-rows-3501)).\n\n`CustomerDisplayData.creditLimitMinor` and `VendorDisplayData.minimumOrderMinor` are integer currency minor units. Detail components convert them only for display and accept an explicit `minorUnitExponent`. Form values deliberately use retained strings such as `1250.00`; the server validates and converts those major-unit strings to the model's integer minor units.\n\n`VendorFormValues.leadTimeDays` is also retained text. The control supplies a numeric keyboard hint without browser number-input sanitization, and the server must parse and validate the submitted non-negative integer.\n\nForms use the provider-free `Form`, `Input`, `Select`, `Textarea`, and `Checkbox` controls from `@happyvertical/smrt-ui/forms`. They default to native `POST` submission through `preventDefault={false}`. `transport.hiddenFields`, `transport.intentName`, and the field-name maps let an application preserve its existing payload contract. Repeatable contact rows submit repeated field names in DOM order. Add and remove controls are native submitters, so they work without JavaScript; the server returns the revised rows and retained values.\n\n`CustomerForm` and `VendorForm` accept submit hooks so a host never has to find\nthe form after mount (#3501). `enhance` (`PartyFormEnhance`, action-shaped) is\napplied to the mounted native `<form>` and destroyed with it; SvelteKit's\n`enhance` from `$app/forms` fits directly, or wrap it to pass a submit callback.\nAn `ssr = false` app needs it, because a plain native post answers with the\nempty shell. `onsubmit` receives the native submit event (submission stays\nnative unless it calls `preventDefault()`), and the `getFormElement()` instance\nmethod returns the native element while mounted.\n\n```svelte\n<script lang=\"ts\">\n import { enhance } from '$app/forms';\n</script>\n\n<VendorForm values={form?.values ?? data.values} errors={form?.errors} transport={{ action: '?/save' }} {enhance} />\n```\n\nDirectory search forms accept `hiddenFields` as a string record for caller-owned GET state such as sort direction and page size. Fields whose names equal the resolved `queryName` or `statusName` are omitted so the visible search controls remain authoritative.\n\nCustomer shipping and billing addresses use the shared `CountrySelect` and\n`ProvinceSelect`. The native names remain `shippingCountry`, `shippingState`,\n`billingCountry`, and `billingState` unless `fieldNames` overrides them. Changing\ncountry changes the region control without clearing its current string; CA and US\nrender localized subdivision choices, while unsupported countries render an\neditable text field. Unknown values remain selectable/postable after a failed\nsubmission. Use `countryOptions` or `provinceOptions` only when the caller must\nrestrict the choices. `VendorForm` has no structured address fields in its public\nDTO and does not invent them.\n\nVendor currency uses the shared `CurrencySelect`. The raw retained string remains\nthe native `currency` payload, including unknown values returned after validation.\nUse `currencyOptions` for caller-authorized restrictions; omitted options use the\nshared currency catalog. Selector labels follow the active smrt-ui i18n locale.\n\nThe forms place a non-focusable default save submitter before contact row actions so Enter in a text field cannot accidentally add or remove a contact. This remains in place when the `actions` snippet customizes the visible action bar; set `transport.saveIntent` to the custom save intent.\n\nUse the `extension` snippets for application fields such as construction roles and trades. Keep those fields and their policy in the application:\n\n```svelte\n<VendorForm values={data.values} transport={{ action: '?/save' }}>\n {#snippet extension(values)}\n <ConstructionVendorFields roles={values.roles} trades={values.trades} />\n {/snippet}\n</VendorForm>\n```\n\nDirectory entries carry caller-owned detail links and pagination links. Detail edit links and form submit buttons appear only when their capability props allow them; the server must still authorize every request.\n\n`CustomerDirectory`, `CustomerDetail`, `VendorDirectory`, and `VendorDetail`\nrender a `PageHeader` (the page `h1`, a subtitle, and the add/edit action) by\ndefault. A host that frames every page itself passes `headless` to render only\nthe body, then renders its own heading and create/edit affordance; or passes a\n`header` snippet to replace the built-in header in place (#3501). `backHref`\nstays independent of both.\n\nDirectory empty and filter behavior follows this contract:\n\n| State | Empty guidance | Native form behavior |\n| --- | --- | --- |\n| No query, status, or caller filter is active | Explain that no records have been added; when `canCreate` and `addHref` are present, point to creating the first record | Search and status controls retain their normal names |\n| Query or status is active | Suggest changing search or filters | Visible query/status controls remain authoritative over same-named `hiddenFields` |\n| A caller-owned filter is active | Suggest changing search or filters | Render the `filters` snippet inside the directory form and list its names in `filterNames` so same-named hidden fields are omitted |\n| Caller supplies `emptyDescription` | Render the supplied description | Submission behavior is unchanged |\n\nThe `filters` snippet is for domain-adjacent GET controls that belong beside\nsearch and status, such as a trade classification. It does not move filtering\nor authorization into the component. `hasAdditionalFilters` describes whether\nthose controls currently narrow the result, while `filterNames` prevents a\nhidden/visible duplicate name from changing native form serialization.\n\nThe registry keys are `customer-directory`, `customer-detail`, `customer-form`, `vendor-directory`, `vendor-detail`, `vendor-form`, and `party-contact-fields`. Interactive examples are published by `@happyvertical/smrt-commerce/playground` and ship in the next generated Commerce package release. DomaCraft can replace its local routes incrementally by adapting its existing server DTOs and payload names; no identity or database migration is required.\n\nThe form playground previews handle save, add-contact, and remove-contact intents in memory. They deliberately return a simulated validation error on save so retained values and retry behavior can be inspected without a backend.\n\n## Contact rows (#3501)\n\n`Customer.contacts` / `Vendor.contacts` store `PartyContact` rows (JSON\ncolumn; additive schema, run `smrt db:migrate`). The root package exports the\nserver half of the form's contact payload:\n\n| Export | Contract |\n| --- | --- |\n| `readPartyContactRows(form, fieldNames?)` | Zips the repeated `contactId`, `contactName`, `contactLabel`, `contactEmail`, `contactPhone`, `contactAddress` values (or the form's `fieldNames` overrides) in DOM order into raw `PartyContactInput` rows. The editable contact block always posts a hidden `contactCount` marker with its row count; a missing marker (`reason: 'missing'`) or any disagreeing count (`'misaligned'`) throws `PartyContactPayloadError`, so zero rows mean an empty list only when the block rendered. |\n| `applyPartyContactIntent(rows, intent, intents?)` | `addContact` appends a blank row; `removeContact:<n>` (or a caller `parseRemoveIntent`) drops row `n`. `handled: true` means render the returned rows and do not save, including for a stale index. |\n| `validatePartyContactRows(rows)` | Trims, drops blank rows, and returns `errors` aligned with the submitted rows using codes `too_long` (limits in `PARTY_CONTACT_MAX_LENGTHS`, matching the inputs) and `invalid_email`. |\n| `savePartyContacts(collection, partyId, formOrRows, { fieldNames?, expectedTenantId? })` | Given the posted form, reads it with `readPartyContactRows` (marker enforced); an array is taken as rows already read that way. Loads the party with `collection.get()` (so the active tenant context scopes it) and replaces its contacts: posted ids are kept, new rows get a random id, unposted rows are removed. A posted id the party does not have is `unknown_contact` on `id`. Invalid input throws `PartyContactValidationError` (`errors`, nothing written); a missing or other-tenant party throws `PartyContactPartyNotFoundError`. |\n\nError codes are not messages: map `PartyContactValidationError.errors` to\nlocalized strings for `PartyFormErrors.contacts`, and return the submitted\nrows as retained `values.contacts`. A form with custom contact `fieldNames`\nmust pass the same names when reading; with mismatched names the marker is\nnot found and the read fails closed instead of removing every contact. Saves are a read-modify-write of the party\nrow (last writer wins). Map saved contacts to `CustomerDisplayData.contacts` /\n`VendorDisplayData.contacts` for the detail screens.\n\n## Selectors (#3602)\n\n`CustomerSelect` and `VendorSelect` are field-sized pickers built on the shared\nsmrt-ui `RelationInput` (`@happyvertical/smrt-ui/forms`, ARIA 1.2 combobox),\nthrough one internal `PartySelect` adapter that maps party DTOs to relation\noptions and localizes the field's text. Search, resolve, debounce, stale-result\nand create handling live in `RelationInput`, not here.\n\n| Prop | Meaning |\n| --- | --- |\n| `name`, `value` (bindable), `label`, `required`, `disabled`, `error`, `placeholder`, `description` | Field basics. `label` defaults to the localized \"Customer\"/\"Vendor\"; `error` renders a linked `role=\"alert\"`. |\n| `search(query)` | Caller-owned lookup returning party DTOs; called with `''` when the list opens and, debounced (`debounceMs`, default 250), as the person types. |\n| `resolve(id)` | Caller-owned lookup of the current value's DTO when it has not been listed; without it a value stays blank until searched. |\n| `onCreate(query)` | Optional. Renders \"+ New customer/vendor\" (`createLabel` overrides); a returned DTO is selected. |\n| `onchange(id)`, `interaction` | Change callback (`''` when cleared) and agent-interaction options. |\n\nRows show `profile.name` with localized status and, for customers, type\n(`Active · Wholesale`). Data never enters the component: no stores, no fetches,\nno tenant logic. A failed `search` shows an inline status, not an exception;\na failed `onCreate` leaves the field as it was. Text comes from the\n`commerce.select.*` messages. The selectors are registered as the\n`customer-select` and `vendor-select` slots, whose `selects` qualified names\nare emitted into the manifest's `uiSelectors`; keep `selects` a string literal\nso the scanner can read it.\n\nWidget hints (`@field({ ui: { widget } })`, #3599) are presentation-only and\nthe build rejects them on non-text fields; `currency` is a currency-code\npicker for ISO 4217 strings and is never applied to minor-unit amounts;\namounts take `ui: { format: 'money' }` instead (#3718).\nBehaviour tests run under jsdom: `pnpm exec vitest run --config\nvitest.selects.config.ts` (also part of `pnpm test:ui`).\n\n## Record page hints (#3718)\n\nMinor-unit amounts on Contract, Invoice, their line items, Payment,\nPaymentAllocation, `Customer.creditLimit` and `Vendor.minimumOrderAmount`\ncarry `ui: { format: 'money' }` (#3718; `widget: 'currency'` stays the\ncurrency-code picker on the text `currency` field). Payout and Payment's\nnative/USD drift amounts are not hinted: their currency is a payout rail or\nfixed USD, not the record's `currency`. The line-item FKs\n(`ContractLineItem.contractId`, `InvoiceLineItem.invoiceId`,\n`FulfillmentLineItem.fulfillmentId`) carry `role: 'items'`, so record pages\nrender them as a line-items table with a totals block;\n`src/__tests__/money-hints-manifest.test.ts` pins both.\n"
19884
20469
  },
20470
+ {
20471
+ "path": "README.md",
20472
+ "module": "README",
20473
+ "content": "# @happyvertical/smrt-commerce\n\nCommerce models for the s-m-r-t framework. Covers customers, vendors, contracts (9 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 {\n Customer, CustomerCollection,\n Order, ContractCollection,\n Invoice, InvoiceCollection,\n Payment, PaymentCollection,\n ContractStatus, InvoiceStatus, PaymentMethod\n} from '@happyvertical/smrt-commerce';\n\n// Create a customer linked to a profile\nconst customers = await CustomerCollection.create({ db });\nconst customer = await customers.create({\n profileId: 'profile-uuid',\n creditLimit: 1000000, // $10,000.00 — money is integer minor units\n paymentTerms: 'Net 30',\n});\nawait customer.save();\n\n// Create an order (STI contract type)\nconst contracts = await ContractCollection.create({ db });\nconst order = await contracts.create({\n _meta_type: 'Order',\n customerId: customer.id,\n subtotal: 100000, // $1,000.00\n taxAmount: 5000, // $50.00\n totalAmount: 105000, // $1,050.00\n currency: 'CAD',\n});\nawait order.save();\n\n// Create an invoice for the order\nconst invoices = await InvoiceCollection.create({ db });\nconst invoiceNumber = await invoices.generateInvoiceNumber();\nconst invoice = await invoices.create({\n customerId: customer.id,\n contractId: order.id,\n invoiceNumber,\n subtotal: 100000, // $1,000.00\n taxAmount: 5000, // $50.00\n totalAmount: 105000, // $1,050.00\n});\nawait invoice.save();\n\n// Recognize revenue (creates balanced journal in smrt-ledgers)\nawait invoice.recognizeRevenue({\n arAccountId: 'ar-account-id',\n revenueAccountId: 'revenue-account-id',\n taxAccountId: 'tax-account-id',\n});\n\n// Record a payment\nconst payments = await PaymentCollection.create({ db });\nconst payment = await payments.create({\n contractId: order.id,\n customerId: customer.id,\n amount: 105000,\n method: PaymentMethod.CREDIT_CARD,\n});\nawait payment.save();\n\n// Record payment with ledger integration\nawait payment.recordPayment({\n ledgerId: 'ledger-id',\n receivablesAccountId: 'ar-account-id',\n cashAccountId: 'bank-account-id',\n});\n```\n\n## API\n\n### Models\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 nine 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` requires 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| `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`, `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.\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 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`.\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`InvoiceLineEditor` and `InvoiceEditor` from `@happyvertical/smrt-commerce/svelte`\nprovide editable descriptions/SKUs, fractional quantities, unit prices, flat or\npercentage discounts, explicit or inherited tax, customer, dates, currency and\nterms. Calculated previews use the same exact minor-unit arithmetic exported\nfor server handlers from `@happyvertical/smrt-commerce/invoices`.\n`InvoiceAllocationFields` and `InvoiceReview` extend this foundation with\ncaller-owned sources and approvals. See [the editor contract](agents/invoice-preparation.md),\n[calculation rules](agents/invoice-calculations.md), and the required\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"
20474
+ },
19885
20475
  {
19886
20476
  "path": "agents/invoice-quantity.md",
19887
20477
  "module": "invoice-quantity",
@@ -19900,12 +20490,12 @@
19900
20490
  {
19901
20491
  "path": "agents/quote-ui.md",
19902
20492
  "module": "quote-ui",
19903
- "content": "# Quotation and estimate presentation\n\nImport `QuoteEditor`, `QuoteRevisionHistory`, `QuoteRevisionComparison` and their\nProps/DTOs from `@happyvertical/smrt-commerce/svelte`. Registry slots are\n`quote-editor`, `quote-revision-history`, and `quote-revision-comparison`.\nCommerce playground has interactive vendor quotation and customer estimate demos.\n\n`kind: 'vendor-quotation'` means received supplier evidence;\n`kind: 'customer-estimate'` means pricing proposed to a customer. Neither widget\nselects pricing, accepts budgets, awards work, sends messages or mutates a model.\nAdapt public Commerce Estimate/Contract fields (`currency`, `totalAmount`,\n`status`, `customerId`/`vendorId`) to these presentation DTOs;\nconstruction package allocation and approval policy remain caller extensions.\nClient records need not become Commerce Customers.\n\n`QuoteRevision.total.amountMinor` is a safe integer or null (unknown), with an\nexplicit `minorUnitDigits` from the owning currency/asset contract. No locale\ninference, monetary rounding or unknown-as-zero comparison occurs. Different\nkinds/currencies/scales cannot produce a delta. Existing invoice `LineItem`\nmajor-unit props retain their established contract. Form money values are raw\n**major-unit strings**: the caller validates and converts before saving; invalid\nstrings remain visible after failed actions. Dates likewise remain text to avoid\nnative date inputs silently clearing rejected server values.\n\nSupply `values`, authorized `counterparties`, `action`, optional `method`,\n`hiddenFields` (name/value pairs), `errors` and `message`. `names` maps payload\nnames to an existing endpoint. `labels.counterparty = 'Client'` supports local\nterminology. `extensions` composes pricing-source/allocation fields;\n`attachments` composes private Assets controls without loading bytes here.\n`QuoteRevisionHistory.evidence(revision)` composes authorized document links.\nAll fixed component text uses the shared i18n catalog; data/status/date labels\ncome from the caller.\n\nCurrency uses the shared `CurrencySelect`, localized by the active smrt-ui i18n\ncontext. Its native name still comes from `names.currency`, and rejected or\nunknown raw strings remain selected for a corrected submission. Pass\n`currencyOptions` only when the caller must restrict the catalog.\n\nRows submit repeated `lineId`, `lineDescription`, `lineQuantity`, `lineUnitRate`.\nDefault submitter `intent` is `save`, `addLine`, or `removeLine:<stable-id>`;\noverride via `intents`. Add/remove are ordinary `formnovalidate` POSTs, including\nall current strings and hidden tokens even without JavaScript. The action must\nread all repeated fields, preserve row IDs, and re-render them; add/remove are\nform-edit operations, not financial writes. Never treat an add-row POST as a save.\n\nThe editor does not generate or rotate submission tokens. Caller-owned\n`@happyvertical/smrt-ui/form-retry` can be attached via `formAttributes` using\nSvelte's `createAttachmentKey()` and `retry.attach`; caller transport enhancement\nmay use `onsubmit`. Use the same server-owned hidden identity when retrying an\nuncertain commit. Never blindly reset after a response. The caller must preserve\nhidden tenant/request/predecessor values, validate authorization on every action,\nand enforce idempotency transactionally. See `docs/content/form-retry.md`.\n`readonly`/`busy` only disable the presented fields; these props are not security.\n\nValidation: `pnpm --filter @happyvertical/smrt-commerce exec vitest run --config vitest.quotes.config.ts`,\n`node packages/commerce/test-support/quote-browser.mjs`, package typecheck/build.\nThe browser harness executes actual SSR POST recovery with JavaScript disabled\nand hydrated demo interactions at 390px. Full release CI/E2E runs at #3423's\nconsolidated release boundary by explicit owner instruction.\n\nOn NixOS, the browser lane accepts the repository-standard\n`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/run/current-system/sw/bin/chromium`.\nSet `QUOTE_EVIDENCE_DIR` and `CI_TEST_TMPDIR` to an owned evidence directory outside\n`/tmp`. Other commercial tests may reuse its host via\n`QUOTE_BROWSER_EXTENSION=/absolute/fixture.mjs`: export `handleRequest(req, res,\n{vite, render, css, requests})` returning true when handled, and\n`run({browser, baseUrl, evidence, requests})` for additional assertions.\n\nInvalid or unsafe display amounts/scales render localized unavailable text.\nStrict money helpers continue to reject malformed data; presentation recovery\ndoes not validate or authorize submitted money.\n\nThe `attachments` snippet renders outside the editor form so an Assets\n`AttachmentPanel`/`AttachmentUpload` keeps its own multipart action and request\nidentity. The attachment region remains disabled while the editor is read-only\nor busy. Use `extensions` only for controls belonging to the editor submission;\nnever place another form there.\n\nComposition regression: run the quote SSR lane, then the maintained quote browser\nhost with `QUOTE_BROWSER_EXTENSION=packages/commerce/test-support/quote-attachments-browser.mjs`.\nIt submits actual Assets upload bytes and the separate editor action with and\nwithout JavaScript at 390px.\n"
20493
+ "content": "# Quotation and estimate presentation\n\nImport `QuoteEditor`, `QuoteRevisionHistory`, `QuoteRevisionComparison` and their\nProps/DTOs from `@happyvertical/smrt-commerce/svelte`. Registry slots are\n`quote-editor`, `quote-revision-history`, and `quote-revision-comparison`.\nCommerce playground has interactive vendor quotation and customer estimate demos.\n\n`kind: 'vendor-quotation'` means received supplier evidence;\n`kind: 'customer-estimate'` means pricing proposed to a customer. Neither widget\nselects pricing, accepts budgets, awards work, sends messages or mutates a model.\nAdapt Commerce `Estimate` fields (`currency`, `totalAmount`, `status`,\n`customerId`) to customer-estimate DTOs. A received vendor quotation is\ncaller-held evidence or a draft `PurchaseOrder`, not an `Estimate.vendorId`;\nconstruction package allocation and approval policy remain caller extensions.\nClient records need not become Commerce Customers.\n\n`QuoteRevision.total.amountMinor` is a safe integer or null (unknown), with an\nexplicit `minorUnitDigits` from the owning currency/asset contract. No locale\ninference, monetary rounding or unknown-as-zero comparison occurs. Different\nkinds/currencies/scales cannot produce a delta. Existing invoice `LineItem`\nmajor-unit props retain their established contract. Form money values are raw\n**major-unit strings**: the caller validates and converts before saving; invalid\nstrings remain visible after failed actions. Dates likewise remain text to avoid\nnative date inputs silently clearing rejected server values.\n\nSupply `values`, authorized `counterparties`, `action`, optional `method`,\n`hiddenFields` (name/value pairs), `errors` and `message`. `names` maps payload\nnames to an existing endpoint. `labels.counterparty = 'Client'` supports local\nterminology. `extensions` composes pricing-source/allocation fields;\n`attachments` composes private Assets controls without loading bytes here.\n`QuoteRevisionHistory.evidence(revision)` composes authorized document links.\nAll fixed component text uses the shared i18n catalog; data/status/date labels\ncome from the caller.\n\nCurrency uses the shared `CurrencySelect`, localized by the active smrt-ui i18n\ncontext. Its native name still comes from `names.currency`, and rejected or\nunknown raw strings remain selected for a corrected submission. Pass\n`currencyOptions` only when the caller must restrict the catalog.\n\nRows submit repeated `lineId`, `lineDescription`, `lineQuantity`, `lineUnitRate`.\nDefault submitter `intent` is `save`, `addLine`, or `removeLine:<stable-id>`;\noverride via `intents`. Add/remove are ordinary `formnovalidate` POSTs, including\nall current strings and hidden tokens even without JavaScript. The action must\nread all repeated fields, preserve row IDs, and re-render them; add/remove are\nform-edit operations, not financial writes. Never treat an add-row POST as a save.\n\nThe editor does not generate or rotate submission tokens. Caller-owned\n`@happyvertical/smrt-ui/form-retry` can be attached via `formAttributes` using\nSvelte's `createAttachmentKey()` and `retry.attach`; caller transport enhancement\nmay use `onsubmit`. Use the same server-owned hidden identity when retrying an\nuncertain commit. Never blindly reset after a response. The caller must preserve\nhidden tenant/request/predecessor values, validate authorization on every action,\nand enforce idempotency transactionally. See `docs/content/form-retry.md`.\n`readonly`/`busy` only disable the presented fields; these props are not security.\n\nValidation: `pnpm --filter @happyvertical/smrt-commerce exec vitest run --config vitest.quotes.config.ts`,\n`node packages/commerce/test-support/quote-browser.mjs`, package typecheck/build.\nThe browser harness executes actual SSR POST recovery with JavaScript disabled\nand hydrated demo interactions at 390px. Full release CI/E2E runs at #3423's\nconsolidated release boundary by explicit owner instruction.\n\nOn NixOS, the browser lane accepts the repository-standard\n`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/run/current-system/sw/bin/chromium`.\nSet `QUOTE_EVIDENCE_DIR` and `CI_TEST_TMPDIR` to an owned evidence directory outside\n`/tmp`. Other commercial tests may reuse its host via\n`QUOTE_BROWSER_EXTENSION=/absolute/fixture.mjs`: export `handleRequest(req, res,\n{vite, render, css, requests})` returning true when handled, and\n`run({browser, baseUrl, evidence, requests})` for additional assertions.\n\nInvalid or unsafe display amounts/scales render localized unavailable text.\nStrict money helpers continue to reject malformed data; presentation recovery\ndoes not validate or authorize submitted money.\n\nThe `attachments` snippet renders outside the editor form so an Assets\n`AttachmentPanel`/`AttachmentUpload` keeps its own multipart action and request\nidentity. The attachment region remains disabled while the editor is read-only\nor busy. Use `extensions` only for controls belonging to the editor submission;\nnever place another form there.\n\nComposition regression: run the quote SSR lane, then the maintained quote browser\nhost with `QUOTE_BROWSER_EXTENSION=packages/commerce/test-support/quote-attachments-browser.mjs`.\nIt submits actual Assets upload bytes and the separate editor action with and\nwithout JavaScript at 390px.\n"
19904
20494
  },
19905
20495
  {
19906
20496
  "path": "agents/purchasing-ui.md",
19907
20497
  "module": "purchasing-ui",
19908
- "content": "# Purchasing and award presentation\n\nPublic `@happyvertical/smrt-commerce/svelte` exports `PurchaseSourceSelector`,\n`PurchaseOrderEditor`, their Props and `Purchase*` DTOs. Registry slots are\n`purchase-source-selector` and `purchase-order-editor`. Interactive playground\nentries cover ordinary amendment and explicit retained-allocation reduction.\n\nThese components use public Commerce PurchaseOrder/Contract as their owning\nfinancial domain without adding a backend command, ledger or fixed approval\npolicy. The caller decides instrument options, source availability, allocation\nlabels, prior commitments, reviewed actuals, floors/ceilings and exact source\nidentity. Construction work packages, accepted budgets, contractor entitlement\nand vendor award policy stay in the consumer. Capability/disabled props only\ncontrol presentation. Every POST needs authoritative server authorization.\n\n`PurchaseSourceSelector` uses a native GET to the supplied action with `name`\n(default `source`) and optional hidden search/context fields. It never writes an\naward. Disabled sources remain visible with caller notices; unknown saved IDs\nremain visible as unavailable. Keep source selection outside a reduction form;\nthe server must reject a downward amendment under an ordinary amendment mode\nand enforce the retained source/instrument during reductions.\n\n`PurchaseOrderEditor` receives `operation` (`create`, `amend`, `reduce`), a\nretained `source`, `currency`, explicit `minorUnitDigits`, raw `values`,\n`instruments`, native POST `action`, and caller-owned `hiddenFields`. Supply\nsource ID, currency, predecessor, tenant and retry IDs in those hidden fields\nunder the exact names your endpoint expects. `names` adapts all built-in fields.\n`labels` adapts approval language; `extensions` and `attachments` compose domain\nfields and authorized Assets evidence without fetching bytes in this component.\n`formAttributes` supports native encoding/ID and caller-owned form-retry Svelte\nattachments; `onsubmit` supports caller transport enhancement. Tokens are never\nminted or rotated here. See the shared [form/action contract](commercial-ui.md)\nand repository `docs/content/form-retry.md`.\n\nAll editable money is **raw major-unit text**, including malformed values and\nempty unknown amounts. The caller parses against the exact currency contract;\nthere is no UI floating-point calculation or implicit blank-to-zero conversion.\nSaved/source/review numbers are safe integer minor units formatted by the\nshared quotation helper. `sourceMinor: null` renders Unknown. Displayed bounds\nare guidance; the server validates floors, ceilings and source eligibility.\n\nEvery allocation posts its stable `id` and amount, even when zero. Mark every\npredecessor allocation `retained: true`. A `reduce` form requires those amounts\nexplicitly and never offers their removal. Supplying fewer predecessor rows is\nnot safe: the server must compare the complete ID set and reject omissions.\nOptional `rowActions` uses native formnovalidate add/remove POSTs carrying all\ncurrent fields; the caller re-renders unchanged rejected strings and IDs.\nNew-row IDs belong to the caller. Row editing never records a commitment.\n\nPressing Enter in an input activates review or record, preserving draft rows.\nExplicit add/remove actions remain available without JavaScript.\nDefault submitter `intent` values are `review`, `record`, `edit`, all configurable.\nOnly a caller-produced `review` snapshot or a retained `retryFingerprint` exposes\nexplicit confirmation. `review.fingerprint` posts under `reviewFingerprint` by\ndefault. After an uncertain outcome, re-render the exact submitted values,\nrequest identity and fingerprint; never fabricate a new intent/token. Confirmation\nis an unchecked required checkbox on native renders and when a new review\nfingerprint arrives in a hydrated form. Its value defaults to\n`yes`. The server must bind fingerprint to all submitted values and authority,\nreject changed/stale review data, and replay identical committed retries.\nRendering a review snapshot or selecting a source never accepts/records anything.\n\nValidation: `pnpm --filter @happyvertical/smrt-commerce exec vitest run --config\nvitest.purchasing.config.ts`, owning build/typecheck/pack checks, and the shared\nquote native/hydrated browser harness with `QUOTE_BROWSER_EXTENSION` pointing at\n`packages/commerce/test-support/purchasing-browser-extension.mjs`. Full repository\nCI/E2E is consolidated at the #3423 release boundary by owner instruction.\n\nInvalid or unsafe display amounts/scales render localized unavailable text.\nStrict money helpers continue to reject malformed data; presentation recovery\ndoes not validate or authorize submitted money.\n\nThe `attachments` snippet renders outside the editor form so an Assets\n`AttachmentPanel`/`AttachmentUpload` keeps its own multipart action and request\nidentity. The attachment region remains disabled while the editor is read-only\nor busy. Use `extensions` only for controls belonging to the editor submission;\nnever place another form there.\n\nComposition regression: run the quote SSR lane, then the maintained quote browser\nhost with `QUOTE_BROWSER_EXTENSION=packages/commerce/test-support/quote-attachments-browser.mjs`.\nIt submits actual Assets upload bytes and the separate editor action with and\nwithout JavaScript at 390px.\n\n## Purchase-order header\n\n`PurchaseOrderHeaderForm` (`purchase-order-header-form` registry slot) edits only\n`vendorId`, `reference`, `notes`, `expectedDate` and `shipTo`. It does not amend\nawards, allocations, totals or status. Supply `action`, caller-authorized `vendors`,\nraw `values`, canonical-key `errors`, optional `message`, `hiddenFields` and\n`names` for an existing endpoint. The native POST submitter defaults to\n`intent=saveHeader`; `intent`, `onsubmit` and `formAttributes` adapt transport.\nReadonly/busy are presentation controls, never authorization. The server must\ncheck active authority, vendor eligibility and any order-status edit policy.\n\nAll header input remains text: even rejected calendar dates survive a rerender.\nThe server validates `expectedDate` as YYYY-MM-DD or empty before saving. An\nunknown retained vendor appears explicitly unavailable but retains its ID on POST\nso a rejected submission cannot silently clear it. The caller must reject an\nunavailable vendor or explicitly permit keeping the existing relationship.\n`shipTo` is a delivery address/instructions snapshot, not a Profile reference.\n`PurchaseOrder.expectedDate` and `.shipTo` use existing STI metadata, default to\nempty strings, and accept `PurchaseOrderOptions`; no table migration is needed.\nExisting Contract fields own the other three values. Header persistence uses the\nexisting SQLite/PostgreSQL Contract schema; DuckDB rejects its existing\nON UPDATE CASCADE foreign keys and is not a supported Contract storage lane. Caller retry identities\nare rendered unchanged; the form itself never persists or retries a mutation.\n\nHeader regression evidence uses the purchasing SSR suite and the shared quote\nbrowser host with `QUOTE_BROWSER_EXTENSION=packages/commerce/test-support/purchasing-header-browser.mjs`.\n"
20498
+ "content": "# Purchasing and award presentation\n\nPublic `@happyvertical/smrt-commerce/svelte` exports `PurchaseSourceSelector`,\n`PurchaseOrderEditor`, their Props and `Purchase*` DTOs. Registry slots are\n`purchase-source-selector` and `purchase-order-editor`. Interactive playground\nentries cover ordinary amendment and explicit retained-allocation reduction.\n\nThese components use public Commerce PurchaseOrder/Contract as their owning\nfinancial domain without adding a backend command, ledger or fixed approval\npolicy. The caller decides instrument options, source availability, allocation\nlabels, prior commitments, reviewed actuals, floors/ceilings and exact source\nidentity. Construction work packages, accepted budgets, contractor entitlement\nand vendor award policy stay in the consumer. Capability/disabled props only\ncontrol presentation. Every POST needs authoritative server authorization.\n\n`PurchaseSourceSelector` uses a native GET to the supplied action with `name`\n(default `source`) and optional hidden search/context fields. It never writes an\naward. Disabled sources remain visible with caller notices; unknown saved IDs\nremain visible as unavailable. Keep source selection outside a reduction form;\nthe server must reject a downward amendment under an ordinary amendment mode\nand enforce the retained source/instrument during reductions.\n\n`PurchaseOrderEditor` receives `operation` (`create`, `amend`, `reduce`), a\nretained `source`, `currency`, explicit `minorUnitDigits`, raw `values`,\n`instruments`, native POST `action`, and caller-owned `hiddenFields`. Supply\nsource ID, currency, predecessor, tenant and retry IDs in those hidden fields\nunder the exact names your endpoint expects. `names` adapts all built-in fields.\n`labels` adapts approval language; `extensions` and `attachments` compose domain\nfields and authorized Assets evidence without fetching bytes in this component.\n`formAttributes` supports native encoding/ID and caller-owned form-retry Svelte\nattachments; `onsubmit` supports caller transport enhancement. Tokens are never\nminted or rotated here. See the shared [form/action contract](commercial-ui.md)\nand repository `docs/content/form-retry.md`.\n\nAll editable money is **raw major-unit text**, including malformed values and\nempty unknown amounts. The caller parses against the exact currency contract;\nthere is no UI floating-point calculation or implicit blank-to-zero conversion.\nSaved/source/review numbers are safe integer minor units formatted by the\nshared quotation helper. `sourceMinor: null` renders Unknown. Displayed bounds\nare guidance; the server validates floors, ceilings and source eligibility.\n\nEvery allocation posts its stable `id` and amount, even when zero. Mark every\npredecessor allocation `retained: true`. A `reduce` form requires those amounts\nexplicitly and never offers their removal. Supplying fewer predecessor rows is\nnot safe: the server must compare the complete ID set and reject omissions.\nOptional `rowActions` uses native formnovalidate add/remove POSTs carrying all\ncurrent fields; the caller re-renders unchanged rejected strings and IDs.\nNew-row IDs belong to the caller. Row editing never records a commitment.\n\nPressing Enter in an input activates review or record, preserving draft rows.\nExplicit add/remove actions remain available without JavaScript.\nDefault submitter `intent` values are `review`, `record`, `edit`, all configurable.\nOnly a caller-produced `review` snapshot or a retained `retryFingerprint` exposes\nexplicit confirmation. `review.fingerprint` posts under `reviewFingerprint` by\ndefault. After an uncertain outcome, re-render the exact submitted values,\nrequest identity and fingerprint; never fabricate a new intent/token. Confirmation\nis an unchecked required checkbox on native renders and when a new review\nfingerprint arrives in a hydrated form. Its value defaults to\n`yes`. The server must bind fingerprint to all submitted values and authority,\nreject changed/stale review data, and replay identical committed retries.\nRendering a review snapshot or selecting a source never accepts/records anything.\n\nValidation: `pnpm --filter @happyvertical/smrt-commerce exec vitest run --config\nvitest.purchasing.config.ts`, owning build/typecheck/pack checks, and the shared\nquote native/hydrated browser harness with `QUOTE_BROWSER_EXTENSION` pointing at\n`packages/commerce/test-support/purchasing-browser-extension.mjs`. Full repository\nCI/E2E is consolidated at the #3423 release boundary by owner instruction.\n\nInvalid or unsafe display amounts/scales render localized unavailable text.\nStrict money helpers continue to reject malformed data; presentation recovery\ndoes not validate or authorize submitted money.\n\nThe `attachments` snippet renders outside the editor form so an Assets\n`AttachmentPanel`/`AttachmentUpload` keeps its own multipart action and request\nidentity. The attachment region remains disabled while the editor is read-only\nor busy. Use `extensions` only for controls belonging to the editor submission;\nnever place another form there.\n\nComposition regression: run the quote SSR lane, then the maintained quote browser\nhost with `QUOTE_BROWSER_EXTENSION=packages/commerce/test-support/quote-attachments-browser.mjs`.\nIt submits actual Assets upload bytes and the separate editor action with and\nwithout JavaScript at 390px.\n\n## Purchase-order header\n\n`PurchaseOrderHeaderForm` (`purchase-order-header-form` registry slot) edits only\n`vendorId`, `reference`, `notes`, `expectedDate` and `shipTo`. It does not amend\nawards, allocations, totals or status. Supply `action`, caller-authorized `vendors`,\nraw `values`, canonical-key `errors`, optional `message`, `hiddenFields` and\n`names` for an existing endpoint. The native POST submitter defaults to\n`intent=saveHeader`; `intent`, `onsubmit` and `formAttributes` adapt transport.\nReadonly/busy are presentation controls, never authorization. The server must\ncheck active authority, vendor eligibility and any order-status edit policy.\n\nAll header input remains text: even rejected calendar dates survive a rerender.\nThe server validates `expectedDate` as YYYY-MM-DD or empty before saving. An\nunknown retained vendor appears explicitly unavailable but retains its ID on POST\nso a rejected submission cannot silently clear it. The caller must reject an\nunavailable vendor or explicitly permit keeping the existing relationship.\n`shipTo` is a delivery address/instructions snapshot, not a Profile reference.\n`PurchaseOrder.expectedDate` and `.shipTo` use existing STI metadata, default to\nempty strings, and accept `PurchaseOrderOptions`; no table migration is needed.\n`PurchaseContract` owns `vendorId`; `Contract` owns `reference` and `notes`.\nHeader persistence uses the existing SQLite/PostgreSQL Contract schema. DuckDB\nrejects its existing ON UPDATE CASCADE foreign keys and is not a supported\nContract storage lane. Caller retry identities\nare rendered unchanged; the form itself never persists or retries a mutation.\n\nHeader regression evidence uses the purchasing SSR suite and the shared quote\nbrowser host with `QUOTE_BROWSER_EXTENSION=packages/commerce/test-support/purchasing-header-browser.mjs`.\n"
19909
20499
  },
19910
20500
  {
19911
20501
  "path": "agents/cost-billing.md",
@@ -19967,11 +20557,7 @@
19967
20557
  },
19968
20558
  "customerId": {
19969
20559
  "label": "Customer",
19970
- "order": 1,
19971
- "required": true
19972
- },
19973
- "vendorId": {
19974
- "visibility": "hidden"
20560
+ "order": 1
19975
20561
  },
19976
20562
  "subtotal": {
19977
20563
  "visibility": "hidden"
@@ -20132,14 +20718,9 @@
20132
20718
  "visibility": "hidden",
20133
20719
  "locked": true
20134
20720
  },
20135
- "vendorId": {
20136
- "visibility": "hidden",
20137
- "locked": true
20138
- },
20139
20721
  "customerId": {
20140
20722
  "label": "Customer",
20141
- "order": 1,
20142
- "required": true
20723
+ "order": 1
20143
20724
  },
20144
20725
  "channelId": {
20145
20726
  "visibility": "hidden"
@@ -20504,11 +21085,7 @@
20504
21085
  },
20505
21086
  "customerId": {
20506
21087
  "label": "Customer",
20507
- "order": 1,
20508
- "required": true
20509
- },
20510
- "vendorId": {
20511
- "visibility": "hidden"
21088
+ "order": 1
20512
21089
  },
20513
21090
  "subtotal": {
20514
21091
  "visibility": "hidden"
@@ -20609,11 +21186,7 @@
20609
21186
  },
20610
21187
  "customerId": {
20611
21188
  "label": "Customer",
20612
- "order": 1,
20613
- "required": true
20614
- },
20615
- "vendorId": {
20616
- "visibility": "hidden"
21189
+ "order": 1
20617
21190
  },
20618
21191
  "subtotal": {
20619
21192
  "visibility": "hidden"
@@ -20707,10 +21280,6 @@
20707
21280
  "visibility": "hidden",
20708
21281
  "locked": true
20709
21282
  },
20710
- "customerId": {
20711
- "visibility": "hidden",
20712
- "locked": true
20713
- },
20714
21283
  "vendorId": {
20715
21284
  "label": "Vendor",
20716
21285
  "order": 1
@@ -20813,14 +21382,9 @@
20813
21382
  "visibility": "hidden",
20814
21383
  "locked": true
20815
21384
  },
20816
- "vendorId": {
20817
- "visibility": "hidden",
20818
- "locked": true
20819
- },
20820
21385
  "customerId": {
20821
21386
  "label": "Customer",
20822
- "order": 1,
20823
- "required": true
21387
+ "order": 1
20824
21388
  },
20825
21389
  "channelId": {
20826
21390
  "visibility": "hidden"
@@ -20975,10 +21539,6 @@
20975
21539
  "visibility": "hidden",
20976
21540
  "locked": true
20977
21541
  },
20978
- "vendorId": {
20979
- "visibility": "hidden",
20980
- "locked": true
20981
- },
20982
21542
  "customerId": {
20983
21543
  "label": "Customer",
20984
21544
  "order": 1