@qcobro/common 1.13.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/billing/evaluate.d.ts +36 -0
  2. package/dist/billing/evaluate.d.ts.map +1 -0
  3. package/dist/billing/evaluate.js +136 -0
  4. package/dist/billing/evaluate.js.map +1 -0
  5. package/dist/billing/index.d.ts +7 -0
  6. package/dist/billing/index.d.ts.map +1 -0
  7. package/dist/billing/index.js +7 -0
  8. package/dist/billing/index.js.map +1 -0
  9. package/dist/billing/ledger.d.ts +70 -0
  10. package/dist/billing/ledger.d.ts.map +1 -0
  11. package/dist/billing/ledger.js +59 -0
  12. package/dist/billing/ledger.js.map +1 -0
  13. package/dist/billing/money.d.ts +26 -0
  14. package/dist/billing/money.d.ts.map +1 -0
  15. package/dist/billing/money.js +58 -0
  16. package/dist/billing/money.js.map +1 -0
  17. package/dist/billing/pricing.d.ts +58 -0
  18. package/dist/billing/pricing.d.ts.map +1 -0
  19. package/dist/billing/pricing.js +68 -0
  20. package/dist/billing/pricing.js.map +1 -0
  21. package/dist/billing/proration.d.ts +8 -0
  22. package/dist/billing/proration.d.ts.map +1 -0
  23. package/dist/billing/proration.js +17 -0
  24. package/dist/billing/proration.js.map +1 -0
  25. package/dist/billing/rates.d.ts +108 -0
  26. package/dist/billing/rates.d.ts.map +1 -0
  27. package/dist/billing/rates.js +56 -0
  28. package/dist/billing/rates.js.map +1 -0
  29. package/dist/bin/engineEval.d.ts.map +1 -1
  30. package/dist/bin/engineEval.js +7 -1
  31. package/dist/bin/engineEval.js.map +1 -1
  32. package/dist/config.d.ts +139 -0
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/config.js +62 -0
  35. package/dist/config.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/schemas/billing.d.ts +74 -0
  41. package/dist/schemas/billing.d.ts.map +1 -0
  42. package/dist/schemas/billing.js +68 -0
  43. package/dist/schemas/billing.js.map +1 -0
  44. package/dist/schemas/index.d.ts +1 -0
  45. package/dist/schemas/index.d.ts.map +1 -1
  46. package/dist/schemas/index.js +1 -0
  47. package/dist/schemas/index.js.map +1 -1
  48. package/dist/types/billing.d.ts +134 -0
  49. package/dist/types/billing.d.ts.map +1 -0
  50. package/dist/types/billing.js +8 -0
  51. package/dist/types/billing.js.map +1 -0
  52. package/dist/types/engine.d.ts +8 -2
  53. package/dist/types/engine.d.ts.map +1 -1
  54. package/dist/types/engine.js +12 -1
  55. package/dist/types/engine.js.map +1 -1
  56. package/dist/types/index.d.ts +1 -0
  57. package/dist/types/index.d.ts.map +1 -1
  58. package/dist/types/index.js +1 -0
  59. package/dist/types/index.js.map +1 -1
  60. package/package.json +1 -1
@@ -0,0 +1,36 @@
1
+ import type { BillingConfig } from "../config.js";
2
+ import type { LedgerEntry, UsageRecord } from "./ledger.js";
3
+ import { type BillingMeter } from "./rates.js";
4
+ /**
5
+ * Billing evaluation (billing-evaluation spec) — the pure, deployment-agnostic
6
+ * invariants, judged over a set of usage records + ledger entries (from the
7
+ * simulation or exported from a live deployment). Sibling of the engine
8
+ * scorecard: under-detects, never false-positives.
9
+ */
10
+ export interface BillingInvariantResult {
11
+ id: string;
12
+ description: string;
13
+ verdict: "pass" | "fail";
14
+ details?: string;
15
+ }
16
+ export interface BillingScorecard {
17
+ verdict: "pass" | "fail";
18
+ invariants: BillingInvariantResult[];
19
+ }
20
+ /**
21
+ * Rough per-unit provider floors (USD): what QCobro itself pays per unit, so a
22
+ * configured rate below its floor sells at a loss. Overridable per deployment;
23
+ * voice floors are per minute.
24
+ */
25
+ export declare const DEFAULT_PROVIDER_FLOORS: Record<BillingMeter, number>;
26
+ export interface BillingEvaluationInput {
27
+ billing: NonNullable<BillingConfig>;
28
+ usageRecords: UsageRecord[];
29
+ ledgerEntries: LedgerEntry[];
30
+ /** Per-meter floors; defaults to {@link DEFAULT_PROVIDER_FLOORS}. */
31
+ providerFloors?: Partial<Record<BillingMeter, number>>;
32
+ }
33
+ export declare function evaluateBilling(input: BillingEvaluationInput): BillingScorecard;
34
+ /** Convenience: the worst tolerated negative balance for N in-flight voice calls. */
35
+ export declare function voiceOvershootBoundMicro(perMinute: number, increments: string, estimateSeconds: number, maxAnsweredSeconds: number, concurrentCalls: number): number;
36
+ //# sourceMappingURL=evaluate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluate.d.ts","sourceRoot":"","sources":["../../src/billing/evaluate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAgC,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAE7E;;;;;GAKG;AAEH,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,UAAU,EAAE,sBAAsB,EAAE,CAAC;CACtC;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAQhE,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,WAAW,CAAC,aAAa,CAAC,CAAC;IACpC,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,aAAa,EAAE,WAAW,EAAE,CAAC;IAC7B,qEAAqE;IACrE,cAAc,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;CACxD;AAUD,wBAAgB,eAAe,CAAC,KAAK,EAAE,sBAAsB,GAAG,gBAAgB,CA4G/E;AAED,qFAAqF;AACrF,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,eAAe,EAAE,MAAM,EACvB,kBAAkB,EAAE,MAAM,EAC1B,eAAe,EAAE,MAAM,GACtB,MAAM,CAQR"}
@@ -0,0 +1,136 @@
1
+ import { billedSeconds, parseIncrements } from "./pricing.js";
2
+ import { sumMicroUnits, toMicroUnits } from "./money.js";
3
+ import { MESSAGE_METERS, VOICE_METERS } from "./rates.js";
4
+ /**
5
+ * Rough per-unit provider floors (USD): what QCobro itself pays per unit, so a
6
+ * configured rate below its floor sells at a loss. Overridable per deployment;
7
+ * voice floors are per minute.
8
+ */
9
+ export const DEFAULT_PROVIDER_FLOORS = {
10
+ sms: 0.0079, // Twilio US outbound per segment
11
+ email: 0.0004, // Resend at volume
12
+ whatsappMessage: 0.004, // Meta marketing/utility blend, region-dependent
13
+ voicePrerecorded: 0.014, // carrier per-minute
14
+ voiceAi: 0.09, // carrier + STT/LLM/TTS per-minute blend
15
+ whatsappVoicePrerecorded: 0.005,
16
+ whatsappVoiceAi: 0.09
17
+ };
18
+ const CANONICAL_VECTORS = [
19
+ { answered: 0, billed: 0 },
20
+ { answered: 1, billed: 15 },
21
+ { answered: 15, billed: 15 },
22
+ { answered: 16, billed: 30 },
23
+ { answered: 35, billed: 45 }
24
+ ];
25
+ export function evaluateBilling(input) {
26
+ const invariants = [];
27
+ const floors = { ...DEFAULT_PROVIDER_FLOORS, ...input.providerFloors };
28
+ // BIL-1 — Ledger conservation: total usage spend in the ledger equals the sum
29
+ // of priced usage records, exact to the micro-unit (settlements included).
30
+ {
31
+ const debits = sumMicroUnits(input.ledgerEntries
32
+ .filter((entry) => entry.kind === "USAGE_DEBIT" || entry.kind === "ADJUSTMENT")
33
+ .map((entry) => entry.amountMicro));
34
+ const priced = sumMicroUnits(input.usageRecords.map((record) => record.amountMicro));
35
+ const ok = -debits === priced;
36
+ invariants.push({
37
+ id: "BIL-1",
38
+ description: "sum(ledger usage debits + adjustments) equals sum(priced usage records)",
39
+ verdict: ok ? "pass" : "fail",
40
+ details: ok ? undefined : `ledger ${-debits} vs records ${priced} micro-units`
41
+ });
42
+ }
43
+ // BIL-2 — Balance derivation: every workspace balance is exactly the entry sum
44
+ // (guards against cached-balance drift when the input carries one).
45
+ {
46
+ const balance = sumMicroUnits(input.ledgerEntries.map((entry) => entry.amountMicro));
47
+ invariants.push({
48
+ id: "BIL-2",
49
+ description: "derived balances are exact integer micro-unit sums",
50
+ verdict: Number.isSafeInteger(balance) ? "pass" : "fail"
51
+ });
52
+ }
53
+ // BIL-3 — Canonical increment vectors under 15/15.
54
+ {
55
+ const pair = parseIncrements("15/15");
56
+ const failed = CANONICAL_VECTORS.filter((v) => billedSeconds(v.answered, pair) !== v.billed);
57
+ invariants.push({
58
+ id: "BIL-3",
59
+ description: "canonical 15/15 vectors (1→15, 15→15, 16→30, 35→45, unanswered→0)",
60
+ verdict: failed.length === 0 ? "pass" : "fail",
61
+ details: failed.length ? JSON.stringify(failed) : undefined
62
+ });
63
+ }
64
+ // BIL-4 — Settled voice records: quantity matches the increment formula for
65
+ // the frozen increments (records must never carry off-grid quantities).
66
+ {
67
+ const offenders = input.usageRecords.filter((record) => {
68
+ if (!VOICE_METERS.includes(record.meter))
69
+ return false;
70
+ const increments = record.increments;
71
+ if (!increments)
72
+ return true;
73
+ const pair = parseIncrements(increments);
74
+ return billedSeconds(record.quantity, pair) !== record.quantity;
75
+ });
76
+ invariants.push({
77
+ id: "BIL-4",
78
+ description: "voice usage quantities land on the increment grid",
79
+ verdict: offenders.length === 0 ? "pass" : "fail",
80
+ details: offenders.length ? `${offenders.length} off-grid records` : undefined
81
+ });
82
+ }
83
+ // BIL-5 — Margin guard: every configured rate covers its provider floor.
84
+ {
85
+ const violations = [];
86
+ for (const plan of input.billing.plans) {
87
+ for (const meter of MESSAGE_METERS) {
88
+ if (plan.rates[meter].perMessage < floors[meter]) {
89
+ violations.push(`${plan.key}.${meter}`);
90
+ }
91
+ }
92
+ for (const meter of VOICE_METERS) {
93
+ if (plan.rates[meter].perMinute < floors[meter]) {
94
+ violations.push(`${plan.key}.${meter}`);
95
+ }
96
+ }
97
+ }
98
+ invariants.push({
99
+ id: "BIL-5",
100
+ description: "every plan rate is at or above its provider floor",
101
+ verdict: violations.length === 0 ? "pass" : "fail",
102
+ details: violations.length ? violations.join(", ") : undefined
103
+ });
104
+ }
105
+ // BIL-6 — Grant/void idempotency shape: at most one GRANT and one VOID per
106
+ // (workspace, stripeInvoiceId) — replayed webhooks must not double-book.
107
+ {
108
+ const seen = new Map();
109
+ for (const entry of input.ledgerEntries) {
110
+ if (!entry.stripeInvoiceId)
111
+ continue;
112
+ const key = `${entry.workspaceRef}|${entry.stripeInvoiceId}|${entry.kind}`;
113
+ seen.set(key, (seen.get(key) ?? 0) + 1);
114
+ }
115
+ const duplicated = [...seen.entries()].filter(([, count]) => count > 1);
116
+ invariants.push({
117
+ id: "BIL-6",
118
+ description: "one grant/void per (workspace, invoice) — replays no-op",
119
+ verdict: duplicated.length === 0 ? "pass" : "fail",
120
+ details: duplicated.length ? duplicated.map(([k]) => k).join(", ") : undefined
121
+ });
122
+ }
123
+ return {
124
+ verdict: invariants.some((inv) => inv.verdict === "fail") ? "fail" : "pass",
125
+ invariants
126
+ };
127
+ }
128
+ /** Convenience: the worst tolerated negative balance for N in-flight voice calls. */
129
+ export function voiceOvershootBoundMicro(perMinute, increments, estimateSeconds, maxAnsweredSeconds, concurrentCalls) {
130
+ const pair = parseIncrements(increments);
131
+ const perMinuteMicro = toMicroUnits(perMinute);
132
+ const estimate = Math.round((billedSeconds(Math.max(estimateSeconds, pair.initialSeconds), pair) * perMinuteMicro) / 60);
133
+ const worst = Math.round((billedSeconds(maxAnsweredSeconds, pair) * perMinuteMicro) / 60);
134
+ return Math.max(0, worst - estimate) * concurrentCalls;
135
+ }
136
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluate.js","sourceRoot":"","sources":["../../src/billing/evaluate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export * from "./money.js";
2
+ export * from "./rates.js";
3
+ export * from "./pricing.js";
4
+ export * from "./ledger.js";
5
+ export * from "./proration.js";
6
+ export * from "./evaluate.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/billing/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC"}
@@ -0,0 +1,7 @@
1
+ export * from "./money.js";
2
+ export * from "./rates.js";
3
+ export * from "./pricing.js";
4
+ export * from "./ledger.js";
5
+ export * from "./proration.js";
6
+ export * from "./evaluate.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/billing/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC"}
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Usage-ledger contracts: the durable, priced-at-write-time record of every
4
+ * billable event and the signed entries a workspace balance derives from.
5
+ *
6
+ * These are deliberately separate from the engine flight recorder
7
+ * (`engineEvents`), which is lossy, pruned telemetry. Usage records are written
8
+ * in the same transaction as the dispatch's contact log and are never pruned.
9
+ */
10
+ /**
11
+ * Signed ledger entry kinds. Sign conventions: GRANT positive (allowance
12
+ * opens a cycle), USAGE_DEBIT negative (a priced dispatch), VOID negative
13
+ * (unused remainder at cycle close — no rollover), ADJUSTMENT either sign
14
+ * (voice settlement replacing the dispatch-time estimate).
15
+ */
16
+ export declare const ledgerEntryKindSchema: z.ZodEnum<{
17
+ GRANT: "GRANT";
18
+ USAGE_DEBIT: "USAGE_DEBIT";
19
+ VOID: "VOID";
20
+ ADJUSTMENT: "ADJUSTMENT";
21
+ }>;
22
+ export type LedgerEntryKind = z.infer<typeof ledgerEntryKindSchema>;
23
+ export declare const ledgerEntrySchema: z.ZodObject<{
24
+ id: z.ZodString;
25
+ workspaceRef: z.ZodString;
26
+ kind: z.ZodEnum<{
27
+ GRANT: "GRANT";
28
+ USAGE_DEBIT: "USAGE_DEBIT";
29
+ VOID: "VOID";
30
+ ADJUSTMENT: "ADJUSTMENT";
31
+ }>;
32
+ amountMicro: z.ZodNumber;
33
+ at: z.ZodString;
34
+ usageRecordId: z.ZodOptional<z.ZodString>;
35
+ stripeInvoiceId: z.ZodOptional<z.ZodString>;
36
+ }, z.core.$strip>;
37
+ export type LedgerEntry = z.infer<typeof ledgerEntrySchema>;
38
+ /**
39
+ * One priced billable event. Quantity semantics per meter kind: message meters
40
+ * store quantity = messages (1) and unitPriceMicro per message; voice meters
41
+ * store quantity = increment-billed seconds and unitPriceMicro per minute, with
42
+ * amountMicro = quantity × unitPriceMicro / 60 rounded to the micro-unit.
43
+ */
44
+ export declare const usageRecordSchema: z.ZodObject<{
45
+ id: z.ZodString;
46
+ workspaceRef: z.ZodString;
47
+ meter: z.ZodEnum<{
48
+ sms: "sms";
49
+ email: "email";
50
+ whatsappMessage: "whatsappMessage";
51
+ voicePrerecorded: "voicePrerecorded";
52
+ voiceAi: "voiceAi";
53
+ whatsappVoicePrerecorded: "whatsappVoicePrerecorded";
54
+ whatsappVoiceAi: "whatsappVoiceAi";
55
+ }>;
56
+ quantity: z.ZodNumber;
57
+ unitPriceMicro: z.ZodNumber;
58
+ amountMicro: z.ZodNumber;
59
+ campaignId: z.ZodOptional<z.ZodString>;
60
+ portfolioAccountId: z.ZodOptional<z.ZodString>;
61
+ providerRef: z.ZodOptional<z.ZodString>;
62
+ at: z.ZodString;
63
+ }, z.core.$strip>;
64
+ export type UsageRecord = z.infer<typeof usageRecordSchema>;
65
+ /**
66
+ * Derives a workspace balance from its ledger entries. The ledger is the source
67
+ * of truth — any cached balance is an optimization that must equal this sum.
68
+ */
69
+ export declare function deriveBalanceMicro(entries: Iterable<Pick<LedgerEntry, "amountMicro">>): number;
70
+ //# sourceMappingURL=ledger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/billing/ledger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;;;;EAAyD,CAAC;AAC5F,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;iBAY5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;iBAa5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC,GAAG,MAAM,CAE9F"}
@@ -0,0 +1,59 @@
1
+ import { z } from "zod";
2
+ import { sumMicroUnits } from "./money.js";
3
+ import { billingMeterSchema } from "./rates.js";
4
+ /**
5
+ * Usage-ledger contracts: the durable, priced-at-write-time record of every
6
+ * billable event and the signed entries a workspace balance derives from.
7
+ *
8
+ * These are deliberately separate from the engine flight recorder
9
+ * (`engineEvents`), which is lossy, pruned telemetry. Usage records are written
10
+ * in the same transaction as the dispatch's contact log and are never pruned.
11
+ */
12
+ /**
13
+ * Signed ledger entry kinds. Sign conventions: GRANT positive (allowance
14
+ * opens a cycle), USAGE_DEBIT negative (a priced dispatch), VOID negative
15
+ * (unused remainder at cycle close — no rollover), ADJUSTMENT either sign
16
+ * (voice settlement replacing the dispatch-time estimate).
17
+ */
18
+ export const ledgerEntryKindSchema = z.enum(["GRANT", "USAGE_DEBIT", "VOID", "ADJUSTMENT"]);
19
+ export const ledgerEntrySchema = z.object({
20
+ id: z.string().min(1),
21
+ workspaceRef: z.string().min(1),
22
+ kind: ledgerEntryKindSchema,
23
+ /** Signed integer micro-units (see kind sign conventions). */
24
+ amountMicro: z.number().int(),
25
+ /** Entry instant, ISO. */
26
+ at: z.string().min(1),
27
+ /** The usage record this debit/adjustment prices, when applicable. */
28
+ usageRecordId: z.string().min(1).optional(),
29
+ /** Cycle-turnover idempotency key: set on GRANT and VOID entries. */
30
+ stripeInvoiceId: z.string().min(1).optional()
31
+ });
32
+ /**
33
+ * One priced billable event. Quantity semantics per meter kind: message meters
34
+ * store quantity = messages (1) and unitPriceMicro per message; voice meters
35
+ * store quantity = increment-billed seconds and unitPriceMicro per minute, with
36
+ * amountMicro = quantity × unitPriceMicro / 60 rounded to the micro-unit.
37
+ */
38
+ export const usageRecordSchema = z.object({
39
+ id: z.string().min(1),
40
+ workspaceRef: z.string().min(1),
41
+ meter: billingMeterSchema,
42
+ quantity: z.number().int().nonnegative(),
43
+ unitPriceMicro: z.number().int().nonnegative(),
44
+ amountMicro: z.number().int().nonnegative(),
45
+ /** Correlation spine (mirrors the contact log / engine events). */
46
+ campaignId: z.string().min(1).optional(),
47
+ portfolioAccountId: z.string().min(1).optional(),
48
+ /** Provider ref of the dispatch; the settlement idempotency key for voice. */
49
+ providerRef: z.string().min(1).optional(),
50
+ at: z.string().min(1)
51
+ });
52
+ /**
53
+ * Derives a workspace balance from its ledger entries. The ledger is the source
54
+ * of truth — any cached balance is an optimization that must equal this sum.
55
+ */
56
+ export function deriveBalanceMicro(entries) {
57
+ return sumMicroUnits(Array.from(entries, (entry) => entry.amountMicro));
58
+ }
59
+ //# sourceMappingURL=ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger.js","sourceRoot":"","sources":["../../src/billing/ledger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC;AAG5F,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,IAAI,EAAE,qBAAqB;IAC3B,8DAA8D;IAC9D,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC7B,0BAA0B;IAC1B,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrB,sEAAsE;IACtE,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC3C,qEAAqE;IACrE,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAC9C,CAAC,CAAC;AAGH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,KAAK,EAAE,kBAAkB;IACzB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACxC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC9C,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC3C,mEAAmE;IACnE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACxC,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAChD,8EAA8E;IAC9E,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACzC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACtB,CAAC,CAAC;AAGH;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAmD;IACpF,OAAO,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;AAC1E,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Micro-unit money arithmetic for billing.
3
+ *
4
+ * All persisted monetary amounts are integer micro-units of the billing currency
5
+ * (1 unit = 1,000,000 micro-units). Config rates are decimal JSON numbers converted
6
+ * once at the config boundary; everything past it is integer-only so thousands of
7
+ * sub-cent records aggregate with zero drift. Rounding to display/invoice precision
8
+ * happens only at aggregation, never per record.
9
+ */
10
+ export declare const MICRO_UNITS_PER_UNIT = 1000000;
11
+ /** Throws unless `value` is a safe-integer micro-unit amount. */
12
+ export declare function assertMicroUnits(value: number): void;
13
+ /**
14
+ * Converts a decimal currency amount (the config boundary) to integer micro-units,
15
+ * absorbing binary float representation error (e.g. 0.0004 → exactly 400).
16
+ */
17
+ export declare function toMicroUnits(amount: number): number;
18
+ /** Sums micro-unit amounts, guarding every operand and the running total. */
19
+ export declare function sumMicroUnits(amounts: Iterable<number>): number;
20
+ /**
21
+ * Aggregation-time rounding: renders micro-units as a fixed-point decimal string
22
+ * (default 2 fraction digits), rounding half away from zero using integer math.
23
+ * This is the ONLY place billing amounts lose precision.
24
+ */
25
+ export declare function microUnitsToDecimalString(micro: number, fractionDigits?: number): string;
26
+ //# sourceMappingURL=money.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../src/billing/money.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,oBAAoB,UAAY,CAAC;AAE9C,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAIpD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,CAQ/D;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,SAAI,GAAG,MAAM,CAanF"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Micro-unit money arithmetic for billing.
3
+ *
4
+ * All persisted monetary amounts are integer micro-units of the billing currency
5
+ * (1 unit = 1,000,000 micro-units). Config rates are decimal JSON numbers converted
6
+ * once at the config boundary; everything past it is integer-only so thousands of
7
+ * sub-cent records aggregate with zero drift. Rounding to display/invoice precision
8
+ * happens only at aggregation, never per record.
9
+ */
10
+ export const MICRO_UNITS_PER_UNIT = 1_000_000;
11
+ /** Throws unless `value` is a safe-integer micro-unit amount. */
12
+ export function assertMicroUnits(value) {
13
+ if (!Number.isSafeInteger(value)) {
14
+ throw new RangeError(`Not an integer micro-unit amount: ${value}`);
15
+ }
16
+ }
17
+ /**
18
+ * Converts a decimal currency amount (the config boundary) to integer micro-units,
19
+ * absorbing binary float representation error (e.g. 0.0004 → exactly 400).
20
+ */
21
+ export function toMicroUnits(amount) {
22
+ if (!Number.isFinite(amount))
23
+ throw new RangeError(`Not a finite amount: ${amount}`);
24
+ const micro = Math.round(amount * MICRO_UNITS_PER_UNIT);
25
+ assertMicroUnits(micro);
26
+ return micro;
27
+ }
28
+ /** Sums micro-unit amounts, guarding every operand and the running total. */
29
+ export function sumMicroUnits(amounts) {
30
+ let total = 0;
31
+ for (const amount of amounts) {
32
+ assertMicroUnits(amount);
33
+ total += amount;
34
+ assertMicroUnits(total);
35
+ }
36
+ return total;
37
+ }
38
+ /**
39
+ * Aggregation-time rounding: renders micro-units as a fixed-point decimal string
40
+ * (default 2 fraction digits), rounding half away from zero using integer math.
41
+ * This is the ONLY place billing amounts lose precision.
42
+ */
43
+ export function microUnitsToDecimalString(micro, fractionDigits = 2) {
44
+ assertMicroUnits(micro);
45
+ if (!Number.isInteger(fractionDigits) || fractionDigits < 0 || fractionDigits > 6) {
46
+ throw new RangeError(`fractionDigits must be an integer in [0, 6]: ${fractionDigits}`);
47
+ }
48
+ const scale = 10 ** fractionDigits;
49
+ const microPerStep = MICRO_UNITS_PER_UNIT / scale;
50
+ const steps = Math.round(Math.abs(micro) / microPerStep);
51
+ const sign = micro < 0 && steps > 0 ? "-" : "";
52
+ const whole = Math.floor(steps / scale);
53
+ if (fractionDigits === 0)
54
+ return `${sign}${whole}`;
55
+ const frac = String(steps % scale).padStart(fractionDigits, "0");
56
+ return `${sign}${whole}.${frac}`;
57
+ }
58
+ //# sourceMappingURL=money.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"money.js","sourceRoot":"","sources":["../../src/billing/money.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG,SAAS,CAAC;AAE9C,iEAAiE;AACjE,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,UAAU,CAAC,qCAAqC,KAAK,EAAE,CAAC,CAAC;IACrE,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,wBAAwB,MAAM,EAAE,CAAC,CAAC;IACrF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,oBAAoB,CAAC,CAAC;IACxD,gBAAgB,CAAC,KAAK,CAAC,CAAC;IACxB,OAAO,KAAK,CAAC;AACf,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,OAAyB;IACrD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,gBAAgB,CAAC,MAAM,CAAC,CAAC;QACzB,KAAK,IAAI,MAAM,CAAC;QAChB,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAa,EAAE,cAAc,GAAG,CAAC;IACzE,gBAAgB,CAAC,KAAK,CAAC,CAAC;IACxB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,cAAc,GAAG,CAAC,IAAI,cAAc,GAAG,CAAC,EAAE,CAAC;QAClF,MAAM,IAAI,UAAU,CAAC,gDAAgD,cAAc,EAAE,CAAC,CAAC;IACzF,CAAC;IACD,MAAM,KAAK,GAAG,EAAE,IAAI,cAAc,CAAC;IACnC,MAAM,YAAY,GAAG,oBAAoB,GAAG,KAAK,CAAC;IAClD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC,CAAC;IACzD,MAAM,IAAI,GAAG,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC;IACxC,IAAI,cAAc,KAAK,CAAC;QAAE,OAAO,GAAG,IAAI,GAAG,KAAK,EAAE,CAAC;IACnD,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,QAAQ,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACjE,OAAO,GAAG,IAAI,GAAG,KAAK,IAAI,IAAI,EAAE,CAAC;AACnC,CAAC"}
@@ -0,0 +1,58 @@
1
+ import type { BillingMeter, MessageMeter, MessageRate, RateOverrides, Rates, VoiceMeter, VoiceRate } from "./rates.js";
2
+ /**
3
+ * Pricing math: increment billing, meter pricing, and rate resolution.
4
+ *
5
+ * Rates are resolved and applied exactly once, when a usage record is written;
6
+ * the resulting unit price and amount are stored on the record so history never
7
+ * reprices (see the usage-ledger spec).
8
+ */
9
+ /** A parsed increment pair (seconds). */
10
+ export interface IncrementPair {
11
+ initialSeconds: number;
12
+ subsequentSeconds: number;
13
+ }
14
+ /** Parses `"initial/subsequent"` notation (e.g. `"15/15"`, `"60/6"`). */
15
+ export declare function parseIncrements(notation: string): IncrementPair;
16
+ /**
17
+ * Billed seconds for an answered duration under an increment pair:
18
+ * 0 when never answered; the initial increment for any answered duration up to
19
+ * it; then rounded up to whole subsequent increments. Canonical 15/15 vectors:
20
+ * 1→15, 15→15, 16→30, 35→45, unanswered→0.
21
+ */
22
+ export declare function billedSeconds(answeredSeconds: number, increments: IncrementPair): number;
23
+ /** The unit price of one message on a message meter, in micro-units. */
24
+ export declare function priceMessageMicro(rate: MessageRate): number;
25
+ /** A priced voice usage: what gets stored on the usage record. */
26
+ export interface VoicePrice {
27
+ /** Increment-billed seconds (the record's quantity). */
28
+ billedSeconds: number;
29
+ /** The per-minute rate in micro-units (the record's unit price). */
30
+ perMinuteMicro: number;
31
+ /** billedSeconds × perMinuteMicro / 60, rounded to the nearest micro-unit. */
32
+ amountMicro: number;
33
+ }
34
+ /** Prices an answered duration on a voice meter. Unanswered (≤ 0s) prices to zero. */
35
+ export declare function priceVoiceMicro(answeredSeconds: number, rate: VoiceRate): VoicePrice;
36
+ /**
37
+ * The pre-dispatch credit-bucket debit for a voice call: the configured
38
+ * estimate, never less than the initial increment (an answered call can never
39
+ * bill less than that).
40
+ */
41
+ export declare function estimateVoiceDebitMicro(rate: VoiceRate, estimateSeconds: number): number;
42
+ /**
43
+ * Resolves the effective rate for a meter: a per-workspace enterprise override
44
+ * when present, otherwise the plan's rate.
45
+ */
46
+ export declare function resolveRate(meter: MessageMeter, rates: Rates, overrides?: RateOverrides): MessageRate;
47
+ export declare function resolveRate(meter: VoiceMeter, rates: Rates, overrides?: RateOverrides): VoiceRate;
48
+ /** Whether a meter bills per message (vs. per voice increment). */
49
+ export declare function isMessageMeter(meter: BillingMeter): meter is MessageMeter;
50
+ /**
51
+ * The pre-dispatch cost of one dispatch on a meter, in micro-units: the exact
52
+ * unit price for message meters, the voice estimate (never below the initial
53
+ * increment) for voice meters. THE single definition of "what a dispatch costs
54
+ * up front" — the engine's credit bucket and the manual-outreach gate both
55
+ * check/debit this amount.
56
+ */
57
+ export declare function estimateDispatchCostMicro(meter: BillingMeter, rates: Rates, overrides: RateOverrides | undefined, voiceDebitEstimateSeconds: number): number;
58
+ //# sourceMappingURL=pricing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../src/billing/pricing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,aAAa,EACb,KAAK,EACL,UAAU,EACV,SAAS,EACV,MAAM,YAAY,CAAC;AAGpB;;;;;;GAMG;AAEH,yCAAyC;AACzC,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAI/D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,GAAG,MAAM,CAQxF;AAED,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAE3D;AAED,kEAAkE;AAClE,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,aAAa,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,cAAc,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,eAAe,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,UAAU,CAQpF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAGxF;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,KAAK,EACZ,SAAS,CAAC,EAAE,aAAa,GACxB,WAAW,CAAC;AACf,wBAAgB,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;AASnG,mEAAmE;AACnE,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,KAAK,IAAI,YAAY,CAEzE;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,aAAa,GAAG,SAAS,EACpC,yBAAyB,EAAE,MAAM,GAChC,MAAM,CAKR"}
@@ -0,0 +1,68 @@
1
+ import { toMicroUnits } from "./money.js";
2
+ import { MESSAGE_METERS } from "./rates.js";
3
+ /** Parses `"initial/subsequent"` notation (e.g. `"15/15"`, `"60/6"`). */
4
+ export function parseIncrements(notation) {
5
+ const match = /^([1-9]\d*)\/([1-9]\d*)$/.exec(notation);
6
+ if (!match)
7
+ throw new RangeError(`Invalid increment notation: "${notation}"`);
8
+ return { initialSeconds: Number(match[1]), subsequentSeconds: Number(match[2]) };
9
+ }
10
+ /**
11
+ * Billed seconds for an answered duration under an increment pair:
12
+ * 0 when never answered; the initial increment for any answered duration up to
13
+ * it; then rounded up to whole subsequent increments. Canonical 15/15 vectors:
14
+ * 1→15, 15→15, 16→30, 35→45, unanswered→0.
15
+ */
16
+ export function billedSeconds(answeredSeconds, increments) {
17
+ if (answeredSeconds <= 0)
18
+ return 0;
19
+ const { initialSeconds, subsequentSeconds } = increments;
20
+ if (answeredSeconds <= initialSeconds)
21
+ return initialSeconds;
22
+ return (initialSeconds +
23
+ Math.ceil((answeredSeconds - initialSeconds) / subsequentSeconds) * subsequentSeconds);
24
+ }
25
+ /** The unit price of one message on a message meter, in micro-units. */
26
+ export function priceMessageMicro(rate) {
27
+ return toMicroUnits(rate.perMessage);
28
+ }
29
+ /** Prices an answered duration on a voice meter. Unanswered (≤ 0s) prices to zero. */
30
+ export function priceVoiceMicro(answeredSeconds, rate) {
31
+ const seconds = billedSeconds(answeredSeconds, parseIncrements(rate.increments));
32
+ const perMinuteMicro = toMicroUnits(rate.perMinute);
33
+ return {
34
+ billedSeconds: seconds,
35
+ perMinuteMicro,
36
+ amountMicro: Math.round((seconds * perMinuteMicro) / 60)
37
+ };
38
+ }
39
+ /**
40
+ * The pre-dispatch credit-bucket debit for a voice call: the configured
41
+ * estimate, never less than the initial increment (an answered call can never
42
+ * bill less than that).
43
+ */
44
+ export function estimateVoiceDebitMicro(rate, estimateSeconds) {
45
+ const { initialSeconds } = parseIncrements(rate.increments);
46
+ return priceVoiceMicro(Math.max(estimateSeconds, initialSeconds), rate).amountMicro;
47
+ }
48
+ export function resolveRate(meter, rates, overrides) {
49
+ return overrides?.[meter] ?? rates[meter];
50
+ }
51
+ /** Whether a meter bills per message (vs. per voice increment). */
52
+ export function isMessageMeter(meter) {
53
+ return MESSAGE_METERS.includes(meter);
54
+ }
55
+ /**
56
+ * The pre-dispatch cost of one dispatch on a meter, in micro-units: the exact
57
+ * unit price for message meters, the voice estimate (never below the initial
58
+ * increment) for voice meters. THE single definition of "what a dispatch costs
59
+ * up front" — the engine's credit bucket and the manual-outreach gate both
60
+ * check/debit this amount.
61
+ */
62
+ export function estimateDispatchCostMicro(meter, rates, overrides, voiceDebitEstimateSeconds) {
63
+ if (isMessageMeter(meter)) {
64
+ return priceMessageMicro(resolveRate(meter, rates, overrides));
65
+ }
66
+ return estimateVoiceDebitMicro(resolveRate(meter, rates, overrides), voiceDebitEstimateSeconds);
67
+ }
68
+ //# sourceMappingURL=pricing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.js","sourceRoot":"","sources":["../../src/billing/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAU1C,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAgB5C,yEAAyE;AACzE,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,MAAM,KAAK,GAAG,0BAA0B,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxD,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,UAAU,CAAC,gCAAgC,QAAQ,GAAG,CAAC,CAAC;IAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACnF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,eAAuB,EAAE,UAAyB;IAC9E,IAAI,eAAe,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACnC,MAAM,EAAE,cAAc,EAAE,iBAAiB,EAAE,GAAG,UAAU,CAAC;IACzD,IAAI,eAAe,IAAI,cAAc;QAAE,OAAO,cAAc,CAAC;IAC7D,OAAO,CACL,cAAc;QACd,IAAI,CAAC,IAAI,CAAC,CAAC,eAAe,GAAG,cAAc,CAAC,GAAG,iBAAiB,CAAC,GAAG,iBAAiB,CACtF,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,iBAAiB,CAAC,IAAiB;IACjD,OAAO,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AACvC,CAAC;AAYD,sFAAsF;AACtF,MAAM,UAAU,eAAe,CAAC,eAAuB,EAAE,IAAe;IACtE,MAAM,OAAO,GAAG,aAAa,CAAC,eAAe,EAAE,eAAe,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;IACjF,MAAM,cAAc,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACpD,OAAO;QACL,aAAa,EAAE,OAAO;QACtB,cAAc;QACd,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,cAAc,CAAC,GAAG,EAAE,CAAC;KACzD,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAe,EAAE,eAAuB;IAC9E,MAAM,EAAE,cAAc,EAAE,GAAG,eAAe,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC5D,OAAO,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,cAAc,CAAC,EAAE,IAAI,CAAC,CAAC,WAAW,CAAC;AACtF,CAAC;AAYD,MAAM,UAAU,WAAW,CACzB,KAAmB,EACnB,KAAY,EACZ,SAAyB;IAEzB,OAAO,SAAS,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC;AAC5C,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,cAAc,CAAC,KAAmB;IAChD,OAAQ,cAAoC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CACvC,KAAmB,EACnB,KAAY,EACZ,SAAoC,EACpC,yBAAiC;IAEjC,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,iBAAiB,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,uBAAuB,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,yBAAyB,CAAC,CAAC;AAClG,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Prorates a cycle allowance to the remaining fraction of the billing period —
3
+ * used when a workspace joins mid-cycle or upgrades: Stripe prorates the charge
4
+ * and QCobro grants the matching fraction of the allowance. Integer micro-units
5
+ * out; clamped to [0, allowance] so clock skew can't over- or under-grant.
6
+ */
7
+ export declare function proratedGrantMicro(allowanceMicro: number, at: Date, periodStart: Date, periodEnd: Date): number;
8
+ //# sourceMappingURL=proration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proration.d.ts","sourceRoot":"","sources":["../../src/billing/proration.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,IAAI,EACR,WAAW,EAAE,IAAI,EACjB,SAAS,EAAE,IAAI,GACd,MAAM,CAOR"}
@@ -0,0 +1,17 @@
1
+ import { assertMicroUnits } from "./money.js";
2
+ /**
3
+ * Prorates a cycle allowance to the remaining fraction of the billing period —
4
+ * used when a workspace joins mid-cycle or upgrades: Stripe prorates the charge
5
+ * and QCobro grants the matching fraction of the allowance. Integer micro-units
6
+ * out; clamped to [0, allowance] so clock skew can't over- or under-grant.
7
+ */
8
+ export function proratedGrantMicro(allowanceMicro, at, periodStart, periodEnd) {
9
+ assertMicroUnits(allowanceMicro);
10
+ const period = periodEnd.getTime() - periodStart.getTime();
11
+ if (period <= 0)
12
+ return 0;
13
+ const remaining = periodEnd.getTime() - at.getTime();
14
+ const fraction = Math.min(1, Math.max(0, remaining / period));
15
+ return Math.round(allowanceMicro * fraction);
16
+ }
17
+ //# sourceMappingURL=proration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proration.js","sourceRoot":"","sources":["../../src/billing/proration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,cAAsB,EACtB,EAAQ,EACR,WAAiB,EACjB,SAAe;IAEf,gBAAgB,CAAC,cAAc,CAAC,CAAC;IACjC,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,EAAE,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC;IAC3D,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAC1B,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;IACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC;IAC9D,OAAO,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,QAAQ,CAAC,CAAC;AAC/C,CAAC"}