@happyvertical/smrt-commerce 0.56.0 → 0.56.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -8
- package/README.md +98 -4
- package/agents/accounting-snapshot-mapper.md +111 -0
- package/agents/contract-money.md +24 -0
- package/agents/cost-billing.md +7 -2
- package/agents/customer-vendor-ui.md +7 -1
- package/agents/document-money.md +127 -0
- package/agents/invoice-calculations.md +14 -2
- package/agents/line-amount-migration.md +50 -0
- package/agents/minor-unit-migration.md +20 -0
- package/agents/money-calculator.md +45 -0
- package/agents/money-model.md +113 -0
- package/agents/revenue-journal.md +23 -0
- package/agents/tax-codes.md +48 -0
- package/agents/tax-summary.md +128 -0
- package/dist/accounting-snapshot.d.ts +79 -0
- package/dist/accounting-snapshot.d.ts.map +1 -0
- package/dist/billing/runtime.d.ts +2 -0
- package/dist/billing/runtime.d.ts.map +1 -1
- package/dist/chunks/ContractLineItemCollection--BrGMtNz.js +265 -0
- package/dist/chunks/ContractLineItemCollection--BrGMtNz.js.map +1 -0
- package/dist/chunks/CustomerCollection-CjGaZSpf.js +99 -0
- package/dist/chunks/CustomerCollection-CjGaZSpf.js.map +1 -0
- package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js +1659 -0
- package/dist/chunks/DocumentTaxCollection-KOkaQT2M.js.map +1 -0
- package/dist/chunks/InvoiceCollection-BUe_8FT1.js +1047 -0
- package/dist/chunks/InvoiceCollection-BUe_8FT1.js.map +1 -0
- package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js +468 -0
- package/dist/chunks/InvoiceLineItemCollection-CPQGghyd.js.map +1 -0
- package/dist/chunks/TaxCodeCollection-DF2eaJed.js +149 -0
- package/dist/chunks/TaxCodeCollection-DF2eaJed.js.map +1 -0
- package/dist/chunks/TaxComponentCollection-3x24xSNh.js +129 -0
- package/dist/chunks/TaxComponentCollection-3x24xSNh.js.map +1 -0
- package/dist/chunks/TaxSettingsCollection-DYYF-In-.js +319 -0
- package/dist/chunks/TaxSettingsCollection-DYYF-In-.js.map +1 -0
- package/dist/chunks/{money-DfrRxrel.js → allocation-integrity-Cq0g6CfO.js} +7 -12
- package/dist/chunks/allocation-integrity-Cq0g6CfO.js.map +1 -0
- package/dist/chunks/{calculations-Bz7RB3qK.js → calculations-DaHR17il.js} +134 -54
- package/dist/chunks/calculations-DaHR17il.js.map +1 -0
- package/dist/chunks/credit-allocation-yspz0Lab.js +121 -0
- package/dist/chunks/credit-allocation-yspz0Lab.js.map +1 -0
- package/dist/chunks/document-money-oUDvdjOf.js +860 -0
- package/dist/chunks/document-money-oUDvdjOf.js.map +1 -0
- package/dist/chunks/invoice-posting-CF3fE_Ec.js +229 -0
- package/dist/chunks/invoice-posting-CF3fE_Ec.js.map +1 -0
- package/dist/chunks/money-rational-CcW7gzG5.js +64 -0
- package/dist/chunks/money-rational-CcW7gzG5.js.map +1 -0
- package/dist/chunks/revenue-journal-DY9fro8E.js +140 -0
- package/dist/chunks/revenue-journal-DY9fro8E.js.map +1 -0
- package/dist/chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/chunks/tenant-money-barrier-y6EAa6vY.js +135 -0
- package/dist/chunks/tenant-money-barrier-y6EAa6vY.js.map +1 -0
- package/dist/chunks/{types-zVnIjSOk.js → types-Bbm0Xflc.js} +4 -14
- package/dist/chunks/types-Bbm0Xflc.js.map +1 -0
- package/dist/chunks/{units-BpZ0_Dpk.js → units-CNcjH_eE.js} +2 -2
- package/dist/chunks/{units-BpZ0_Dpk.js.map → units-CNcjH_eE.js.map} +1 -1
- package/dist/collections/ContractCollection.d.ts +35 -1
- package/dist/collections/ContractCollection.d.ts.map +1 -1
- package/dist/collections/ContractLineItemCollection.d.ts +26 -1
- package/dist/collections/ContractLineItemCollection.d.ts.map +1 -1
- package/dist/collections/DocumentTaxCollection.d.ts +75 -0
- package/dist/collections/DocumentTaxCollection.d.ts.map +1 -0
- package/dist/collections/InvoiceCollection.d.ts +26 -1
- package/dist/collections/InvoiceCollection.d.ts.map +1 -1
- package/dist/collections/InvoiceLineItemCollection.d.ts +26 -1
- package/dist/collections/InvoiceLineItemCollection.d.ts.map +1 -1
- package/dist/collections/MoneyMigrationDocumentCollection.d.ts +12 -0
- package/dist/collections/MoneyMigrationDocumentCollection.d.ts.map +1 -0
- package/dist/collections/PaymentAllocationCollection.d.ts +2 -0
- package/dist/collections/PaymentAllocationCollection.d.ts.map +1 -1
- package/dist/collections/TaxCodeCollection.d.ts +7 -0
- package/dist/collections/TaxCodeCollection.d.ts.map +1 -0
- package/dist/collections/TaxComponentCollection.d.ts +6 -0
- package/dist/collections/TaxComponentCollection.d.ts.map +1 -0
- package/dist/collections/TaxSettingsCollection.d.ts +16 -0
- package/dist/collections/TaxSettingsCollection.d.ts.map +1 -0
- package/dist/collections/TaxSummaryReportCollection.d.ts +151 -0
- package/dist/collections/TaxSummaryReportCollection.d.ts.map +1 -0
- package/dist/collections/index.d.ts +6 -0
- package/dist/collections/index.d.ts.map +1 -1
- package/dist/commerce-money-tools.d.ts +9 -0
- package/dist/commerce-money-tools.d.ts.map +1 -0
- package/dist/contract-money.d.ts +27 -0
- package/dist/contract-money.d.ts.map +1 -0
- package/dist/cost-billing.d.ts +0 -1
- package/dist/cost-billing.d.ts.map +1 -1
- package/dist/cost-billing.js +10 -15
- package/dist/cost-billing.js.map +1 -1
- package/dist/credit-allocation.d.ts +48 -0
- package/dist/credit-allocation.d.ts.map +1 -0
- package/dist/document-money-internal.d.ts +44 -0
- package/dist/document-money-internal.d.ts.map +1 -0
- package/dist/document-money.d.ts +85 -0
- package/dist/document-money.d.ts.map +1 -0
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5411 -4249
- package/dist/index.js.map +1 -1
- package/dist/invoice-accounting-sync.d.ts +96 -0
- package/dist/invoice-accounting-sync.d.ts.map +1 -0
- package/dist/invoice-mcp.d.ts +16 -0
- package/dist/invoice-mcp.d.ts.map +1 -0
- package/dist/invoice-posting.d.ts +9 -0
- package/dist/invoice-posting.d.ts.map +1 -0
- package/dist/invoice-record-screen.d.ts +60 -0
- package/dist/invoice-record-screen.d.ts.map +1 -0
- package/dist/invoices.d.ts +2 -0
- package/dist/invoices.d.ts.map +1 -1
- package/dist/invoices.js +3 -2
- package/dist/manifest.json +12804 -6095
- package/dist/migrations/commerceLineAmounts.d.ts +84 -0
- package/dist/migrations/commerceLineAmounts.d.ts.map +1 -0
- package/dist/migrations/partyLinks.d.ts.map +1 -1
- package/dist/models/Contract.d.ts +34 -4
- package/dist/models/Contract.d.ts.map +1 -1
- package/dist/models/ContractLineItem.d.ts +43 -6
- package/dist/models/ContractLineItem.d.ts.map +1 -1
- package/dist/models/Customer.d.ts +7 -0
- package/dist/models/Customer.d.ts.map +1 -1
- package/dist/models/DocumentTax.d.ts +50 -0
- package/dist/models/DocumentTax.d.ts.map +1 -0
- package/dist/models/Invoice.d.ts +46 -2
- package/dist/models/Invoice.d.ts.map +1 -1
- package/dist/models/InvoiceLineItem.d.ts +46 -5
- package/dist/models/InvoiceLineItem.d.ts.map +1 -1
- package/dist/models/MoneyMigrationDocument.d.ts +28 -0
- package/dist/models/MoneyMigrationDocument.d.ts.map +1 -0
- package/dist/models/Payment.d.ts +5 -1
- package/dist/models/Payment.d.ts.map +1 -1
- package/dist/models/PaymentAllocation.d.ts.map +1 -1
- package/dist/models/TaxCode.d.ts +22 -0
- package/dist/models/TaxCode.d.ts.map +1 -0
- package/dist/models/TaxComponent.d.ts +21 -0
- package/dist/models/TaxComponent.d.ts.map +1 -0
- package/dist/models/TaxSettings.d.ts +33 -0
- package/dist/models/TaxSettings.d.ts.map +1 -0
- package/dist/models/TaxSummaryReport.d.ts +35 -0
- package/dist/models/TaxSummaryReport.d.ts.map +1 -0
- package/dist/models/Vendor.d.ts +9 -0
- package/dist/models/Vendor.d.ts.map +1 -1
- package/dist/models/index.d.ts +7 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/money-rational.d.ts +15 -0
- package/dist/money-rational.d.ts.map +1 -0
- package/dist/money.d.ts +106 -25
- package/dist/money.d.ts.map +1 -1
- package/dist/money.js +274 -0
- package/dist/money.js.map +1 -0
- package/dist/recipes.d.ts +15 -15
- package/dist/revenue-journal.d.ts +61 -0
- package/dist/revenue-journal.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +9621 -7592
- package/dist/svelte/components/InvoiceActions.svelte +12 -0
- package/dist/svelte/components/InvoiceActions.svelte.d.ts +5 -0
- package/dist/svelte/components/InvoiceActions.svelte.d.ts.map +1 -1
- package/dist/svelte/components/InvoiceEditor.svelte +15 -1
- package/dist/svelte/components/InvoiceEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/InvoiceLineItems.svelte +3 -3
- package/dist/svelte/components/InvoiceRecordScreen.svelte +94 -0
- package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts +21 -0
- package/dist/svelte/components/InvoiceRecordScreen.svelte.d.ts.map +1 -0
- package/dist/svelte/components/InvoiceTotals.svelte +2 -2
- package/dist/svelte/components/MoneyInvoiceEditor.svelte +282 -0
- package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts +31 -0
- package/dist/svelte/components/MoneyInvoiceEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PurchaseOrderEditor.svelte +53 -1
- package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts +5 -0
- package/dist/svelte/components/PurchaseOrderEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/components/QuoteEditor.svelte +54 -1
- package/dist/svelte/components/QuoteEditor.svelte.d.ts +5 -0
- package/dist/svelte/components/QuoteEditor.svelte.d.ts.map +1 -1
- package/dist/svelte/i18n.d.ts +16 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +16 -0
- package/dist/svelte/index.d.ts +7 -2
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +4 -1
- package/dist/svelte/invoices/calculations.d.ts +7 -2
- package/dist/svelte/invoices/calculations.d.ts.map +1 -1
- package/dist/svelte/invoices/calculations.js +141 -47
- package/dist/svelte/invoices/money-editor.d.ts +123 -0
- package/dist/svelte/invoices/money-editor.d.ts.map +1 -0
- package/dist/svelte/invoices/money-editor.js +101 -0
- package/dist/svelte/invoices/types.d.ts +5 -1
- package/dist/svelte/invoices/types.d.ts.map +1 -1
- package/dist/svelte/purchasing-i18n.d.ts +5 -0
- package/dist/svelte/purchasing-i18n.d.ts.map +1 -1
- package/dist/svelte/purchasing-i18n.js +5 -0
- package/dist/svelte/quote-i18n.d.ts +5 -0
- package/dist/svelte/quote-i18n.d.ts.map +1 -1
- package/dist/svelte/quote-i18n.js +5 -0
- package/dist/tax/cookbook.d.ts +22 -0
- package/dist/tax/cookbook.d.ts.map +1 -0
- package/dist/tax/example-pack.d.ts +25 -0
- package/dist/tax/example-pack.d.ts.map +1 -0
- package/dist/tax/identity.d.ts +5 -0
- package/dist/tax/identity.d.ts.map +1 -0
- package/dist/tax/index.d.ts +4 -0
- package/dist/tax/index.d.ts.map +1 -0
- package/dist/tax/legacy.d.ts +48 -0
- package/dist/tax/legacy.d.ts.map +1 -0
- package/dist/tax-summary-report-adapter.d.ts +37 -0
- package/dist/tax-summary-report-adapter.d.ts.map +1 -0
- package/dist/tax-summary.d.ts +129 -0
- package/dist/tax-summary.d.ts.map +1 -0
- package/dist/tenant-money-barrier.d.ts +66 -0
- package/dist/tenant-money-barrier.d.ts.map +1 -0
- package/dist/types/index.d.ts +138 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +19 -11
- package/dist/chunks/calculations-Bz7RB3qK.js.map +0 -1
- package/dist/chunks/money-DfrRxrel.js.map +0 -1
- package/dist/chunks/types-zVnIjSOk.js.map +0 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import { MoneyValidationError } from "../money.js";
|
|
3
|
+
import { SmrtCollection, SmrtObject, field, smrt } from "@happyvertical/smrt-core";
|
|
4
|
+
import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
|
|
5
|
+
//#region src/models/TaxCode.ts
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value
|
|
13
|
+
}) : obj[key] = value;
|
|
14
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
15
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
16
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
17
|
+
if (kind && result) __defProp(target, key, result);
|
|
18
|
+
return result;
|
|
19
|
+
};
|
|
20
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
21
|
+
var TaxCode = class extends SmrtObject {
|
|
22
|
+
tenantId = "";
|
|
23
|
+
code = "";
|
|
24
|
+
name = "";
|
|
25
|
+
kind = "taxable";
|
|
26
|
+
appliesTo = "both";
|
|
27
|
+
/** Ordered component IDs; compound is reserved and deliberately unsupported. */
|
|
28
|
+
/** Stored JSON is guarded through the typed accessor below. */
|
|
29
|
+
componentsJson = "[]";
|
|
30
|
+
get components() {
|
|
31
|
+
try {
|
|
32
|
+
const value = JSON.parse(this.componentsJson);
|
|
33
|
+
if (!Array.isArray(value)) return [];
|
|
34
|
+
return value.filter((part) => !!part && typeof part === "object" && typeof part.taxComponentId === "string" && typeof part.compound === "boolean");
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
set components(value) {
|
|
40
|
+
this.componentsJson = typeof value === "string" ? value : JSON.stringify(value);
|
|
41
|
+
}
|
|
42
|
+
active = true;
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
super(options);
|
|
45
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId ?? "";
|
|
46
|
+
if (options.code !== void 0) this.code = options.code;
|
|
47
|
+
if (options.name !== void 0) this.name = options.name;
|
|
48
|
+
if (options.kind !== void 0) this.kind = options.kind;
|
|
49
|
+
if (options.appliesTo !== void 0) this.appliesTo = options.appliesTo;
|
|
50
|
+
if (options.components !== void 0) this.components = structuredClone(options.components);
|
|
51
|
+
if (options.active !== void 0) this.active = options.active;
|
|
52
|
+
}
|
|
53
|
+
async save() {
|
|
54
|
+
if (!this.code.trim() || !this.name.trim()) throw new Error("Tax code requires a nonempty code and name");
|
|
55
|
+
if (![
|
|
56
|
+
"taxable",
|
|
57
|
+
"zero-rated",
|
|
58
|
+
"exempt",
|
|
59
|
+
"out-of-scope"
|
|
60
|
+
].includes(this.kind)) throw new Error("Tax code kind is invalid");
|
|
61
|
+
if (![
|
|
62
|
+
"sales",
|
|
63
|
+
"purchases",
|
|
64
|
+
"both"
|
|
65
|
+
].includes(this.appliesTo)) throw new Error("Tax code appliesTo is invalid");
|
|
66
|
+
const parts = this.parsedComponents();
|
|
67
|
+
if (!parts) throw new Error("Tax code components must be a valid JSON array");
|
|
68
|
+
const nonTaxable = this.kind !== "taxable";
|
|
69
|
+
if (nonTaxable && parts.length) throw new MoneyValidationError("TAX_CODE_NON_TAXABLE_COMPONENTS", `${this.kind} tax codes cannot have components.`);
|
|
70
|
+
if (!nonTaxable && !parts.length) throw new MoneyValidationError("TAX_CODE_TAXABLE_EMPTY", "Taxable tax codes require at least one component.");
|
|
71
|
+
const seen = /* @__PURE__ */ new Set();
|
|
72
|
+
for (const component of parts) {
|
|
73
|
+
if (seen.has(component.taxComponentId)) throw new MoneyValidationError("TAX_CODE_DUPLICATE_COMPONENT", "A tax code cannot contain the same component twice.");
|
|
74
|
+
seen.add(component.taxComponentId);
|
|
75
|
+
if (component.compound) throw new MoneyValidationError("COMPOUND_UNSUPPORTED", "Compound tax components are reserved for a future release.");
|
|
76
|
+
}
|
|
77
|
+
return this.withTransaction(async () => {
|
|
78
|
+
const { TaxComponentCollection } = await import("./TaxComponentCollection-3x24xSNh.js").then((n) => n.n);
|
|
79
|
+
const components = await TaxComponentCollection.create({ db: this.db });
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
const component = await components.get({ id: part.taxComponentId }, { cache: false });
|
|
82
|
+
if (!component) throw new Error("Tax code components must belong to the same tenant");
|
|
83
|
+
if (!component.updated_at) throw new Error("Tax code component is missing its revision");
|
|
84
|
+
await component.claimRevision(component.updated_at);
|
|
85
|
+
if (!this.tenantId) this.tenantId = component.tenantId;
|
|
86
|
+
if (component.tenantId !== this.tenantId) throw new Error("Tax code components must belong to the same tenant");
|
|
87
|
+
}
|
|
88
|
+
return await super.save();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
parsedComponents() {
|
|
92
|
+
try {
|
|
93
|
+
const value = JSON.parse(this.componentsJson);
|
|
94
|
+
if (!Array.isArray(value)) return null;
|
|
95
|
+
if (value.some((part) => !part || typeof part !== "object" || typeof part.taxComponentId !== "string" || !part.taxComponentId.trim() || typeof part.compound !== "boolean")) return null;
|
|
96
|
+
return value;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
__publicField(TaxCode, "__smrtPackage__", "@happyvertical/smrt-commerce");
|
|
103
|
+
__decorateClass([tenantId()], TaxCode.prototype, "tenantId", 2);
|
|
104
|
+
__decorateClass([field({ description: "A stable short code, for example gst-pst-bc." })], TaxCode.prototype, "code", 2);
|
|
105
|
+
__decorateClass([field({ description: "The name people see for this tax code." })], TaxCode.prototype, "name", 2);
|
|
106
|
+
__decorateClass([field({ description: "How the tax code is reported." })], TaxCode.prototype, "kind", 2);
|
|
107
|
+
__decorateClass([field({ description: "Whether the code applies to sales, purchases or both." })], TaxCode.prototype, "appliesTo", 2);
|
|
108
|
+
__decorateClass([field({
|
|
109
|
+
description: "The ordered tax components in this code.",
|
|
110
|
+
transient: true
|
|
111
|
+
})], TaxCode.prototype, "components", 1);
|
|
112
|
+
__decorateClass([field({ description: "Whether users may select this code for new documents." })], TaxCode.prototype, "active", 2);
|
|
113
|
+
TaxCode = __decorateClass([TenantScoped({ mode: "required" }), smrt({
|
|
114
|
+
conflictColumns: ["tenant_id", "code"],
|
|
115
|
+
api: {
|
|
116
|
+
include: [
|
|
117
|
+
"list",
|
|
118
|
+
"get",
|
|
119
|
+
"create",
|
|
120
|
+
"update"
|
|
121
|
+
],
|
|
122
|
+
writable: [
|
|
123
|
+
"code",
|
|
124
|
+
"name",
|
|
125
|
+
"kind",
|
|
126
|
+
"appliesTo",
|
|
127
|
+
"components",
|
|
128
|
+
"active"
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
mcp: { include: ["list", "get"] },
|
|
132
|
+
cli: { skipApiCheck: true }
|
|
133
|
+
})], TaxCode);
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/collections/TaxCodeCollection.ts
|
|
136
|
+
var TaxCodeCollection_exports = /* @__PURE__ */ __exportAll({ TaxCodeCollection: () => TaxCodeCollection });
|
|
137
|
+
var TaxCodeCollection = class extends SmrtCollection {
|
|
138
|
+
static _itemClass = TaxCode;
|
|
139
|
+
async findByCode(code) {
|
|
140
|
+
return (await this.list({
|
|
141
|
+
where: { code },
|
|
142
|
+
limit: 1
|
|
143
|
+
}))[0] ?? null;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
//#endregion
|
|
147
|
+
export { TaxCodeCollection_exports as n, TaxCode as r, TaxCodeCollection as t };
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=TaxCodeCollection-DF2eaJed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaxCodeCollection-DF2eaJed.js","names":[],"sources":["../../src/models/TaxCode.ts","../../src/collections/TaxCodeCollection.ts"],"sourcesContent":["import { field, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport { MoneyValidationError } from '../money.js';\nimport type { TaxCodeOptions, TaxCodePart } from '../types/index.js';\n\nexport type TaxCodeKind = 'taxable' | 'zero-rated' | 'exempt' | 'out-of-scope';\nexport type TaxCodeAppliesTo = 'sales' | 'purchases' | 'both';\n\n/** A reusable tax code whose ordered component list is safe to edit. */\n@TenantScoped({ mode: 'required' })\n@smrt({\n conflictColumns: ['tenant_id', 'code'],\n api: {\n include: ['list', 'get', 'create', 'update'],\n writable: ['code', 'name', 'kind', 'appliesTo', 'components', 'active'],\n },\n mcp: { include: ['list', 'get'] },\n cli: { skipApiCheck: true },\n})\nexport class TaxCode extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-commerce\";\n @tenantId()\n tenantId = '';\n\n @field({ description: 'A stable short code, for example gst-pst-bc.' })\n code = '';\n\n @field({ description: 'The name people see for this tax code.' })\n name = '';\n\n @field({ description: 'How the tax code is reported.' })\n kind: TaxCodeKind = 'taxable';\n\n @field({\n description: 'Whether the code applies to sales, purchases or both.',\n })\n appliesTo: TaxCodeAppliesTo = 'both';\n\n /** Ordered component IDs; compound is reserved and deliberately unsupported. */\n /** Stored JSON is guarded through the typed accessor below. */\n componentsJson = '[]';\n\n @field({\n description: 'The ordered tax components in this code.',\n transient: true,\n })\n get components(): TaxCodePart[] {\n try {\n const value: unknown = JSON.parse(this.componentsJson);\n if (!Array.isArray(value)) return [];\n return value.filter(\n (part): part is TaxCodePart =>\n !!part &&\n typeof part === 'object' &&\n typeof (part as TaxCodePart).taxComponentId === 'string' &&\n typeof (part as TaxCodePart).compound === 'boolean',\n );\n } catch {\n return [];\n }\n }\n\n set components(value: TaxCodePart[] | string) {\n this.componentsJson =\n typeof value === 'string' ? value : JSON.stringify(value);\n }\n\n @field({\n description: 'Whether users may select this code for new documents.',\n })\n active = true;\n\n constructor(options: TaxCodeOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId ?? '';\n if (options.code !== undefined) this.code = options.code;\n if (options.name !== undefined) this.name = options.name;\n if (options.kind !== undefined) this.kind = options.kind;\n if (options.appliesTo !== undefined) this.appliesTo = options.appliesTo;\n if (options.components !== undefined)\n this.components = structuredClone(options.components);\n if (options.active !== undefined) this.active = options.active;\n }\n\n override async save(): Promise<this> {\n if (!this.code.trim() || !this.name.trim())\n throw new Error('Tax code requires a nonempty code and name');\n if (\n !['taxable', 'zero-rated', 'exempt', 'out-of-scope'].includes(this.kind)\n )\n throw new Error('Tax code kind is invalid');\n if (!['sales', 'purchases', 'both'].includes(this.appliesTo))\n throw new Error('Tax code appliesTo is invalid');\n const parts = this.parsedComponents();\n if (!parts)\n throw new Error('Tax code components must be a valid JSON array');\n const nonTaxable = this.kind !== 'taxable';\n if (nonTaxable && parts.length) {\n throw new MoneyValidationError(\n 'TAX_CODE_NON_TAXABLE_COMPONENTS',\n `${this.kind} tax codes cannot have components.`,\n );\n }\n if (!nonTaxable && !parts.length) {\n throw new MoneyValidationError(\n 'TAX_CODE_TAXABLE_EMPTY',\n 'Taxable tax codes require at least one component.',\n );\n }\n const seen = new Set<string>();\n for (const component of parts) {\n if (seen.has(component.taxComponentId)) {\n throw new MoneyValidationError(\n 'TAX_CODE_DUPLICATE_COMPONENT',\n 'A tax code cannot contain the same component twice.',\n );\n }\n seen.add(component.taxComponentId);\n if (component.compound) {\n throw new MoneyValidationError(\n 'COMPOUND_UNSUPPORTED',\n 'Compound tax components are reserved for a future release.',\n );\n }\n }\n return this.withTransaction(async () => {\n const { TaxComponentCollection } = await import(\n '../collections/TaxComponentCollection.js'\n );\n const components = await TaxComponentCollection.create({ db: this.db });\n for (const part of parts) {\n const component = await components.get(\n { id: part.taxComponentId },\n { cache: false },\n );\n if (!component) {\n throw new Error('Tax code components must belong to the same tenant');\n }\n if (!component.updated_at)\n throw new Error('Tax code component is missing its revision');\n await component.claimRevision(component.updated_at);\n if (!this.tenantId) this.tenantId = component.tenantId;\n if (component.tenantId !== this.tenantId)\n throw new Error('Tax code components must belong to the same tenant');\n }\n return (await super.save()) as this;\n });\n }\n\n private parsedComponents(): TaxCodePart[] | null {\n try {\n const value: unknown = JSON.parse(this.componentsJson);\n if (!Array.isArray(value)) return null;\n if (\n value.some(\n (part) =>\n !part ||\n typeof part !== 'object' ||\n typeof (part as TaxCodePart).taxComponentId !== 'string' ||\n !(part as TaxCodePart).taxComponentId.trim() ||\n typeof (part as TaxCodePart).compound !== 'boolean',\n )\n )\n return null;\n return value as TaxCodePart[];\n } catch {\n return null;\n }\n }\n}\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { TaxCode } from '../models/TaxCode.js';\n\nexport class TaxCodeCollection extends SmrtCollection<TaxCode> {\n static readonly _itemClass = TaxCode;\n\n async findByCode(code: string): Promise<TaxCode | null> {\n return (await this.list({ where: { code }, limit: 1 }))[0] ?? null;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import { MoneyValidationError } from "../money.js";
|
|
3
|
+
import { SmrtCollection, SmrtObject, crossPackageRef, field, smrt } from "@happyvertical/smrt-core";
|
|
4
|
+
import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
|
|
5
|
+
import { AccountCollection } from "@happyvertical/smrt-ledgers";
|
|
6
|
+
//#region src/models/TaxComponent.ts
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value
|
|
14
|
+
}) : obj[key] = value;
|
|
15
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
16
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
17
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
18
|
+
if (kind && result) __defProp(target, key, result);
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
22
|
+
var PERCENT = /^(\d+)(?:\.(\d{1,6}))?$/;
|
|
23
|
+
function isPercent(value) {
|
|
24
|
+
const match = PERCENT.exec(value);
|
|
25
|
+
if (!match) return false;
|
|
26
|
+
return BigInt(match[1]) * 1000000n + BigInt((match[2] ?? "").padEnd(6, "0")) <= 100000000n;
|
|
27
|
+
}
|
|
28
|
+
function canonicalPercent(value) {
|
|
29
|
+
const [whole, fraction = ""] = String(value).split(".");
|
|
30
|
+
const normalizedWhole = whole.replace(/^0+(?=\d)/, "") || "0";
|
|
31
|
+
const normalizedFraction = fraction.replace(/0+$/, "");
|
|
32
|
+
return normalizedFraction ? `${normalizedWhole}.${normalizedFraction}` : normalizedWhole;
|
|
33
|
+
}
|
|
34
|
+
var TaxComponent = class extends SmrtObject {
|
|
35
|
+
tenantId = "";
|
|
36
|
+
name = "";
|
|
37
|
+
percent = "0.0";
|
|
38
|
+
liabilityAccountId = "";
|
|
39
|
+
recoverable = false;
|
|
40
|
+
active = true;
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
super(options);
|
|
43
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId ?? "";
|
|
44
|
+
if (options.name !== void 0) this.name = options.name;
|
|
45
|
+
if (options.percent !== void 0) this.percent = options.percent;
|
|
46
|
+
if (options.liabilityAccountId !== void 0) this.liabilityAccountId = options.liabilityAccountId;
|
|
47
|
+
if (options.recoverable !== void 0) this.recoverable = options.recoverable;
|
|
48
|
+
if (options.active !== void 0) this.active = options.active;
|
|
49
|
+
}
|
|
50
|
+
async initialize() {
|
|
51
|
+
await super.initialize();
|
|
52
|
+
this.percent = canonicalPercent(String(this.percent));
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
async save() {
|
|
56
|
+
if (!this.name.trim()) throw new Error("Tax component requires a nonempty name");
|
|
57
|
+
if (!isPercent(this.percent)) throw new MoneyValidationError("PERCENT_OUT_OF_RANGE", "Tax component percent must be between 0 and 100 with at most six decimal places.");
|
|
58
|
+
this.percent = canonicalPercent(this.percent);
|
|
59
|
+
return this.withTransaction(async () => {
|
|
60
|
+
const { TaxComponentCollection } = await Promise.resolve().then(() => TaxComponentCollection_exports);
|
|
61
|
+
const components = await TaxComponentCollection.create({ db: this.db });
|
|
62
|
+
const persisted = this.id ? await components.get({ id: this.id }, { cache: false }) : null;
|
|
63
|
+
if (persisted && canonicalPercent(persisted.percent) !== this.percent) {
|
|
64
|
+
const referenced = await this.isReferenced(persisted);
|
|
65
|
+
this.updated_at = persisted.updated_at;
|
|
66
|
+
if (referenced) throw new MoneyValidationError("PERCENT_OUT_OF_RANGE", "A referenced tax component rate is immutable; archive it and repoint its tax codes.");
|
|
67
|
+
}
|
|
68
|
+
if (this.liabilityAccountId) {
|
|
69
|
+
const account = await (await AccountCollection.create({ db: this.db })).get({ id: this.liabilityAccountId }, { cache: false });
|
|
70
|
+
if (!account || account.tenantId !== this.tenantId) throw new Error("Tax component liability account must belong to its tenant");
|
|
71
|
+
}
|
|
72
|
+
return await super.save();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async isReferenced(persisted) {
|
|
76
|
+
if (!this.id) return false;
|
|
77
|
+
if (!persisted.updated_at) throw new Error("Referenced tax component is missing its revision");
|
|
78
|
+
await persisted.claimRevision(persisted.updated_at);
|
|
79
|
+
const { TaxCodeCollection } = await import("./TaxCodeCollection-DF2eaJed.js").then((n) => n.n);
|
|
80
|
+
const { DocumentTaxCollection } = await import("./DocumentTaxCollection-KOkaQT2M.js").then((n) => n.n);
|
|
81
|
+
if ((await (await TaxCodeCollection.create({ db: this.db })).list({ where: { tenantId: this.tenantId } })).some((code) => code.components.some((component) => component.taxComponentId === this.id))) return true;
|
|
82
|
+
return (await (await DocumentTaxCollection.create({ db: this.db })).list({
|
|
83
|
+
where: {
|
|
84
|
+
tenantId: this.tenantId,
|
|
85
|
+
taxComponentId: this.id
|
|
86
|
+
},
|
|
87
|
+
limit: 1
|
|
88
|
+
})).length > 0;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
__publicField(TaxComponent, "__smrtPackage__", "@happyvertical/smrt-commerce");
|
|
92
|
+
__decorateClass([tenantId()], TaxComponent.prototype, "tenantId", 2);
|
|
93
|
+
__decorateClass([field({ description: "The name of this tax component, for example GST." })], TaxComponent.prototype, "name", 2);
|
|
94
|
+
__decorateClass([field({
|
|
95
|
+
type: "decimal",
|
|
96
|
+
description: "The tax percentage, for example 7.125 for 7.125 percent."
|
|
97
|
+
})], TaxComponent.prototype, "percent", 2);
|
|
98
|
+
__decorateClass([crossPackageRef("@happyvertical/smrt-ledgers:Account", { description: "The liability account that receives this tax component." })], TaxComponent.prototype, "liabilityAccountId", 2);
|
|
99
|
+
__decorateClass([field({ description: "Whether purchases using this component may recover the full input tax. Partial recovery is not supported." })], TaxComponent.prototype, "recoverable", 2);
|
|
100
|
+
__decorateClass([field({ description: "Whether new tax codes and overrides may select it." })], TaxComponent.prototype, "active", 2);
|
|
101
|
+
TaxComponent = __decorateClass([TenantScoped({ mode: "required" }), smrt({
|
|
102
|
+
api: {
|
|
103
|
+
include: [
|
|
104
|
+
"list",
|
|
105
|
+
"get",
|
|
106
|
+
"create",
|
|
107
|
+
"update"
|
|
108
|
+
],
|
|
109
|
+
writable: [
|
|
110
|
+
"name",
|
|
111
|
+
"percent",
|
|
112
|
+
"liabilityAccountId",
|
|
113
|
+
"recoverable",
|
|
114
|
+
"active"
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
mcp: { include: ["list", "get"] },
|
|
118
|
+
cli: { skipApiCheck: true }
|
|
119
|
+
})], TaxComponent);
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/collections/TaxComponentCollection.ts
|
|
122
|
+
var TaxComponentCollection_exports = /* @__PURE__ */ __exportAll({ TaxComponentCollection: () => TaxComponentCollection });
|
|
123
|
+
var TaxComponentCollection = class extends SmrtCollection {
|
|
124
|
+
static _itemClass = TaxComponent;
|
|
125
|
+
};
|
|
126
|
+
//#endregion
|
|
127
|
+
export { TaxComponentCollection_exports as n, TaxComponent as r, TaxComponentCollection as t };
|
|
128
|
+
|
|
129
|
+
//# sourceMappingURL=TaxComponentCollection-3x24xSNh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaxComponentCollection-3x24xSNh.js","names":[],"sources":["../../src/models/TaxComponent.ts","../../src/collections/TaxComponentCollection.ts"],"sourcesContent":["import {\n crossPackageRef,\n field,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { AccountCollection } from '@happyvertical/smrt-ledgers';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport { MoneyValidationError } from '../money.js';\nimport type { TaxComponentOptions } from '../types/index.js';\n\nconst PERCENT = /^(\\d+)(?:\\.(\\d{1,6}))?$/;\n\nfunction isPercent(value: string): boolean {\n const match = PERCENT.exec(value);\n if (!match) return false;\n const scaled =\n BigInt(match[1]) * 1_000_000n + BigInt((match[2] ?? '').padEnd(6, '0'));\n return scaled <= 100_000_000n;\n}\n\nfunction canonicalPercent(value: string): string {\n const [whole, fraction = ''] = String(value).split('.');\n const normalizedWhole = whole.replace(/^0+(?=\\d)/, '') || '0';\n const normalizedFraction = fraction.replace(/0+$/, '');\n return normalizedFraction\n ? `${normalizedWhole}.${normalizedFraction}`\n : normalizedWhole;\n}\n\n/**\n * One reusable tax rate component. Codes may combine components, but the\n * component's percent is immutable after it has been saved: archive it and\n * repoint editable codes to a replacement rate instead of rewriting history.\n */\n@TenantScoped({ mode: 'required' })\n@smrt({\n api: {\n include: ['list', 'get', 'create', 'update'],\n writable: [\n 'name',\n 'percent',\n 'liabilityAccountId',\n 'recoverable',\n 'active',\n ],\n },\n mcp: { include: ['list', 'get'] },\n cli: { skipApiCheck: true },\n})\nexport class TaxComponent extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-commerce\";\n @tenantId()\n tenantId = '';\n\n @field({ description: 'The name of this tax component, for example GST.' })\n name = '';\n\n /** Percent, not a fraction: 7.125 means 7.125%. Stored as DECIMAL. */\n @field({\n type: 'decimal',\n description: 'The tax percentage, for example 7.125 for 7.125 percent.',\n })\n percent = '0.0';\n\n @crossPackageRef('@happyvertical/smrt-ledgers:Account', {\n description: 'The liability account that receives this tax component.',\n })\n liabilityAccountId = '';\n\n @field({\n description:\n 'Whether purchases using this component may recover the full input tax. Partial recovery is not supported.',\n })\n recoverable = false;\n\n @field({ description: 'Whether new tax codes and overrides may select it.' })\n active = true;\n\n constructor(options: TaxComponentOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId ?? '';\n if (options.name !== undefined) this.name = options.name;\n if (options.percent !== undefined) this.percent = options.percent;\n if (options.liabilityAccountId !== undefined)\n this.liabilityAccountId = options.liabilityAccountId;\n if (options.recoverable !== undefined)\n this.recoverable = options.recoverable;\n if (options.active !== undefined) this.active = options.active;\n }\n\n override async initialize(): Promise<this> {\n await super.initialize();\n this.percent = canonicalPercent(String(this.percent));\n return this;\n }\n\n override async save(): Promise<this> {\n if (!this.name.trim())\n throw new Error('Tax component requires a nonempty name');\n if (!isPercent(this.percent)) {\n throw new MoneyValidationError(\n 'PERCENT_OUT_OF_RANGE',\n 'Tax component percent must be between 0 and 100 with at most six decimal places.',\n );\n }\n this.percent = canonicalPercent(this.percent);\n return this.withTransaction(async () => {\n const { TaxComponentCollection } = await import(\n '../collections/TaxComponentCollection.js'\n );\n const components = await TaxComponentCollection.create({ db: this.db });\n const persisted = this.id\n ? await components.get({ id: this.id }, { cache: false })\n : null;\n if (persisted && canonicalPercent(persisted.percent) !== this.percent) {\n const referenced = await this.isReferenced(persisted);\n // claimRevision advanced the row's version; the pending save must use\n // that version rather than its stale caller snapshot.\n this.updated_at = persisted.updated_at;\n if (referenced) {\n throw new MoneyValidationError(\n 'PERCENT_OUT_OF_RANGE',\n 'A referenced tax component rate is immutable; archive it and repoint its tax codes.',\n );\n }\n }\n if (this.liabilityAccountId) {\n const accounts = await AccountCollection.create({ db: this.db });\n const account = await accounts.get(\n { id: this.liabilityAccountId },\n { cache: false },\n );\n if (!account || account.tenantId !== this.tenantId) {\n throw new Error(\n 'Tax component liability account must belong to its tenant',\n );\n }\n }\n return (await super.save()) as this;\n });\n }\n\n private async isReferenced(persisted: TaxComponent): Promise<boolean> {\n if (!this.id) return false;\n if (!persisted.updated_at)\n throw new Error('Referenced tax component is missing its revision');\n // Claim before inspecting references. TaxCode.save() claims the same row\n // before attaching it, so a concurrent rate edit must reload and retry.\n await persisted.claimRevision(persisted.updated_at);\n const { TaxCodeCollection } = await import(\n '../collections/TaxCodeCollection.js'\n );\n const { DocumentTaxCollection } = await import(\n '../collections/DocumentTaxCollection.js'\n );\n const codes = await TaxCodeCollection.create({ db: this.db });\n const usedByCode = (\n await codes.list({ where: { tenantId: this.tenantId } })\n ).some((code) =>\n code.components.some((component) => component.taxComponentId === this.id),\n );\n if (usedByCode) return true;\n const documentTaxes = await DocumentTaxCollection.create({ db: this.db });\n return (\n (\n await documentTaxes.list({\n where: { tenantId: this.tenantId, taxComponentId: this.id },\n limit: 1,\n })\n ).length > 0\n );\n }\n}\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { TaxComponent } from '../models/TaxComponent.js';\n\nexport class TaxComponentCollection extends SmrtCollection<TaxComponent> {\n static readonly _itemClass = TaxComponent;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
+
import { MoneyValidationError } from "../money.js";
|
|
3
|
+
import { SmrtCollection, SmrtObject, field, foreignKey, smrt } from "@happyvertical/smrt-core";
|
|
4
|
+
import { TenantScoped, getTenantId, tenantId } from "@happyvertical/smrt-tenancy";
|
|
5
|
+
//#region src/document-money-internal.ts
|
|
6
|
+
var activeMoneyTransactions = /* @__PURE__ */ new WeakSet();
|
|
7
|
+
var DOCUMENT_MONEY_WRITE = /* @__PURE__ */ Symbol("commerce.documentMoneyWrite");
|
|
8
|
+
var MONEY_MIGRATION_WRITE = /* @__PURE__ */ Symbol("commerce.moneyMigrationWrite");
|
|
9
|
+
var DOCUMENT_MONEY_READ_REBIND = /* @__PURE__ */ Symbol("commerce.documentMoneyReadRebind");
|
|
10
|
+
function compareCodeUnits(left, right) {
|
|
11
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
12
|
+
}
|
|
13
|
+
function quoteIdentifier(identifier) {
|
|
14
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Unsafe money parent table identifier: ${identifier}`);
|
|
15
|
+
return `"${identifier}"`;
|
|
16
|
+
}
|
|
17
|
+
function assertIdentifier(value, label) {
|
|
18
|
+
if (typeof value !== "string" || value.trim() !== value || value.length === 0) throw new MoneyValidationError("INVALID_MONEY_INPUT", `${label} must be a nonempty identifier`);
|
|
19
|
+
}
|
|
20
|
+
async function withDocumentMoneyTransaction(db, operation) {
|
|
21
|
+
if (activeMoneyTransactions.has(db)) return operation(db);
|
|
22
|
+
if (!db.transaction) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Document money persistence requires transaction support");
|
|
23
|
+
return db.transaction(async (tx) => {
|
|
24
|
+
activeMoneyTransactions.add(tx);
|
|
25
|
+
try {
|
|
26
|
+
return await operation(tx);
|
|
27
|
+
} finally {
|
|
28
|
+
activeMoneyTransactions.delete(tx);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async function withDocumentMoneyObjectTransaction(input, operation) {
|
|
33
|
+
if (activeMoneyTransactions.has(input.db)) return operation(input, input.db);
|
|
34
|
+
return input.withTransaction(async (bound) => {
|
|
35
|
+
const tx = bound.db;
|
|
36
|
+
activeMoneyTransactions.add(tx);
|
|
37
|
+
try {
|
|
38
|
+
return await operation(bound, tx);
|
|
39
|
+
} finally {
|
|
40
|
+
activeMoneyTransactions.delete(tx);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function lockDocumentMoneyParents(targets) {
|
|
45
|
+
const sorted = [...targets].sort((left, right) => compareCodeUnits(left.id, right.id) || compareCodeUnits(left.collection.tableName, right.collection.tableName));
|
|
46
|
+
const identities = /* @__PURE__ */ new Set();
|
|
47
|
+
for (const target of sorted) {
|
|
48
|
+
assertIdentifier(target.id, "Money parent id");
|
|
49
|
+
assertIdentifier(target.tenantId, "Money parent tenant id");
|
|
50
|
+
const identity = `${target.collection.tableName}\0${target.id}`;
|
|
51
|
+
if (identities.has(identity)) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Duplicate money parent lock target: ${target.id}`);
|
|
52
|
+
identities.add(identity);
|
|
53
|
+
const table = quoteIdentifier(target.collection.tableName);
|
|
54
|
+
if ((await target.collection.query(`UPDATE ${table}
|
|
55
|
+
SET "updated_at" = "updated_at"
|
|
56
|
+
WHERE "id" = ? AND "tenant_id" = ?
|
|
57
|
+
RETURNING "id"`, [target.id, target.tenantId], { allowRawOnTenantScoped: true })).length !== 1) throw new MoneyValidationError("INVALID_MONEY_INPUT", `Money parent ${target.id} was not found in tenant ${target.tenantId}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/models/TaxSettings.ts
|
|
62
|
+
var __defProp = Object.defineProperty;
|
|
63
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
64
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
|
|
65
|
+
enumerable: true,
|
|
66
|
+
configurable: true,
|
|
67
|
+
writable: true,
|
|
68
|
+
value
|
|
69
|
+
}) : obj[key] = value;
|
|
70
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
71
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
72
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
73
|
+
if (kind && result) __defProp(target, key, result);
|
|
74
|
+
return result;
|
|
75
|
+
};
|
|
76
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
77
|
+
var TaxSettings = class extends SmrtObject {
|
|
78
|
+
tenantId = "";
|
|
79
|
+
defaultSalesTaxCodeId = "";
|
|
80
|
+
defaultPurchaseTaxCodeId = "";
|
|
81
|
+
defaultLineAmountType = "exclusive";
|
|
82
|
+
taxRoundingScope = "line";
|
|
83
|
+
/** Stored JSON is guarded through the typed accessor below. */
|
|
84
|
+
taxRegistrationsJson = "[]";
|
|
85
|
+
/** Server-owned runtime semantics gate. Never exposed as a generated write. */
|
|
86
|
+
moneyModel = "legacy";
|
|
87
|
+
/** Opaque owner token while a tenant migration is in progress. */
|
|
88
|
+
moneyMigrationId = "";
|
|
89
|
+
get taxRegistrations() {
|
|
90
|
+
try {
|
|
91
|
+
const value = JSON.parse(this.taxRegistrationsJson);
|
|
92
|
+
if (!Array.isArray(value)) return [];
|
|
93
|
+
return value.filter((entry) => !!entry && typeof entry === "object" && typeof entry.taxComponentId === "string" && typeof entry.number === "string" && typeof entry.label === "string");
|
|
94
|
+
} catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
set taxRegistrations(value) {
|
|
99
|
+
this.taxRegistrationsJson = typeof value === "string" ? value : JSON.stringify(value);
|
|
100
|
+
}
|
|
101
|
+
constructor(options = {}) {
|
|
102
|
+
super(options);
|
|
103
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
104
|
+
if (options.defaultSalesTaxCodeId !== void 0) this.defaultSalesTaxCodeId = options.defaultSalesTaxCodeId;
|
|
105
|
+
if (options.defaultPurchaseTaxCodeId !== void 0) this.defaultPurchaseTaxCodeId = options.defaultPurchaseTaxCodeId;
|
|
106
|
+
if (options.defaultLineAmountType !== void 0) this.defaultLineAmountType = options.defaultLineAmountType;
|
|
107
|
+
if (options.taxRoundingScope !== void 0) this.taxRoundingScope = options.taxRoundingScope;
|
|
108
|
+
if (options.taxRegistrations !== void 0) this.taxRegistrations = structuredClone(options.taxRegistrations);
|
|
109
|
+
if (options.moneyModel !== void 0) this.moneyModel = options.moneyModel;
|
|
110
|
+
if (options.moneyMigrationId !== void 0) this.moneyMigrationId = options.moneyMigrationId;
|
|
111
|
+
}
|
|
112
|
+
async save() {
|
|
113
|
+
return this.saveValidated(false);
|
|
114
|
+
}
|
|
115
|
+
/** @internal Money migration owns marker transitions. */
|
|
116
|
+
async saveMoneyModel(capability) {
|
|
117
|
+
if (capability !== MONEY_MIGRATION_WRITE) throw new MoneyValidationError("FROZEN_DOCUMENT", "Tenant money model requires the migration capability");
|
|
118
|
+
return this.saveValidated(true);
|
|
119
|
+
}
|
|
120
|
+
/** @internal Caller already owns the transaction-bound migration barrier. */
|
|
121
|
+
async saveMoneyModelOnTransaction(capability) {
|
|
122
|
+
if (capability !== MONEY_MIGRATION_WRITE) throw new MoneyValidationError("FROZEN_DOCUMENT", "Tenant money model requires the migration capability");
|
|
123
|
+
return this.saveValidated(true, true);
|
|
124
|
+
}
|
|
125
|
+
async saveValidated(allowMoneyModelWrite, transactionBound = false) {
|
|
126
|
+
if (![
|
|
127
|
+
"exclusive",
|
|
128
|
+
"inclusive",
|
|
129
|
+
"none"
|
|
130
|
+
].includes(this.defaultLineAmountType)) throw new Error("Tax settings defaultLineAmountType is invalid");
|
|
131
|
+
if (!["line", "document"].includes(this.taxRoundingScope)) throw new Error("Tax settings taxRoundingScope is invalid");
|
|
132
|
+
if (![
|
|
133
|
+
"legacy",
|
|
134
|
+
"migrating",
|
|
135
|
+
"v2"
|
|
136
|
+
].includes(this.moneyModel)) throw new Error("Tax settings moneyModel is invalid");
|
|
137
|
+
if (this.moneyModel === "migrating" !== !!this.moneyMigrationId) throw new Error("Tax settings migrating state requires exactly one migration owner");
|
|
138
|
+
const registrations = this.parsedRegistrations();
|
|
139
|
+
if (!registrations) throw new Error("Tax registrations must be a valid JSON array");
|
|
140
|
+
if (registrations.some((entry) => !entry.taxComponentId || !entry.number.trim() || !entry.label.trim())) throw new Error("Tax registrations require component, number and label");
|
|
141
|
+
const persist = async () => {
|
|
142
|
+
const { TaxCodeCollection } = await import("./TaxCodeCollection-DF2eaJed.js").then((n) => n.n);
|
|
143
|
+
const { TaxComponentCollection } = await import("./TaxComponentCollection-3x24xSNh.js").then((n) => n.n);
|
|
144
|
+
const codes = await TaxCodeCollection.create({ db: this.db });
|
|
145
|
+
const components = await TaxComponentCollection.create({ db: this.db });
|
|
146
|
+
if (!allowMoneyModelWrite) {
|
|
147
|
+
const tenantKey = this.tenantId || getTenantId() || "";
|
|
148
|
+
const { TaxSettingsCollection } = await Promise.resolve().then(() => TaxSettingsCollection_exports);
|
|
149
|
+
const persistedByTenant = tenantKey ? await (await TaxSettingsCollection.create({ db: this.db })).lockForTenant(tenantKey, false) : null;
|
|
150
|
+
const persistedById = this.id ? await this.db.get(this.tableName, { id: this.id }) : null;
|
|
151
|
+
if (persistedByTenant && (persistedByTenant.moneyModel !== this.moneyModel || persistedByTenant.moneyMigrationId !== this.moneyMigrationId) || persistedById && (persistedById.money_model !== this.moneyModel || (persistedById.money_migration_id ?? "") !== this.moneyMigrationId)) throw new MoneyValidationError("FROZEN_DOCUMENT", "Tenant money model is server-managed");
|
|
152
|
+
if (!persistedByTenant && !this.isPersisted) this.requireInsertOnSave();
|
|
153
|
+
}
|
|
154
|
+
for (const id of [this.defaultSalesTaxCodeId, this.defaultPurchaseTaxCodeId].filter(Boolean)) {
|
|
155
|
+
const code = await codes.get({ id }, { cache: false });
|
|
156
|
+
if (!code || code.tenantId !== this.tenantId) throw new Error("Tax setting default codes must belong to its tenant");
|
|
157
|
+
}
|
|
158
|
+
for (const registration of registrations) {
|
|
159
|
+
const component = await components.get({ id: registration.taxComponentId }, { cache: false });
|
|
160
|
+
if (!component || component.tenantId !== this.tenantId) throw new Error("Tax setting registrations must belong to its tenant");
|
|
161
|
+
}
|
|
162
|
+
return await super.save();
|
|
163
|
+
};
|
|
164
|
+
return transactionBound ? persist() : this.withTransaction(persist);
|
|
165
|
+
}
|
|
166
|
+
parsedRegistrations() {
|
|
167
|
+
try {
|
|
168
|
+
const value = JSON.parse(this.taxRegistrationsJson);
|
|
169
|
+
if (!Array.isArray(value)) return null;
|
|
170
|
+
if (value.some((entry) => !entry || typeof entry !== "object" || typeof entry.taxComponentId !== "string" || typeof entry.number !== "string" || typeof entry.label !== "string")) return null;
|
|
171
|
+
return value;
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
__publicField(TaxSettings, "__smrtPackage__", "@happyvertical/smrt-commerce");
|
|
178
|
+
__decorateClass([tenantId()], TaxSettings.prototype, "tenantId", 2);
|
|
179
|
+
__decorateClass([foreignKey("TaxCode", { description: "The default tax code for sales documents." })], TaxSettings.prototype, "defaultSalesTaxCodeId", 2);
|
|
180
|
+
__decorateClass([foreignKey("TaxCode", { description: "The default tax code for purchase documents." })], TaxSettings.prototype, "defaultPurchaseTaxCodeId", 2);
|
|
181
|
+
__decorateClass([field({ description: "Whether new document amounts are exclusive, inclusive or out of scope." })], TaxSettings.prototype, "defaultLineAmountType", 2);
|
|
182
|
+
__decorateClass([field({ description: "Whether components round per line or per document." })], TaxSettings.prototype, "taxRoundingScope", 2);
|
|
183
|
+
__decorateClass([field({
|
|
184
|
+
sensitive: true,
|
|
185
|
+
transient: true,
|
|
186
|
+
description: "Registration numbers by tax component, for issued-document snapshots."
|
|
187
|
+
})], TaxSettings.prototype, "taxRegistrations", 1);
|
|
188
|
+
TaxSettings = __decorateClass([TenantScoped({ mode: "required" }), smrt({
|
|
189
|
+
conflictColumns: ["tenant_id"],
|
|
190
|
+
api: {
|
|
191
|
+
include: [
|
|
192
|
+
"get",
|
|
193
|
+
"create",
|
|
194
|
+
"update"
|
|
195
|
+
],
|
|
196
|
+
writable: [
|
|
197
|
+
"defaultSalesTaxCodeId",
|
|
198
|
+
"defaultPurchaseTaxCodeId",
|
|
199
|
+
"defaultLineAmountType",
|
|
200
|
+
"taxRoundingScope",
|
|
201
|
+
"taxRegistrations"
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
mcp: { include: ["get"] },
|
|
205
|
+
cli: { skipApiCheck: true }
|
|
206
|
+
})], TaxSettings);
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/tax/identity.ts
|
|
209
|
+
async function deterministicTaxId(tenantId, kind, key) {
|
|
210
|
+
if (!tenantId.trim() || !key.trim()) throw new Error("Tax bootstrap identity requires tenant and key");
|
|
211
|
+
const bytes = new TextEncoder().encode(JSON.stringify([
|
|
212
|
+
"commerce-tax",
|
|
213
|
+
tenantId,
|
|
214
|
+
kind,
|
|
215
|
+
key
|
|
216
|
+
]));
|
|
217
|
+
const uuid = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)).slice(0, 16);
|
|
218
|
+
uuid[6] = uuid[6] & 15 | 80;
|
|
219
|
+
uuid[8] = uuid[8] & 63 | 128;
|
|
220
|
+
const joined = Array.from(uuid, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
221
|
+
return `${joined.slice(0, 8)}-${joined.slice(8, 12)}-${joined.slice(12, 16)}-${joined.slice(16, 20)}-${joined.slice(20)}`;
|
|
222
|
+
}
|
|
223
|
+
async function createOrLoadTaxSeed(create, load) {
|
|
224
|
+
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
225
|
+
return await create();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const code = error?.code;
|
|
228
|
+
if (code === "RUNTIME_REVISION_CONFLICT" && attempt < 2) continue;
|
|
229
|
+
if (code !== "VALIDATION_UNIQUE_CONSTRAINT" && code !== "23505") throw error;
|
|
230
|
+
const winner = await load();
|
|
231
|
+
if (!winner) throw error;
|
|
232
|
+
return winner;
|
|
233
|
+
}
|
|
234
|
+
throw new Error("Tax seed retry exhausted");
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/collections/TaxSettingsCollection.ts
|
|
238
|
+
var TaxSettingsCollection_exports = /* @__PURE__ */ __exportAll({ TaxSettingsCollection: () => TaxSettingsCollection });
|
|
239
|
+
var TaxSettingsCollection = class TaxSettingsCollection extends SmrtCollection {
|
|
240
|
+
static _itemClass = TaxSettings;
|
|
241
|
+
async getOrCreateForTenant(tenantId) {
|
|
242
|
+
const existing = await this.getForTenant(tenantId);
|
|
243
|
+
if (existing) return existing;
|
|
244
|
+
const id = await deterministicTaxId(tenantId, "settings", "default");
|
|
245
|
+
return createOrLoadTaxSeed(() => this.create({
|
|
246
|
+
id,
|
|
247
|
+
tenantId
|
|
248
|
+
}), async () => (await this.list({
|
|
249
|
+
where: { tenantId },
|
|
250
|
+
limit: 1
|
|
251
|
+
}))[0] ?? null);
|
|
252
|
+
}
|
|
253
|
+
async getForTenant(tenantId) {
|
|
254
|
+
const listed = (await this.list({
|
|
255
|
+
where: { tenantId },
|
|
256
|
+
limit: 1
|
|
257
|
+
}))[0];
|
|
258
|
+
if (!listed?.id) return listed ?? null;
|
|
259
|
+
return await this.get({ id: listed.id }, { cache: false }) ?? null;
|
|
260
|
+
}
|
|
261
|
+
/** @internal Lock and re-read the one tenant marker on this transaction. */
|
|
262
|
+
async lockForTenant(tenantId, create = true) {
|
|
263
|
+
const first = create ? await this.getOrCreateForTenant(tenantId) : await this.getForTenant(tenantId);
|
|
264
|
+
if (!first?.id) return null;
|
|
265
|
+
await lockDocumentMoneyParents([{
|
|
266
|
+
id: first.id,
|
|
267
|
+
tenantId,
|
|
268
|
+
collection: this
|
|
269
|
+
}]);
|
|
270
|
+
const locked = await this.getForTenant(tenantId);
|
|
271
|
+
if (!locked || locked.id !== first.id) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Tenant money settings changed while acquiring the barrier");
|
|
272
|
+
return locked;
|
|
273
|
+
}
|
|
274
|
+
/** Claim exclusive runtime maintenance before any per-document rewrite. */
|
|
275
|
+
async beginMoneyMigration(tenantId, requestedMigrationId) {
|
|
276
|
+
if (requestedMigrationId !== void 0 && !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(requestedMigrationId)) throw new MoneyValidationError("INVALID_MONEY_INPUT", "Migration owner identity must be a UUID");
|
|
277
|
+
return withDocumentMoneyTransaction(this.db, async (tx) => {
|
|
278
|
+
const { lockTenantMoneyMigrationClaim } = await import("./tenant-money-barrier-y6EAa6vY.js").then((n) => n.r);
|
|
279
|
+
const current = await lockTenantMoneyMigrationClaim(tx, tenantId);
|
|
280
|
+
const migrationId = requestedMigrationId ?? crypto.randomUUID();
|
|
281
|
+
current.moneyModel = "migrating";
|
|
282
|
+
current.moneyMigrationId = migrationId;
|
|
283
|
+
await current.saveMoneyModelOnTransaction(MONEY_MIGRATION_WRITE);
|
|
284
|
+
return migrationId;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async activateMoneyV2(tenantId, migrationId) {
|
|
288
|
+
await withDocumentMoneyTransaction(this.db, async (tx) => {
|
|
289
|
+
const { lockTenantMoneyMigrationOwner } = await import("./tenant-money-barrier-y6EAa6vY.js").then((n) => n.r);
|
|
290
|
+
const current = await lockTenantMoneyMigrationOwner(tx, tenantId, migrationId);
|
|
291
|
+
current.moneyModel = "v2";
|
|
292
|
+
current.moneyMigrationId = "";
|
|
293
|
+
await current.saveMoneyModelOnTransaction(MONEY_MIGRATION_WRITE);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
async restoreLegacyMoneyModel(tenantId, migrationId) {
|
|
297
|
+
await withDocumentMoneyTransaction(this.db, async (tx) => {
|
|
298
|
+
const { lockTenantMoneyMigrationOwner } = await import("./tenant-money-barrier-y6EAa6vY.js").then((n) => n.r);
|
|
299
|
+
const current = await lockTenantMoneyMigrationOwner(tx, tenantId, migrationId);
|
|
300
|
+
current.moneyModel = "legacy";
|
|
301
|
+
current.moneyMigrationId = "";
|
|
302
|
+
await current.saveMoneyModelOnTransaction(MONEY_MIGRATION_WRITE);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
/** Claim a previously completed migration for checksum-guarded rollback. */
|
|
306
|
+
async beginMoneyRollback(tenantId, migrationId) {
|
|
307
|
+
await withDocumentMoneyTransaction(this.db, async (tx) => {
|
|
308
|
+
const current = await (await TaxSettingsCollection.create({ db: tx })).lockForTenant(tenantId, false);
|
|
309
|
+
if (current?.moneyModel !== "v2") throw new MoneyValidationError("FROZEN_DOCUMENT", "Money rollback requires an active v2 tenant marker");
|
|
310
|
+
current.moneyModel = "migrating";
|
|
311
|
+
current.moneyMigrationId = migrationId;
|
|
312
|
+
await current.saveMoneyModelOnTransaction(MONEY_MIGRATION_WRITE);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
//#endregion
|
|
317
|
+
export { DOCUMENT_MONEY_READ_REBIND as a, lockDocumentMoneyParents as c, TaxSettings as i, withDocumentMoneyObjectTransaction as l, createOrLoadTaxSeed as n, DOCUMENT_MONEY_WRITE as o, deterministicTaxId as r, MONEY_MIGRATION_WRITE as s, TaxSettingsCollection as t, withDocumentMoneyTransaction as u };
|
|
318
|
+
|
|
319
|
+
//# sourceMappingURL=TaxSettingsCollection-DYYF-In-.js.map
|