@happyvertical/smrt-commerce 0.55.19 → 0.55.20
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/README.md +3 -1
- package/agents/cost-billing.md +70 -0
- package/dist/cost-billing.d.ts +48 -0
- package/dist/cost-billing.d.ts.map +1 -1
- package/dist/cost-billing.js +109 -1
- package/dist/cost-billing.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +5 -5
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -600,7 +600,9 @@ caller-owned sources and approvals. See [the editor contract](agents/invoice-pre
|
|
|
600
600
|
|
|
601
601
|
The independent `@happyvertical/smrt-commerce/cost-billing` entry point provides
|
|
602
602
|
exact signed reimbursement/fee calculations, ordered generic markup provenance,
|
|
603
|
-
and explicit line or invoice tax rounding.
|
|
603
|
+
and explicit line or invoice tax rounding. Its opt-in ordered tax calculator
|
|
604
|
+
supports subset compounding with caller-selected signed invoice allocation.
|
|
605
|
+
See [the cost billing contract](agents/cost-billing.md).
|
|
604
606
|
It does not alter Invoice persistence, editor defaults or settlement behavior.
|
|
605
607
|
|
|
606
608
|
### Purchase-order header editing
|
package/agents/cost-billing.md
CHANGED
|
@@ -39,3 +39,73 @@ rounds once. Two 10-unit lines at 5 percent yield tax 2 versus 1 respectively.
|
|
|
39
39
|
Returns `taxMinor` and component totals, deliberately without allocating rounded
|
|
40
40
|
invoice tax back to lines. Signed reversals use symmetric rounding, but mixed
|
|
41
41
|
positive/negative lines may differ between rounding scopes.
|
|
42
|
+
|
|
43
|
+
## Ordered compound tax with explicit allocation
|
|
44
|
+
|
|
45
|
+
`calculateOrderedCostBillingTax` is a separate opt-in calculator on the same
|
|
46
|
+
entry point. The additive calculator above, existing model/editor rounding and
|
|
47
|
+
historical snapshots are unchanged. This helper neither infers regional rules
|
|
48
|
+
nor persists policy. Applications must explicitly select approved tenant/regional
|
|
49
|
+
settings, preserve their accepted policy and result snapshots, and decide when a
|
|
50
|
+
new calculation is appropriate.
|
|
51
|
+
|
|
52
|
+
Supply `lines` (`id`, signed safe-integer `amountMinor`, `category`), `categories`
|
|
53
|
+
(`category`, `componentIds`) and ordered `components` (`id`, exact `percent`,
|
|
54
|
+
`priorComponentIds`). Every component must explicitly supply `priorComponentIds`:
|
|
55
|
+
`[]` means net-only; otherwise each ID must name a distinct earlier component.
|
|
56
|
+
The component basis on each eligible line is its net plus that line's allocated
|
|
57
|
+
tax for exactly those prior components. A prior tax not applicable to that line
|
|
58
|
+
contributes zero. Category membership order does not control execution; the
|
|
59
|
+
global component array does. A category's empty `componentIds` means exempt.
|
|
60
|
+
|
|
61
|
+
Choose `roundingScope: 'line'` to round each eligible line/component pair
|
|
62
|
+
half-away-from-zero. Invoice allocation options are forbidden in this mode,
|
|
63
|
+
including an explicitly present `invoiceAllocation: undefined` property.
|
|
64
|
+
Choose `roundingScope: 'invoice'` with the mandatory explicit
|
|
65
|
+
`invoiceAllocation: 'signed-largest-remainder'` to round each component once
|
|
66
|
+
over its eligible lines and distribute that rounded amount as follows:
|
|
67
|
+
|
|
68
|
+
1. Calculate each eligible line's exact rational tax from its compound basis.
|
|
69
|
+
2. Truncate each exact tax toward zero, retaining its signed fractional remainder.
|
|
70
|
+
3. Round the exact component aggregate half-away-from-zero. Subtract the sum of
|
|
71
|
+
truncated line taxes to obtain the signed residual.
|
|
72
|
+
4. For positive residual, assign one extra minor unit to each of the lines with
|
|
73
|
+
the largest signed remainders; for negative residual, subtract one unit from
|
|
74
|
+
each line with the smallest (most negative) remainders. Stop when conserved.
|
|
75
|
+
Ties use ascending JavaScript UTF-16 code-unit line ID order, never locale
|
|
76
|
+
collation or input order. A zero residual leaves every truncated tax unchanged.
|
|
77
|
+
|
|
78
|
+
This allocation is a caller-selected arithmetic convention, not a jurisdiction
|
|
79
|
+
default or independent per-line rounding. It supports mixed signs and exact full
|
|
80
|
+
reversal: keeping the same IDs/policy and negating all amounts negates every
|
|
81
|
+
returned basis and allocation. An individual line's allocation can change when
|
|
82
|
+
other lines change; independently recalculating a subset or a partial reversal
|
|
83
|
+
does not preserve the original invoice allocation. Consumers needing historical
|
|
84
|
+
partial adjustments must use their accepted snapshot and explicit adjustment
|
|
85
|
+
policy. Mixed exact taxes `+0.8` and `-0.8` have zero aggregate and allocate
|
|
86
|
+
`0, 0` under this convention.
|
|
87
|
+
|
|
88
|
+
For example, lines `a` and `b`, each net 10, both receive `first` at 5 percent.
|
|
89
|
+
The invoice tax is 1, allocated `a: 1, b: 0`. If only `a` receives `second` at
|
|
90
|
+
100 percent with `priorComponentIds: ['first']`, its basis is 11 and its tax 11;
|
|
91
|
+
the total tax is 12. If only `b` receives `second`, its basis and tax are 10.
|
|
92
|
+
Negating both original amounts allocates `first` as `a: -1, b: 0`; the second
|
|
93
|
+
tax on `a` has basis and tax -11, total tax -12. At line scope the positive
|
|
94
|
+
example instead yields first tax 2 and total tax 13.
|
|
95
|
+
|
|
96
|
+
The result is `{ taxMinor, components }`. Components follow the supplied global
|
|
97
|
+
order and each returns `{ id, taxMinor, lines }`, where eligible lines are
|
|
98
|
+
`{ id, basisMinor, taxMinor }` in ascending code-unit ID order. Each component's
|
|
99
|
+
line allocations sum exactly to its total. Components with no eligible lines
|
|
100
|
+
return zero and `lines: []`; well-formed unused categories and components remain
|
|
101
|
+
valid for reusable policy. Exempt lines appear in no component allocation.
|
|
102
|
+
Repeated calculations and input line/category/membership permutations produce
|
|
103
|
+
identical output. All arithmetic intermediates use BigInt; every returned basis,
|
|
104
|
+
allocation, component tax and final tax must fit a safe integer, even when a
|
|
105
|
+
larger value would later cancel or a component has a zero rate.
|
|
106
|
+
|
|
107
|
+
All configuration is validated, including unused entries. Missing category
|
|
108
|
+
mapping, unknown IDs, duplicate line/category/component/membership/prior IDs,
|
|
109
|
+
forward/self references, missing arrays, malformed rates and unknown modes fail
|
|
110
|
+
closed. Rates use the same bounded decimal-string contract as the additive API.
|
|
111
|
+
The input is never mutated and the result holds no aliases to input objects.
|
package/dist/cost-billing.d.ts
CHANGED
|
@@ -55,4 +55,52 @@ export declare function calculateCostBillingTax(input: {
|
|
|
55
55
|
taxMinor: number;
|
|
56
56
|
}[];
|
|
57
57
|
};
|
|
58
|
+
/** Caller-ordered component; only explicitly named earlier taxes enter its basis. */
|
|
59
|
+
export interface OrderedCostBillingTaxComponent {
|
|
60
|
+
id: string;
|
|
61
|
+
percent: string;
|
|
62
|
+
priorComponentIds: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
/** Explicit membership; an empty component list declares the category exempt. */
|
|
65
|
+
export interface OrderedCostBillingTaxCategory {
|
|
66
|
+
category: string;
|
|
67
|
+
componentIds: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
/** Invoice allocation is mandatory at invoice scope and forbidden at line scope. */
|
|
70
|
+
export type OrderedCostBillingTaxInput = {
|
|
71
|
+
lines: readonly {
|
|
72
|
+
id: string;
|
|
73
|
+
amountMinor: number;
|
|
74
|
+
category: string;
|
|
75
|
+
}[];
|
|
76
|
+
categories: readonly OrderedCostBillingTaxCategory[];
|
|
77
|
+
components: readonly OrderedCostBillingTaxComponent[];
|
|
78
|
+
} & ({
|
|
79
|
+
roundingScope: 'line';
|
|
80
|
+
invoiceAllocation?: never;
|
|
81
|
+
} | {
|
|
82
|
+
roundingScope: 'invoice';
|
|
83
|
+
invoiceAllocation: 'signed-largest-remainder';
|
|
84
|
+
});
|
|
85
|
+
/** Component order follows policy; eligible line details use code-unit ID order. */
|
|
86
|
+
export interface OrderedCostBillingTaxResult {
|
|
87
|
+
taxMinor: number;
|
|
88
|
+
components: {
|
|
89
|
+
id: string;
|
|
90
|
+
taxMinor: number;
|
|
91
|
+
lines: {
|
|
92
|
+
id: string;
|
|
93
|
+
basisMinor: number;
|
|
94
|
+
taxMinor: number;
|
|
95
|
+
}[];
|
|
96
|
+
}[];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Calculate explicitly ordered taxes without selecting jurisdiction or persistence
|
|
100
|
+
* policy. Invoice allocation truncates exact line taxes toward zero, then assigns
|
|
101
|
+
* residual minor units by signed remainder, ties by code-unit line ID order.
|
|
102
|
+
* Full signed reversals negate allocations; caller-selected subsets use those
|
|
103
|
+
* allocations, never independently rounded prior taxes. See agents/cost-billing.md.
|
|
104
|
+
*/
|
|
105
|
+
export declare function calculateOrderedCostBillingTax(input: OrderedCostBillingTaxInput): OrderedCostBillingTaxResult;
|
|
58
106
|
//# sourceMappingURL=cost-billing.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cost-billing.d.ts","sourceRoot":"","sources":["../src/cost-billing.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,MAAM,MAAM,eAAe,GACvB,wBAAwB,GACxB,UAAU,GACV,aAAa,GACb,gBAAgB,CAAC;AAErB,wFAAwF;AACxF,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,kBAAkB,EAAE,CAAC;CACjC;AA4CD,6EAA6E;AAC7E,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,SAAS,kBAAkB,EAAE,GACvC,kBAAkB,CAmBpB;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,kBAAkB,CAAC;CAC5B,GAAG;IACF,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,kBAAkB,CAAC;CAC5B,CA+BA;AAED,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACxD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,KAAK,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxE,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAiDvE"}
|
|
1
|
+
{"version":3,"file":"cost-billing.d.ts","sourceRoot":"","sources":["../src/cost-billing.ts"],"names":[],"mappings":"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"}
|
package/dist/cost-billing.js
CHANGED
|
@@ -120,7 +120,115 @@ function calculateCostBillingTax(input) {
|
|
|
120
120
|
components
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
|
+
function compareTaxLineIds(a, b) {
|
|
124
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
125
|
+
}
|
|
126
|
+
function calculateOrderedCostBillingTax(input) {
|
|
127
|
+
if (input.roundingScope === "invoice") {
|
|
128
|
+
if (input.invoiceAllocation !== "signed-largest-remainder") throw new TypeError("Explicit invoice tax allocation is required");
|
|
129
|
+
} else if (input.roundingScope === "line") {
|
|
130
|
+
if ("invoiceAllocation" in input) throw new TypeError("Invoice allocation is not valid at line scope");
|
|
131
|
+
} else throw new TypeError("Unknown tax rounding scope");
|
|
132
|
+
if (!Array.isArray(input.lines) || !Array.isArray(input.categories) || !Array.isArray(input.components)) throw new TypeError("Tax lines, categories and components must be arrays");
|
|
133
|
+
const knownComponents = /* @__PURE__ */ new Set();
|
|
134
|
+
const definitions = input.components.map((component) => {
|
|
135
|
+
identifier(component.id);
|
|
136
|
+
if (knownComponents.has(component.id)) throw new TypeError("Duplicate tax component");
|
|
137
|
+
const rate = percentUnits(component.percent);
|
|
138
|
+
if (!Array.isArray(component.priorComponentIds)) throw new TypeError("Explicit prior tax component IDs are required");
|
|
139
|
+
const priorIds = /* @__PURE__ */ new Set();
|
|
140
|
+
for (const id of component.priorComponentIds) {
|
|
141
|
+
identifier(id);
|
|
142
|
+
if (!knownComponents.has(id)) throw new TypeError("Prior tax component must precede this component");
|
|
143
|
+
if (priorIds.has(id)) throw new TypeError("Duplicate prior tax component");
|
|
144
|
+
priorIds.add(id);
|
|
145
|
+
}
|
|
146
|
+
knownComponents.add(component.id);
|
|
147
|
+
return {
|
|
148
|
+
id: component.id,
|
|
149
|
+
rate,
|
|
150
|
+
priorIds
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
const categories = /* @__PURE__ */ new Map();
|
|
154
|
+
for (const category of input.categories) {
|
|
155
|
+
identifier(category.category);
|
|
156
|
+
if (categories.has(category.category)) throw new TypeError("Duplicate tax category");
|
|
157
|
+
if (!Array.isArray(category.componentIds)) throw new TypeError("Explicit category tax component IDs are required");
|
|
158
|
+
const ids = /* @__PURE__ */ new Set();
|
|
159
|
+
for (const id of category.componentIds) {
|
|
160
|
+
identifier(id);
|
|
161
|
+
if (!knownComponents.has(id)) throw new TypeError("Unknown category tax component");
|
|
162
|
+
if (ids.has(id)) throw new TypeError("Duplicate category tax component");
|
|
163
|
+
ids.add(id);
|
|
164
|
+
}
|
|
165
|
+
categories.set(category.category, ids);
|
|
166
|
+
}
|
|
167
|
+
const lineIds = /* @__PURE__ */ new Set();
|
|
168
|
+
const lines = input.lines.map((line) => {
|
|
169
|
+
identifier(line.id);
|
|
170
|
+
identifier(line.category);
|
|
171
|
+
if (lineIds.has(line.id)) throw new TypeError("Duplicate tax line");
|
|
172
|
+
lineIds.add(line.id);
|
|
173
|
+
const eligible = categories.get(line.category);
|
|
174
|
+
if (!eligible) throw new TypeError("Missing explicit tax category mapping");
|
|
175
|
+
return {
|
|
176
|
+
id: line.id,
|
|
177
|
+
net: money(line.amountMinor),
|
|
178
|
+
eligible,
|
|
179
|
+
taxes: /* @__PURE__ */ new Map()
|
|
180
|
+
};
|
|
181
|
+
}).sort(compareTaxLineIds);
|
|
182
|
+
let total = 0n;
|
|
183
|
+
const components = definitions.map((component) => {
|
|
184
|
+
const allocations = lines.filter((line) => line.eligible.has(component.id)).map((line) => {
|
|
185
|
+
let basis = line.net;
|
|
186
|
+
for (const id of component.priorIds) basis += line.taxes.get(id) ?? 0n;
|
|
187
|
+
const numerator = basis * component.rate;
|
|
188
|
+
return {
|
|
189
|
+
id: line.id,
|
|
190
|
+
line,
|
|
191
|
+
basisMinor: safe(basis),
|
|
192
|
+
numerator,
|
|
193
|
+
remainder: numerator % DENOMINATOR,
|
|
194
|
+
tax: input.roundingScope === "line" ? rounded(numerator) : numerator / DENOMINATOR
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
let tax = allocations.reduce((sum, allocation) => sum + allocation.tax, 0n);
|
|
198
|
+
if (input.roundingScope === "invoice") {
|
|
199
|
+
const target = rounded(allocations.reduce((sum, a) => sum + a.numerator, 0n));
|
|
200
|
+
const residual = target - tax;
|
|
201
|
+
if (residual !== 0n) {
|
|
202
|
+
const direction = residual > 0n ? 1n : -1n;
|
|
203
|
+
const ranked = [...allocations].sort((a, b) => {
|
|
204
|
+
const difference = (b.remainder - a.remainder) * direction;
|
|
205
|
+
return difference < 0n ? -1 : difference > 0n ? 1 : compareTaxLineIds(a, b);
|
|
206
|
+
});
|
|
207
|
+
const count = residual * direction;
|
|
208
|
+
for (let index = 0; BigInt(index) < count; index++) ranked[index].tax += direction;
|
|
209
|
+
}
|
|
210
|
+
tax = target;
|
|
211
|
+
}
|
|
212
|
+
total += tax;
|
|
213
|
+
return {
|
|
214
|
+
id: component.id,
|
|
215
|
+
taxMinor: safe(tax),
|
|
216
|
+
lines: allocations.map((allocation) => {
|
|
217
|
+
allocation.line.taxes.set(component.id, allocation.tax);
|
|
218
|
+
return {
|
|
219
|
+
id: allocation.id,
|
|
220
|
+
basisMinor: allocation.basisMinor,
|
|
221
|
+
taxMinor: safe(allocation.tax)
|
|
222
|
+
};
|
|
223
|
+
})
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
taxMinor: safe(total),
|
|
228
|
+
components
|
|
229
|
+
};
|
|
230
|
+
}
|
|
123
231
|
//#endregion
|
|
124
|
-
export { calculateCostBilling, calculateCostBillingTax, resolveCostMarkup };
|
|
232
|
+
export { calculateCostBilling, calculateCostBillingTax, calculateOrderedCostBillingTax, resolveCostMarkup };
|
|
125
233
|
|
|
126
234
|
//# sourceMappingURL=cost-billing.js.map
|
package/dist/cost-billing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cost-billing.js","names":["components"],"sources":["../src/cost-billing.ts"],"sourcesContent":["/** Exact, transport-neutral cost pricing. All money is signed integer minor units. */\nexport type CostBillingMode =\n | 'reimbursement-plus-fee'\n | 'fee-only'\n | 'nonbillable'\n | 'fixed-contract';\n\n/** Caller-defined precedence, most specific first. No industry hierarchy is assumed. */\nexport interface CostMarkupOverride {\n source: string;\n policy?: string;\n version?: string;\n /** Exact percent in [0, 100000], with at most six decimal places. */\n percent?: string;\n}\n\n/** Detached provenance suitable for the caller's accepted pricing snapshot. */\nexport interface ResolvedCostMarkup {\n percent: string;\n selectedIndex: number;\n overrides: CostMarkupOverride[];\n}\n\nconst SCALE = 1000000n;\nconst DENOMINATOR = 100n * SCALE;\nconst LIMIT = BigInt(Number.MAX_SAFE_INTEGER);\n\nfunction percentUnits(value: string): bigint {\n if (\n typeof value !== 'string' ||\n !/^(0|[1-9]\\d{0,5})(\\.\\d{1,6})?$/.test(value)\n ) {\n throw new TypeError(\n 'Percent must be an exact decimal string with at most six decimal places',\n );\n }\n const [whole, fraction = ''] = value.split('.');\n const units = BigInt(whole) * SCALE + BigInt(fraction.padEnd(6, '0'));\n if (units > 100000n * SCALE) throw new RangeError('Percent exceeds 100000');\n return units;\n}\n\nfunction money(value: number): bigint {\n if (!Number.isSafeInteger(value))\n throw new RangeError('Money must be safe integer minor units');\n return BigInt(value);\n}\n\nfunction safe(value: bigint): number {\n if (value > LIMIT || value < -LIMIT)\n throw new RangeError('Calculated money exceeds safe integer minor units');\n return Number(value);\n}\n\nfunction rounded(numerator: bigint): bigint {\n const magnitude = numerator < 0n ? -numerator : numerator;\n const result = (magnitude + DENOMINATOR / 2n) / DENOMINATOR;\n return numerator < 0n ? -result : result;\n}\n\nfunction identifier(value: string): void {\n if (typeof value !== 'string' || value.trim().length === 0)\n throw new TypeError('Identifiers must be nonempty strings');\n}\n\n/** Select the first supplied percent, validating even shadowed overrides. */\nexport function resolveCostMarkup(\n overrides: readonly CostMarkupOverride[],\n): ResolvedCostMarkup {\n let selectedIndex = -1;\n const copy = overrides.map((override, index) => {\n identifier(override.source);\n if (override.policy !== undefined) identifier(override.policy);\n if (override.version !== undefined) identifier(override.version);\n if (override.percent !== undefined) {\n percentUnits(override.percent);\n if (selectedIndex === -1) selectedIndex = index;\n }\n return { ...override };\n });\n if (selectedIndex === -1)\n throw new TypeError('An explicit markup percent is required');\n return {\n percent: copy[selectedIndex].percent as string,\n selectedIndex,\n overrides: copy,\n };\n}\n\n/** Calculate an explicitly supplied basis; this never reprices persisted snapshots. */\nexport function calculateCostBilling(input: {\n basisMinor: number;\n mode: CostBillingMode;\n markup: ResolvedCostMarkup;\n}): {\n principalMinor: number;\n feeMinor: number;\n totalMinor: number;\n markup: ResolvedCostMarkup;\n} {\n const basis = money(input.basisMinor);\n const markup = resolveCostMarkup(input.markup.overrides);\n if (\n markup.percent !== input.markup.percent ||\n markup.selectedIndex !== input.markup.selectedIndex\n ) {\n throw new TypeError('Resolved markup does not match its provenance');\n }\n let principal = 0n;\n let fee = 0n;\n switch (input.mode) {\n case 'reimbursement-plus-fee':\n principal = basis;\n fee = rounded(basis * percentUnits(markup.percent));\n break;\n case 'fee-only':\n fee = rounded(basis * percentUnits(markup.percent));\n break;\n case 'nonbillable':\n case 'fixed-contract':\n break;\n default:\n throw new TypeError('Unknown cost billing mode');\n }\n return {\n principalMinor: safe(principal),\n feeMinor: safe(fee),\n totalMinor: safe(principal + fee),\n markup,\n };\n}\n\n/** Explicit noncompounding tax components; an empty array means tax exempt. */\nexport interface CostBillingTaxRule {\n category: string;\n components: readonly { id: string; percent: string }[];\n}\n\n/**\n * Tax signed amounts with half-away-from-zero rounding. Invoice scope rounds each\n * component once after aggregation; line scope rounds each line/component pair.\n * A shared component ID must have the same exact rate across categories.\n */\nexport function calculateCostBillingTax(input: {\n lines: readonly { id: string; amountMinor: number; category: string }[];\n rules: readonly CostBillingTaxRule[];\n roundingScope: 'line' | 'invoice';\n}): { taxMinor: number; components: { id: string; taxMinor: number }[] } {\n if (input.roundingScope !== 'line' && input.roundingScope !== 'invoice') {\n throw new TypeError('Unknown tax rounding scope');\n }\n const categories = new Map<string, { id: string; rate: bigint }[]>();\n const rates = new Map<string, bigint>();\n const totals = new Map<string, bigint>();\n for (const rule of input.rules) {\n identifier(rule.category);\n if (categories.has(rule.category))\n throw new TypeError('Duplicate tax category');\n const seen = new Set<string>();\n const components = rule.components.map((component) => {\n identifier(component.id);\n const rate = percentUnits(component.percent);\n if (seen.has(component.id))\n throw new TypeError('Duplicate tax component');\n if (rates.has(component.id) && rates.get(component.id) !== rate)\n throw new TypeError('Conflicting tax component rate');\n seen.add(component.id);\n rates.set(component.id, rate);\n return { id: component.id, rate };\n });\n categories.set(rule.category, components);\n }\n const ids = new Set<string>();\n for (const line of input.lines) {\n identifier(line.id);\n identifier(line.category);\n if (ids.has(line.id)) throw new TypeError('Duplicate tax line');\n ids.add(line.id);\n const amount = money(line.amountMinor);\n const components = categories.get(line.category);\n if (!components)\n throw new TypeError('Missing explicit tax category mapping');\n for (const component of components) {\n const numerator = amount * component.rate;\n const tax =\n input.roundingScope === 'line' ? rounded(numerator) : numerator;\n totals.set(component.id, (totals.get(component.id) ?? 0n) + tax);\n }\n }\n let total = 0n;\n const components = Array.from(totals, ([id, value]) => {\n const tax = input.roundingScope === 'invoice' ? rounded(value) : value;\n total += tax;\n return { id, taxMinor: safe(tax) };\n });\n return { taxMinor: safe(total), components };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cost-billing.js","names":["components"],"sources":["../src/cost-billing.ts"],"sourcesContent":["/** Exact, transport-neutral cost pricing. All money is signed integer minor units. */\nexport type CostBillingMode =\n | 'reimbursement-plus-fee'\n | 'fee-only'\n | 'nonbillable'\n | 'fixed-contract';\n\n/** Caller-defined precedence, most specific first. No industry hierarchy is assumed. */\nexport interface CostMarkupOverride {\n source: string;\n policy?: string;\n version?: string;\n /** Exact percent in [0, 100000], with at most six decimal places. */\n percent?: string;\n}\n\n/** Detached provenance suitable for the caller's accepted pricing snapshot. */\nexport interface ResolvedCostMarkup {\n percent: string;\n selectedIndex: number;\n overrides: CostMarkupOverride[];\n}\n\nconst SCALE = 1000000n;\nconst DENOMINATOR = 100n * SCALE;\nconst LIMIT = BigInt(Number.MAX_SAFE_INTEGER);\n\nfunction percentUnits(value: string): bigint {\n if (\n typeof value !== 'string' ||\n !/^(0|[1-9]\\d{0,5})(\\.\\d{1,6})?$/.test(value)\n ) {\n throw new TypeError(\n 'Percent must be an exact decimal string with at most six decimal places',\n );\n }\n const [whole, fraction = ''] = value.split('.');\n const units = BigInt(whole) * SCALE + BigInt(fraction.padEnd(6, '0'));\n if (units > 100000n * SCALE) throw new RangeError('Percent exceeds 100000');\n return units;\n}\n\nfunction money(value: number): bigint {\n if (!Number.isSafeInteger(value))\n throw new RangeError('Money must be safe integer minor units');\n return BigInt(value);\n}\n\nfunction safe(value: bigint): number {\n if (value > LIMIT || value < -LIMIT)\n throw new RangeError('Calculated money exceeds safe integer minor units');\n return Number(value);\n}\n\nfunction rounded(numerator: bigint): bigint {\n const magnitude = numerator < 0n ? -numerator : numerator;\n const result = (magnitude + DENOMINATOR / 2n) / DENOMINATOR;\n return numerator < 0n ? -result : result;\n}\n\nfunction identifier(value: string): void {\n if (typeof value !== 'string' || value.trim().length === 0)\n throw new TypeError('Identifiers must be nonempty strings');\n}\n\n/** Select the first supplied percent, validating even shadowed overrides. */\nexport function resolveCostMarkup(\n overrides: readonly CostMarkupOverride[],\n): ResolvedCostMarkup {\n let selectedIndex = -1;\n const copy = overrides.map((override, index) => {\n identifier(override.source);\n if (override.policy !== undefined) identifier(override.policy);\n if (override.version !== undefined) identifier(override.version);\n if (override.percent !== undefined) {\n percentUnits(override.percent);\n if (selectedIndex === -1) selectedIndex = index;\n }\n return { ...override };\n });\n if (selectedIndex === -1)\n throw new TypeError('An explicit markup percent is required');\n return {\n percent: copy[selectedIndex].percent as string,\n selectedIndex,\n overrides: copy,\n };\n}\n\n/** Calculate an explicitly supplied basis; this never reprices persisted snapshots. */\nexport function calculateCostBilling(input: {\n basisMinor: number;\n mode: CostBillingMode;\n markup: ResolvedCostMarkup;\n}): {\n principalMinor: number;\n feeMinor: number;\n totalMinor: number;\n markup: ResolvedCostMarkup;\n} {\n const basis = money(input.basisMinor);\n const markup = resolveCostMarkup(input.markup.overrides);\n if (\n markup.percent !== input.markup.percent ||\n markup.selectedIndex !== input.markup.selectedIndex\n ) {\n throw new TypeError('Resolved markup does not match its provenance');\n }\n let principal = 0n;\n let fee = 0n;\n switch (input.mode) {\n case 'reimbursement-plus-fee':\n principal = basis;\n fee = rounded(basis * percentUnits(markup.percent));\n break;\n case 'fee-only':\n fee = rounded(basis * percentUnits(markup.percent));\n break;\n case 'nonbillable':\n case 'fixed-contract':\n break;\n default:\n throw new TypeError('Unknown cost billing mode');\n }\n return {\n principalMinor: safe(principal),\n feeMinor: safe(fee),\n totalMinor: safe(principal + fee),\n markup,\n };\n}\n\n/** Explicit noncompounding tax components; an empty array means tax exempt. */\nexport interface CostBillingTaxRule {\n category: string;\n components: readonly { id: string; percent: string }[];\n}\n\n/**\n * Tax signed amounts with half-away-from-zero rounding. Invoice scope rounds each\n * component once after aggregation; line scope rounds each line/component pair.\n * A shared component ID must have the same exact rate across categories.\n */\nexport function calculateCostBillingTax(input: {\n lines: readonly { id: string; amountMinor: number; category: string }[];\n rules: readonly CostBillingTaxRule[];\n roundingScope: 'line' | 'invoice';\n}): { taxMinor: number; components: { id: string; taxMinor: number }[] } {\n if (input.roundingScope !== 'line' && input.roundingScope !== 'invoice') {\n throw new TypeError('Unknown tax rounding scope');\n }\n const categories = new Map<string, { id: string; rate: bigint }[]>();\n const rates = new Map<string, bigint>();\n const totals = new Map<string, bigint>();\n for (const rule of input.rules) {\n identifier(rule.category);\n if (categories.has(rule.category))\n throw new TypeError('Duplicate tax category');\n const seen = new Set<string>();\n const components = rule.components.map((component) => {\n identifier(component.id);\n const rate = percentUnits(component.percent);\n if (seen.has(component.id))\n throw new TypeError('Duplicate tax component');\n if (rates.has(component.id) && rates.get(component.id) !== rate)\n throw new TypeError('Conflicting tax component rate');\n seen.add(component.id);\n rates.set(component.id, rate);\n return { id: component.id, rate };\n });\n categories.set(rule.category, components);\n }\n const ids = new Set<string>();\n for (const line of input.lines) {\n identifier(line.id);\n identifier(line.category);\n if (ids.has(line.id)) throw new TypeError('Duplicate tax line');\n ids.add(line.id);\n const amount = money(line.amountMinor);\n const components = categories.get(line.category);\n if (!components)\n throw new TypeError('Missing explicit tax category mapping');\n for (const component of components) {\n const numerator = amount * component.rate;\n const tax =\n input.roundingScope === 'line' ? rounded(numerator) : numerator;\n totals.set(component.id, (totals.get(component.id) ?? 0n) + tax);\n }\n }\n let total = 0n;\n const components = Array.from(totals, ([id, value]) => {\n const tax = input.roundingScope === 'invoice' ? rounded(value) : value;\n total += tax;\n return { id, taxMinor: safe(tax) };\n });\n return { taxMinor: safe(total), components };\n}\n\n/** Caller-ordered component; only explicitly named earlier taxes enter its basis. */\nexport interface OrderedCostBillingTaxComponent {\n id: string;\n percent: string;\n priorComponentIds: readonly string[];\n}\n\n/** Explicit membership; an empty component list declares the category exempt. */\nexport interface OrderedCostBillingTaxCategory {\n category: string;\n componentIds: readonly string[];\n}\n\n/** Invoice allocation is mandatory at invoice scope and forbidden at line scope. */\nexport type OrderedCostBillingTaxInput = {\n lines: readonly { id: string; amountMinor: number; category: string }[];\n categories: readonly OrderedCostBillingTaxCategory[];\n components: readonly OrderedCostBillingTaxComponent[];\n} & (\n | { roundingScope: 'line'; invoiceAllocation?: never }\n | {\n roundingScope: 'invoice';\n invoiceAllocation: 'signed-largest-remainder';\n }\n);\n\n/** Component order follows policy; eligible line details use code-unit ID order. */\nexport interface OrderedCostBillingTaxResult {\n taxMinor: number;\n components: {\n id: string;\n taxMinor: number;\n lines: { id: string; basisMinor: number; taxMinor: number }[];\n }[];\n}\n\nfunction compareTaxLineIds(a: { id: string }, b: { id: string }): number {\n return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;\n}\n\n/**\n * Calculate explicitly ordered taxes without selecting jurisdiction or persistence\n * policy. Invoice allocation truncates exact line taxes toward zero, then assigns\n * residual minor units by signed remainder, ties by code-unit line ID order.\n * Full signed reversals negate allocations; caller-selected subsets use those\n * allocations, never independently rounded prior taxes. See agents/cost-billing.md.\n */\nexport function calculateOrderedCostBillingTax(\n input: OrderedCostBillingTaxInput,\n): OrderedCostBillingTaxResult {\n if (input.roundingScope === 'invoice') {\n if (input.invoiceAllocation !== 'signed-largest-remainder')\n throw new TypeError('Explicit invoice tax allocation is required');\n } else if (input.roundingScope === 'line') {\n if ('invoiceAllocation' in input)\n throw new TypeError('Invoice allocation is not valid at line scope');\n } else {\n throw new TypeError('Unknown tax rounding scope');\n }\n if (\n !Array.isArray(input.lines) ||\n !Array.isArray(input.categories) ||\n !Array.isArray(input.components)\n )\n throw new TypeError('Tax lines, categories and components must be arrays');\n\n const knownComponents = new Set<string>();\n const definitions = input.components.map((component) => {\n identifier(component.id);\n if (knownComponents.has(component.id))\n throw new TypeError('Duplicate tax component');\n const rate = percentUnits(component.percent);\n if (!Array.isArray(component.priorComponentIds))\n throw new TypeError('Explicit prior tax component IDs are required');\n const priorIds = new Set<string>();\n for (const id of component.priorComponentIds) {\n identifier(id);\n if (!knownComponents.has(id))\n throw new TypeError('Prior tax component must precede this component');\n if (priorIds.has(id))\n throw new TypeError('Duplicate prior tax component');\n priorIds.add(id);\n }\n knownComponents.add(component.id);\n return { id: component.id, rate, priorIds };\n });\n const categories = new Map<string, Set<string>>();\n for (const category of input.categories) {\n identifier(category.category);\n if (categories.has(category.category))\n throw new TypeError('Duplicate tax category');\n if (!Array.isArray(category.componentIds))\n throw new TypeError('Explicit category tax component IDs are required');\n const ids = new Set<string>();\n for (const id of category.componentIds) {\n identifier(id);\n if (!knownComponents.has(id))\n throw new TypeError('Unknown category tax component');\n if (ids.has(id)) throw new TypeError('Duplicate category tax component');\n ids.add(id);\n }\n categories.set(category.category, ids);\n }\n const lineIds = new Set<string>();\n const lines = input.lines\n .map((line) => {\n identifier(line.id);\n identifier(line.category);\n if (lineIds.has(line.id)) throw new TypeError('Duplicate tax line');\n lineIds.add(line.id);\n const eligible = categories.get(line.category);\n if (!eligible)\n throw new TypeError('Missing explicit tax category mapping');\n return {\n id: line.id,\n net: money(line.amountMinor),\n eligible,\n taxes: new Map<string, bigint>(),\n };\n })\n .sort(compareTaxLineIds);\n\n let total = 0n;\n const components = definitions.map((component) => {\n const allocations = lines\n .filter((line) => line.eligible.has(component.id))\n .map((line) => {\n let basis = line.net;\n for (const id of component.priorIds) basis += line.taxes.get(id) ?? 0n;\n const numerator = basis * component.rate;\n return {\n id: line.id,\n line,\n basisMinor: safe(basis),\n numerator,\n remainder: numerator % DENOMINATOR,\n tax:\n input.roundingScope === 'line'\n ? rounded(numerator)\n : numerator / DENOMINATOR,\n };\n });\n let tax = allocations.reduce((sum, allocation) => sum + allocation.tax, 0n);\n if (input.roundingScope === 'invoice') {\n const target = rounded(\n allocations.reduce((sum, a) => sum + a.numerator, 0n),\n );\n const residual = target - tax;\n if (residual !== 0n) {\n const direction = residual > 0n ? 1n : -1n;\n const ranked = [...allocations].sort((a, b) => {\n const difference = (b.remainder - a.remainder) * direction;\n return difference < 0n\n ? -1\n : difference > 0n\n ? 1\n : compareTaxLineIds(a, b);\n });\n const count = residual * direction;\n for (let index = 0; BigInt(index) < count; index++)\n ranked[index].tax += direction;\n }\n tax = target;\n }\n total += tax;\n return {\n id: component.id,\n taxMinor: safe(tax),\n lines: allocations.map((allocation) => {\n allocation.line.taxes.set(component.id, allocation.tax);\n return {\n id: allocation.id,\n basisMinor: allocation.basisMinor,\n taxMinor: safe(allocation.tax),\n };\n }),\n };\n });\n return { taxMinor: safe(total), components };\n}\n"],"mappings":"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"}
|