@venturekit-pro/billing 0.0.42 → 0.0.43

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 CHANGED
@@ -18,6 +18,7 @@ npm install @venturekit-pro/billing@dev
18
18
  - **Subscription management** — create, upgrade, downgrade, cancel, pause, resume
19
19
  - **Invoice generation** — from plan + usage, with tax calculation
20
20
  - **Usage tracking** — record and meter feature usage
21
+ - **Metered allowances** — an included quantity per period, credits that roll over, exempt consumptions on debt, automatic top-ups
21
22
  - **Feature gating** — plan-based and usage-based access control
22
23
  - **Auto-migrations** — billing tables created automatically via `vk migrate`
23
24
 
@@ -113,6 +114,55 @@ The latest step whose day has come is due; each step goes once; steps skipped ov
113
114
  are gone (the schedule says *when to nudge*, not how many messages to catch up on);
114
115
  and a reminder a person sent since the previous step covers the current one.
115
116
 
117
+ ## Metered Allowances
118
+
119
+ For anything a subject consumes and pays for per unit — WhatsApp messages, SMS, AI tokens:
120
+ an **included** quantity per period (the plan's limit or a negotiated one), **credits** on
121
+ top (packs bought, grants, automatic top-ups), and **debt** for the consumptions that must
122
+ never be refused (a safety alert, a sign-in code) — paid from the next credits, so nothing
123
+ is free.
124
+
125
+ ```typescript
126
+ import { createMeter, createPostgresMeterStore } from '@venturekit-pro/billing';
127
+
128
+ const whatsapp = createMeter({
129
+ key: 'whatsapp_messages',
130
+ store: createPostgresMeterStore(tx.query), // vk_billing_meter_usage + vk_billing_credit_ledger
131
+ included: (tenantId) => planLimit(tenantId, 'whatsapp_messages'),
132
+ autoTopUp: { packSize: 100, request: (tenantId, req) => orders.autoTopUp(tenantId, req) },
133
+ onAlert: (tenantId, alert, balance) => notifyOwners(tenantId, alert, balance), // 80 %, nothing left
134
+ });
135
+
136
+ await whatsapp.check(tenantId, { reserved: queued }); // room for one more? nothing written
137
+ await whatsapp.consume(tenantId, { exempt: true }); // never refused; on debt when nothing is left
138
+ await whatsapp.addCredits(tenantId, 500, { kind: 'purchase', reference: invoiceId });
139
+ await whatsapp.balance(tenantId); // { included, used, fromCredits, onDebt, opening, expiring, credits }
140
+ ```
141
+
142
+ Every policy is a product decision, and off by default:
143
+
144
+ | Option | What it does |
145
+ | --- | --- |
146
+ | `credits: { expiresAfterPeriods: 1 }` | Bought credits expire with the period they were bought in (`12` = a year). Default: they roll over until spent. Per lot: `addCredits(…, { expiresAfterPeriods })`, `0` = never. |
147
+ | `carryOver: { max, expiresAfterPeriods }` | What a period included and did not use is carried into the next ones, capped. |
148
+ | `overage: { allow: 'all', maxDebt }` | Pay as you go: nobody is refused while what is owed stays under the cap. Default: only exempt consumptions may run a debt. |
149
+ | `autoTopUp: { packSize, below?, request }` | Ask for packs as soon as a debt appears (or the balance drops under `below`), under a key that holds until they arrive. |
150
+ | `timeZone: 'Africa/Casablanca'` | Periods follow the subject's clock rather than UTC. |
151
+ | `thresholds: [0.5, 0.8]` | Alerts on the included quantity; "nothing left" and "a debt began" alert once per credit level. |
152
+
153
+ What credits are worth in a period is a **replay** of the lots against every earlier
154
+ period's draws (`openingCredits`), spending the soonest-to-expire first, so expiry, voids
155
+ (what a voided pack had paid is owed again) and carry-over compose. With none of them on,
156
+ it is the running total — credits bought, less credits spent — read in one query.
157
+
158
+ `consume` takes the period row under a lock for the rest of the caller's transaction, so
159
+ concurrent consumptions cannot both take the last unit. Credits bought in another service
160
+ are supplied with `creditsFrom`: a running total (it never expires) or the lots with their
161
+ periods (any policy). A product that keeps the counters in its own tables, under its own
162
+ row-level security, implements `MeterStore` — eight small queries — and keeps every rule
163
+ here; the pure rules (`meterGate`, `splitOf`, `openingCredits`, `alertsCrossed`,
164
+ `topUpFor`, `packsFor`, `periodKey`, `shiftPeriod`) are exported too.
165
+
116
166
  ## Usage-to-Invoice Mapping
117
167
 
118
168
  ```typescript
package/dist/index.d.ts CHANGED
@@ -30,6 +30,8 @@ export { createFeatureGate, } from './gating/feature-gate.js';
30
30
  export type { FeatureCheckResult, FeatureGateOptions, } from './gating/feature-gate.js';
31
31
  export { resolveEntitlements, createEntitlementsResolver, EntitlementError, } from './entitlements/entitlements.js';
32
32
  export type { Entitlements, EntitlementOverrides, EntitlementErrorCode, EntitlementsResolver, EntitlementsResolverOptions, ResolvedFeature, LimitCheck, } from './entitlements/entitlements.js';
33
+ export { BEFORE_ANY_PERIOD, NO_SPLIT, afterConsume, afterRelease, alertsCrossed, carryOverSources, creditsLeft, debtOf, expiringLeft, gate as meterGate, includedLeft, openingCredits, packsFor, periodFromIndex, periodIndex, periodKey, remainingOf, shiftPeriod, splitOf, topUpFor, assertCreditEntry, createMemoryMeterStore, createPostgresMeterStore, createMeter, isExhausted, } from './meters/index.js';
34
+ export type { CarryOverPolicy, CreditLot, CreditPolicy, GateDecision as MeterGateDecision, GateInput as MeterGateInput, MeterAlert, MeterAlertKind, MeterBalance, MeterPeriod, MeterSplit, OpeningCredits, OveragePolicy, PeriodOptions, PeriodUsage, ReplayInput, TopUpPolicy, TopUpRequest, CreditEntry, CreditEntryRow, CreditKind, MeterPeriodRow, MeterStore, MeterQuerier, PostgresMeterStoreOptions, AutoTopUpOptions, ConsumeInput, ConsumeResult, Meter, MeterOptions, } from './meters/index.js';
33
35
  export { nextDunningStep, dunningScheduleRefusal, normalizeDunningSteps, DUNNING_SCHEDULE_LIMITS, } from './dunning/schedule.js';
34
36
  export type { DunningSchedule, DunningReminderSent, NextDunningStepInput, } from './dunning/schedule.js';
35
37
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EACL,WAAW,EACX,eAAe,EACf,UAAU,EACV,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpE,OAAO,EACL,yBAAyB,EACzB,6BAA6B,EAC7B,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EACV,iBAAiB,EACjB,0BAA0B,EAC1B,eAAe,GAChB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EACL,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,YAAY,EACZ,uBAAuB,GACxB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,WAAW,EACX,YAAY,EACZ,UAAU,GACX,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,gBAAgB,GACjB,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,YAAY,EACZ,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,2BAA2B,EAC3B,eAAe,EACf,UAAU,GACX,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAE/B;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAGhD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EACL,WAAW,EACX,eAAe,EACf,UAAU,EACV,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpE,OAAO,EACL,yBAAyB,EACzB,6BAA6B,EAC7B,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EACV,iBAAiB,EACjB,0BAA0B,EAC1B,eAAe,GAChB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EACL,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,YAAY,EACZ,uBAAuB,GACxB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EACL,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,WAAW,EACX,YAAY,EACZ,UAAU,GACX,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,gBAAgB,GACjB,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,YAAY,EACZ,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,2BAA2B,EAC3B,eAAe,EACf,UAAU,GACX,MAAM,gCAAgC,CAAC;AAIxC,OAAO,EACL,iBAAiB,EACjB,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,MAAM,EACN,YAAY,EACZ,IAAI,IAAI,SAAS,EACjB,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,eAAe,EACf,WAAW,EACX,SAAS,EACT,WAAW,EACX,WAAW,EACX,OAAO,EACP,QAAQ,EACR,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,WAAW,EACX,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,eAAe,EACf,SAAS,EACT,YAAY,EACZ,YAAY,IAAI,iBAAiB,EACjC,SAAS,IAAI,cAAc,EAC3B,UAAU,EACV,cAAc,EACd,YAAY,EACZ,WAAW,EACX,UAAU,EACV,cAAc,EACd,aAAa,EACb,aAAa,EACb,WAAW,EACX,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,EACX,cAAc,EACd,UAAU,EACV,cAAc,EACd,UAAU,EACV,YAAY,EACZ,yBAAyB,EACzB,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,KAAK,EACL,YAAY,GACb,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAE/B;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAGhD"}
package/dist/index.js CHANGED
@@ -33,6 +33,9 @@ export { createUsageTracker, createMemoryUsageStore, } from './usage/tracker.js'
33
33
  export { createFeatureGate, } from './gating/feature-gate.js';
34
34
  // Entitlements (plan + overrides → what a tenant may do, HTTP-mapped refusals)
35
35
  export { resolveEntitlements, createEntitlementsResolver, EntitlementError, } from './entitlements/entitlements.js';
36
+ // Metered allowances (included per period, credits that roll over or expire, carry-over,
37
+ // exempt or pay-as-you-go debt, automatic top-ups)
38
+ export { BEFORE_ANY_PERIOD, NO_SPLIT, afterConsume, afterRelease, alertsCrossed, carryOverSources, creditsLeft, debtOf, expiringLeft, gate as meterGate, includedLeft, openingCredits, packsFor, periodFromIndex, periodIndex, periodKey, remainingOf, shiftPeriod, splitOf, topUpFor, assertCreditEntry, createMemoryMeterStore, createPostgresMeterStore, createMeter, isExhausted, } from './meters/index.js';
36
39
  // Dunning (which payment reminder an unpaid invoice is due — pure, for a cron)
37
40
  export { nextDunningStep, dunningScheduleRefusal, normalizeDunningSteps, DUNNING_SCHEDULE_LIMITS, } from './dunning/schedule.js';
38
41
  /**
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,QAAQ;AACR,cAAc,kBAAkB,CAAC;AAEjC,0BAA0B;AAC1B,OAAO,EACL,WAAW,EACX,eAAe,EACf,UAAU,EACV,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,0BAA0B;AAC1B,OAAO,EACL,yBAAyB,EACzB,6BAA6B,EAC7B,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AAOpC,qBAAqB;AACrB,OAAO,EACL,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AAMjC,iBAAiB;AACjB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,oBAAoB,CAAC;AAO5B,iBAAiB;AACjB,OAAO,EACL,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAMlC,+EAA+E;AAC/E,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,gBAAgB,GACjB,MAAM,gCAAgC,CAAC;AAWxC,+EAA+E;AAC/E,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAO/B;;;;GAIG;AACH,MAAM,UAAU,uBAAuB;IACrC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/D,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AAC5C,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,QAAQ;AACR,cAAc,kBAAkB,CAAC;AAEjC,0BAA0B;AAC1B,OAAO,EACL,WAAW,EACX,eAAe,EACf,UAAU,EACV,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,0BAA0B;AAC1B,OAAO,EACL,yBAAyB,EACzB,6BAA6B,EAC7B,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AAOpC,qBAAqB;AACrB,OAAO,EACL,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AAMjC,iBAAiB;AACjB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,oBAAoB,CAAC;AAO5B,iBAAiB;AACjB,OAAO,EACL,iBAAiB,GAClB,MAAM,0BAA0B,CAAC;AAMlC,+EAA+E;AAC/E,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,gBAAgB,GACjB,MAAM,gCAAgC,CAAC;AAWxC,yFAAyF;AACzF,mDAAmD;AACnD,OAAO,EACL,iBAAiB,EACjB,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,MAAM,EACN,YAAY,EACZ,IAAI,IAAI,SAAS,EACjB,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,eAAe,EACf,WAAW,EACX,SAAS,EACT,WAAW,EACX,WAAW,EACX,OAAO,EACP,QAAQ,EACR,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,EACxB,WAAW,EACX,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAiC3B,+EAA+E;AAC/E,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAO/B;;;;GAIG;AACH,MAAM,UAAU,uBAAuB;IACrC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/D,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Metered allowances: an included quantity per period, credits on top that roll over or
3
+ * expire, unused quantities carried over, exempt (or pay-as-you-go) consumptions on debt,
4
+ * and automatic top-ups that pay it.
5
+ */
6
+ export { BEFORE_ANY_PERIOD, NO_SPLIT, afterConsume, afterRelease, alertsCrossed, carryOverSources, creditsLeft, debtOf, expiringLeft, gate, includedLeft, openingCredits, packsFor, periodFromIndex, periodIndex, periodKey, remainingOf, shiftPeriod, splitOf, topUpFor, } from './rules.js';
7
+ export type { CarryOverPolicy, CreditLot, CreditPolicy, GateDecision, GateInput, MeterAlert, MeterAlertKind, MeterBalance, MeterPeriod, MeterSplit, OpeningCredits, OveragePolicy, PeriodOptions, PeriodUsage, ReplayInput, TopUpPolicy, TopUpRequest, } from './rules.js';
8
+ export { assertCreditEntry, createMemoryMeterStore } from './store.js';
9
+ export type { CreditEntry, CreditEntryRow, CreditKind, MeterPeriodRow, MeterStore } from './store.js';
10
+ export { createPostgresMeterStore } from './postgres.js';
11
+ export type { MeterQuerier, PostgresMeterStoreOptions } from './postgres.js';
12
+ export { createMeter, isExhausted } from './meter.js';
13
+ export type { AutoTopUpOptions, ConsumeInput, ConsumeResult, Meter, MeterOptions } from './meter.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/meters/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,iBAAiB,EACjB,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,MAAM,EACN,YAAY,EACZ,IAAI,EACJ,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,eAAe,EACf,WAAW,EACX,SAAS,EACT,WAAW,EACX,WAAW,EACX,OAAO,EACP,QAAQ,GACT,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,eAAe,EACf,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,UAAU,EACV,cAAc,EACd,YAAY,EACZ,WAAW,EACX,UAAU,EACV,cAAc,EACd,aAAa,EACb,aAAa,EACb,WAAW,EACX,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACvE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACtG,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACtD,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,aAAa,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Metered allowances: an included quantity per period, credits on top that roll over or
3
+ * expire, unused quantities carried over, exempt (or pay-as-you-go) consumptions on debt,
4
+ * and automatic top-ups that pay it.
5
+ */
6
+ export { BEFORE_ANY_PERIOD, NO_SPLIT, afterConsume, afterRelease, alertsCrossed, carryOverSources, creditsLeft, debtOf, expiringLeft, gate, includedLeft, openingCredits, packsFor, periodFromIndex, periodIndex, periodKey, remainingOf, shiftPeriod, splitOf, topUpFor, } from './rules.js';
7
+ export { assertCreditEntry, createMemoryMeterStore } from './store.js';
8
+ export { createPostgresMeterStore } from './postgres.js';
9
+ export { createMeter, isExhausted } from './meter.js';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/meters/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,iBAAiB,EACjB,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,MAAM,EACN,YAAY,EACZ,IAAI,EACJ,YAAY,EACZ,cAAc,EACd,QAAQ,EACR,eAAe,EACf,WAAW,EACX,SAAS,EACT,WAAW,EACX,WAAW,EACX,OAAO,EACP,QAAQ,GACT,MAAM,YAAY,CAAC;AAoBpB,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * A metered allowance: the rules (`rules.ts`) over a store (`store.ts`), for one meter.
3
+ *
4
+ * ```ts
5
+ * const whatsapp = createMeter({
6
+ * key: 'whatsapp_messages',
7
+ * store: createPostgresMeterStore(tx.query),
8
+ * included: (tenantId) => entitlements.forSubject(tenantId).then((e) => e.limitOf('whatsapp_messages') ?? 0),
9
+ * autoTopUp: {
10
+ * packSize: 100,
11
+ * // no free messages: an exempt send that found nothing left buys a small pack
12
+ * request: (tenantId, req) => orders.autoTopUp(tenantId, req), // invoices it, then meter.addCredits(...)
13
+ * },
14
+ * onAlert: (tenantId, alert, balance) => notifyOwners(tenantId, alert, balance),
15
+ * });
16
+ *
17
+ * // at enqueue: is there room, counting what is already queued?
18
+ * if (!(await whatsapp.check(tenantId, { reserved: queued })).allowed) return skip('quota');
19
+ * // when the message leaves (or reserve-then-release, around a synchronous send)
20
+ * const used = await whatsapp.consume(tenantId, { exempt: isSafetyAlert });
21
+ * if (!used.allowed) return skip('quota');
22
+ * // the provider refused it synchronously: give the units back
23
+ * await whatsapp.release(tenantId, used);
24
+ * ```
25
+ *
26
+ * **The policies** — each a product decision, each off by default:
27
+ *
28
+ * - `credits: { expiresAfterPeriods: 1 }` — bought credits expire with the period they were
29
+ * bought in (`12` = a year; per lot with `addCredits(…, { expiresAfterPeriods })`).
30
+ * Default: they roll over until spent.
31
+ * - `carryOver: { max, expiresAfterPeriods }` — what a period included and did not use is
32
+ * carried into the next ones.
33
+ * - `overage: { allow: 'all', maxDebt }` — pay as you go: nobody is refused while what is
34
+ * owed stays under the cap. Default: only exempt consumptions may run a debt.
35
+ * - `autoTopUp: { packSize, below? }` — ask for packs as soon as a debt appears (or the
36
+ * balance drops under `below`), under an idempotency key that holds until they arrive.
37
+ * - `timeZone` — the periods follow the subject's clock, not UTC.
38
+ *
39
+ * **Credits** come from the store's ledger by default (`addCredits`). A product whose
40
+ * credits are bought elsewhere — another service, another database — passes `creditsFrom`
41
+ * instead: a running total (they never expire), or the lots with their periods (any policy).
42
+ *
43
+ * **Exempt** consumptions (`exempt: true`) are never refused. With nothing left they land
44
+ * on debt, which the next credits pay. `autoTopUp.request` is called after the usage is
45
+ * written, and a failure there never undoes the consumption: it is reported on the result
46
+ * (`topUp.error`) and asked again by the next consumption that still sees the debt.
47
+ */
48
+ import type { CarryOverPolicy, CreditLot, CreditPolicy, GateDecision, MeterAlert, MeterBalance, MeterPeriod, MeterSplit, OveragePolicy, TopUpPolicy, TopUpRequest } from './rules.js';
49
+ import type { CreditEntry, MeterStore } from './store.js';
50
+ type MaybePromise<T> = T | Promise<T>;
51
+ export interface AutoTopUpOptions extends TopUpPolicy {
52
+ /**
53
+ * Buy (or grant) `req.units` credits for the subject — typically an order and its
54
+ * invoice, then {@link Meter.addCredits} with `req.idempotencyKey`. Must be idempotent
55
+ * on that key.
56
+ */
57
+ request(subjectId: string, req: TopUpRequest & {
58
+ balance: MeterBalance;
59
+ }): MaybePromise<void>;
60
+ }
61
+ export interface MeterOptions {
62
+ /** The meter's name in the store (`whatsapp_messages`, `sms`, `ai_tokens`). */
63
+ key: string;
64
+ period?: MeterPeriod;
65
+ /** IANA zone the periods follow. Default UTC. */
66
+ timeZone?: string;
67
+ store: MeterStore;
68
+ /** What a period includes for this subject: its plan's limit, or one negotiated for it. */
69
+ included: (subjectId: string, period: string) => MaybePromise<number>;
70
+ /**
71
+ * The subject's credits, when they are kept outside the store's ledger: a running total
72
+ * (dated before any period, so it never expires) or the lots, each with its period.
73
+ */
74
+ creditsFrom?: (subjectId: string) => MaybePromise<number | readonly CreditLot[]>;
75
+ credits?: CreditPolicy;
76
+ carryOver?: CarryOverPolicy;
77
+ overage?: OveragePolicy;
78
+ /** Fractions of the included quantity to alert at, once per period. Default `[0.8]`. */
79
+ thresholds?: readonly number[];
80
+ autoTopUp?: AutoTopUpOptions;
81
+ /** Called once per alert key; a failure is swallowed — an alert never blocks a consumption. */
82
+ onAlert?: (subjectId: string, alert: MeterAlert, balance: MeterBalance) => MaybePromise<void>;
83
+ /** For tests. */
84
+ now?: () => Date;
85
+ }
86
+ export interface ConsumeInput {
87
+ cost?: number;
88
+ exempt?: boolean;
89
+ now?: Date;
90
+ }
91
+ export type ConsumeResult = {
92
+ allowed: true;
93
+ period: string;
94
+ split: MeterSplit;
95
+ /** The balance after this consumption. */
96
+ balance: MeterBalance;
97
+ alerts: MeterAlert[];
98
+ /** The automatic pack asked for, when the consumption left a debt (or crossed `below`). */
99
+ topUp?: TopUpRequest & {
100
+ error?: string;
101
+ };
102
+ } | {
103
+ allowed: false;
104
+ period: string;
105
+ reason: NonNullable<GateDecision['reason']>;
106
+ balance: MeterBalance;
107
+ };
108
+ export interface Meter {
109
+ readonly key: string;
110
+ /** The period `at` is counted in, on this meter's clock. */
111
+ periodOf(at?: Date): string;
112
+ balance(subjectId: string, now?: Date): Promise<MeterBalance>;
113
+ /** Room for one more, counting `reserved` units already spoken for; nothing is written. */
114
+ check(subjectId: string, input?: ConsumeInput & {
115
+ reserved?: number;
116
+ }): Promise<GateDecision & {
117
+ balance: MeterBalance;
118
+ }>;
119
+ /** Consume `cost` units: refused when there is no room (unless exempt, or paying as it goes), counted otherwise. */
120
+ consume(subjectId: string, input?: ConsumeInput): Promise<ConsumeResult>;
121
+ /** Give back what a consumption took — a send the provider refused before it left. */
122
+ release(subjectId: string, consumed: Pick<Extract<ConsumeResult, {
123
+ allowed: true;
124
+ }>, 'period' | 'split'>): Promise<void>;
125
+ /** Add credits to the store's ledger (a pack bought, a grant, an automatic top-up, a void). */
126
+ addCredits(subjectId: string, units: number, entry: Omit<CreditEntry, 'subjectId' | 'meterKey' | 'units'>): Promise<{
127
+ added: boolean;
128
+ }>;
129
+ }
130
+ export declare function createMeter(options: MeterOptions): Meter;
131
+ /** Whether nothing at all is left — for a UI that disables a "send" button exactly when the send would be refused. */
132
+ export declare function isExhausted(b: MeterBalance, cost?: number): boolean;
133
+ export {};
134
+ //# sourceMappingURL=meter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meter.d.ts","sourceRoot":"","sources":["../../src/meters/meter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAGH,OAAO,KAAK,EACV,eAAe,EACf,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,WAAW,EACX,UAAU,EACV,aAAa,EAEb,WAAW,EACX,YAAY,EACb,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,WAAW,EAAkB,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1E,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEtC,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG;QAAE,OAAO,EAAE,YAAY,CAAA;KAAE,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;CAC/F;AAED,MAAM,WAAW,YAAY;IAC3B,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,UAAU,CAAC;IAClB,2FAA2F;IAC3F,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,YAAY,CAAC,MAAM,CAAC,CAAC;IACtE;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAC,MAAM,GAAG,SAAS,SAAS,EAAE,CAAC,CAAC;IACjF,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,wFAAwF;IACxF,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,+FAA+F;IAC/F,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;IAC9F,iBAAiB;IACjB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,MAAM,aAAa,GACrB;IACE,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,UAAU,CAAC;IAClB,0CAA0C;IAC1C,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,2FAA2F;IAC3F,KAAK,CAAC,EAAE,YAAY,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C,GACD;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,WAAW,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,CAAC;AAE3G,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAC5B,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9D,2FAA2F;IAC3F,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,YAAY,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,GAAG;QAAE,OAAO,EAAE,YAAY,CAAA;KAAE,CAAC,CAAC;IAC1H,oHAAoH;IACpH,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACzE,sFAAsF;IACtF,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE;QAAE,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzH,+FAA+F;IAC/F,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACzI;AAID,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,KAAK,CAuGxD;AAED,sHAAsH;AACtH,wBAAgB,WAAW,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,SAAI,GAAG,OAAO,CAE9D"}
@@ -0,0 +1,150 @@
1
+ /**
2
+ * A metered allowance: the rules (`rules.ts`) over a store (`store.ts`), for one meter.
3
+ *
4
+ * ```ts
5
+ * const whatsapp = createMeter({
6
+ * key: 'whatsapp_messages',
7
+ * store: createPostgresMeterStore(tx.query),
8
+ * included: (tenantId) => entitlements.forSubject(tenantId).then((e) => e.limitOf('whatsapp_messages') ?? 0),
9
+ * autoTopUp: {
10
+ * packSize: 100,
11
+ * // no free messages: an exempt send that found nothing left buys a small pack
12
+ * request: (tenantId, req) => orders.autoTopUp(tenantId, req), // invoices it, then meter.addCredits(...)
13
+ * },
14
+ * onAlert: (tenantId, alert, balance) => notifyOwners(tenantId, alert, balance),
15
+ * });
16
+ *
17
+ * // at enqueue: is there room, counting what is already queued?
18
+ * if (!(await whatsapp.check(tenantId, { reserved: queued })).allowed) return skip('quota');
19
+ * // when the message leaves (or reserve-then-release, around a synchronous send)
20
+ * const used = await whatsapp.consume(tenantId, { exempt: isSafetyAlert });
21
+ * if (!used.allowed) return skip('quota');
22
+ * // the provider refused it synchronously: give the units back
23
+ * await whatsapp.release(tenantId, used);
24
+ * ```
25
+ *
26
+ * **The policies** — each a product decision, each off by default:
27
+ *
28
+ * - `credits: { expiresAfterPeriods: 1 }` — bought credits expire with the period they were
29
+ * bought in (`12` = a year; per lot with `addCredits(…, { expiresAfterPeriods })`).
30
+ * Default: they roll over until spent.
31
+ * - `carryOver: { max, expiresAfterPeriods }` — what a period included and did not use is
32
+ * carried into the next ones.
33
+ * - `overage: { allow: 'all', maxDebt }` — pay as you go: nobody is refused while what is
34
+ * owed stays under the cap. Default: only exempt consumptions may run a debt.
35
+ * - `autoTopUp: { packSize, below? }` — ask for packs as soon as a debt appears (or the
36
+ * balance drops under `below`), under an idempotency key that holds until they arrive.
37
+ * - `timeZone` — the periods follow the subject's clock, not UTC.
38
+ *
39
+ * **Credits** come from the store's ledger by default (`addCredits`). A product whose
40
+ * credits are bought elsewhere — another service, another database — passes `creditsFrom`
41
+ * instead: a running total (they never expire), or the lots with their periods (any policy).
42
+ *
43
+ * **Exempt** consumptions (`exempt: true`) are never refused. With nothing left they land
44
+ * on debt, which the next credits pay. `autoTopUp.request` is called after the usage is
45
+ * written, and a failure there never undoes the consumption: it is reported on the result
46
+ * (`topUp.error`) and asked again by the next consumption that still sees the debt.
47
+ */
48
+ import { BEFORE_ANY_PERIOD, afterConsume, alertsCrossed, carryOverSources, gate, openingCredits, periodKey, remainingOf, splitOf, topUpFor } from './rules.js';
49
+ const wholeOf = (n) => Math.max(0, Math.floor(Number(n) || 0));
50
+ export function createMeter(options) {
51
+ const { key, store } = options;
52
+ const kind = options.period ?? 'month';
53
+ const thresholds = options.thresholds ?? [0.8];
54
+ const clock = options.now ?? (() => new Date());
55
+ const periodOf = (at = clock()) => periodKey(at, kind, { timeZone: options.timeZone });
56
+ const includedOf = async (subjectId, p) => wholeOf(await options.included(subjectId, p));
57
+ async function lotsOf(subjectId) {
58
+ if (options.creditsFrom) {
59
+ const from = await options.creditsFrom(subjectId);
60
+ return typeof from === 'number' ? (from > 0 ? [{ units: Math.floor(from), period: BEFORE_ANY_PERIOD, expiresAfterPeriods: 0 }] : []) : [...from];
61
+ }
62
+ return (await store.lots(subjectId, key)).map((e) => ({ units: e.units, period: periodOf(e.createdAt), expiresAfterPeriods: e.expiresAfterPeriods }));
63
+ }
64
+ /** Everything but this period's counters: what credits are worth in it. */
65
+ async function opening(subjectId, p) {
66
+ const lots = await lotsOf(subjectId);
67
+ const credits = Math.max(0, lots.reduce((n, l) => n + l.units, 0));
68
+ const expires = (options.credits?.expiresAfterPeriods ?? 0) > 0 || lots.some((l) => (l.expiresAfterPeriods ?? 0) > 0);
69
+ let history;
70
+ if (expires || options.carryOver) {
71
+ history = await store.history(subjectId, key, p);
72
+ }
73
+ else {
74
+ /* nothing expires: the replay is the running total, and one figure stands for every earlier period */
75
+ const drawn = await store.drawnBefore(subjectId, key, p);
76
+ history = drawn > 0 ? [{ period: BEFORE_ANY_PERIOD, included: 0, used: drawn, fromCredits: drawn, onDebt: 0 }] : [];
77
+ }
78
+ const replay = { period: p, kind, lots, history, credits: options.credits, carryOver: options.carryOver };
79
+ const sources = options.carryOver ? carryOverSources(replay) : [];
80
+ const known = new Map(await Promise.all(sources.map(async (s) => [s, await includedOf(subjectId, s)])));
81
+ return { ...openingCredits({ ...replay, includedOf: (s) => known.get(s) ?? 0 }), credits };
82
+ }
83
+ const balanceFrom = (p, included, row, open) => ({
84
+ period: p,
85
+ included,
86
+ used: row?.used ?? 0,
87
+ fromCredits: row?.fromCredits ?? 0,
88
+ onDebt: row?.onDebt ?? 0,
89
+ ...open,
90
+ });
91
+ async function balance(subjectId, now = clock()) {
92
+ const p = periodOf(now);
93
+ const [included, row, open] = await Promise.all([includedOf(subjectId, p), store.readPeriod(subjectId, key, p), opening(subjectId, p)]);
94
+ return balanceFrom(p, included, row, open);
95
+ }
96
+ return {
97
+ key,
98
+ periodOf,
99
+ balance,
100
+ async check(subjectId, input = {}) {
101
+ const b = await balance(subjectId, input.now ?? clock());
102
+ return { ...gate(b, { ...input, overage: options.overage }), balance: b };
103
+ },
104
+ async consume(subjectId, input = {}) {
105
+ const p = periodOf(input.now ?? clock());
106
+ const included = await includedOf(subjectId, p);
107
+ /* the period row first: from here to commit, no other consumption of this subject reads it */
108
+ const row = await store.lockPeriod(subjectId, key, p, included);
109
+ const before = balanceFrom(p, included, row, await opening(subjectId, p));
110
+ const decision = gate(before, { cost: input.cost, exempt: input.exempt, overage: options.overage });
111
+ if (!decision.allowed)
112
+ return { allowed: false, period: p, reason: decision.reason, balance: before };
113
+ const split = splitOf(before, input.cost);
114
+ const after = afterConsume(before, split);
115
+ const alerts = alertsCrossed(before, after, thresholds, row.alerts);
116
+ await store.addUsage(subjectId, key, p, split, alerts.map((a) => a.key));
117
+ for (const alert of alerts) {
118
+ try {
119
+ await options.onAlert?.(subjectId, alert, after);
120
+ }
121
+ catch {
122
+ /* the slot is claimed either way: an alert is a courtesy, never a reason to refuse */
123
+ }
124
+ }
125
+ const result = { allowed: true, period: p, split, balance: after, alerts };
126
+ const due = options.autoTopUp ? topUpFor(after, options.autoTopUp) : null;
127
+ if (options.autoTopUp && due) {
128
+ try {
129
+ await options.autoTopUp.request(subjectId, { ...due, balance: after });
130
+ result.topUp = due;
131
+ }
132
+ catch (err) {
133
+ result.topUp = { ...due, error: err instanceof Error ? err.message : String(err) };
134
+ }
135
+ }
136
+ return result;
137
+ },
138
+ async release(subjectId, consumed) {
139
+ await store.releaseUsage(subjectId, key, consumed.period, consumed.split);
140
+ },
141
+ async addCredits(subjectId, units, entry) {
142
+ return store.addCredits({ subjectId, meterKey: key, units, ...entry });
143
+ },
144
+ };
145
+ }
146
+ /** Whether nothing at all is left — for a UI that disables a "send" button exactly when the send would be refused. */
147
+ export function isExhausted(b, cost = 1) {
148
+ return remainingOf(b) < Math.max(1, Math.floor(cost));
149
+ }
150
+ //# sourceMappingURL=meter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meter.js","sourceRoot":"","sources":["../../src/meters/meter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * {@link MeterStore} over `vk_billing_meter_usage` and `vk_billing_credit_ledger`
3
+ * (`vk_billing_0002_meters.sql`).
4
+ *
5
+ * Pass the transaction's `query` as `querier`: `lockPeriod` takes the period row with
6
+ * `INSERT … ON CONFLICT … DO UPDATE … RETURNING`, which holds its row lock until the
7
+ * transaction ends, so the read-decide-write of a consumption is serialised per subject
8
+ * and meter — two concurrent sends cannot both take the last unit. Typed structurally, so
9
+ * this package does not depend on `@venturekit/data`; its `query` and `tx.query` fit.
10
+ *
11
+ * Table names are options because a consumer may keep the counters next to its own data
12
+ * (a schema, a prefix); the columns are fixed. A consumer with tables of its own shape
13
+ * implements {@link MeterStore} instead — it is eight small queries.
14
+ */
15
+ import type { MeterStore } from './store.js';
16
+ /** `@venturekit/data`'s `query` / `tx.query` shape. */
17
+ export type MeterQuerier = <T = Record<string, unknown>[]>(sql: string, params?: unknown[]) => Promise<T>;
18
+ export interface PostgresMeterStoreOptions {
19
+ usageTable?: string;
20
+ ledgerTable?: string;
21
+ }
22
+ export declare function createPostgresMeterStore(querier: MeterQuerier, options?: PostgresMeterStoreOptions): MeterStore;
23
+ //# sourceMappingURL=postgres.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/meters/postgres.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAA2D,UAAU,EAAE,MAAM,YAAY,CAAC;AAEtG,uDAAuD;AACvD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;AAE1G,MAAM,WAAW,yBAAyB;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAmBD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,yBAA8B,GAAG,UAAU,CAoFnH"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * {@link MeterStore} over `vk_billing_meter_usage` and `vk_billing_credit_ledger`
3
+ * (`vk_billing_0002_meters.sql`).
4
+ *
5
+ * Pass the transaction's `query` as `querier`: `lockPeriod` takes the period row with
6
+ * `INSERT … ON CONFLICT … DO UPDATE … RETURNING`, which holds its row lock until the
7
+ * transaction ends, so the read-decide-write of a consumption is serialised per subject
8
+ * and meter — two concurrent sends cannot both take the last unit. Typed structurally, so
9
+ * this package does not depend on `@venturekit/data`; its `query` and `tx.query` fit.
10
+ *
11
+ * Table names are options because a consumer may keep the counters next to its own data
12
+ * (a schema, a prefix); the columns are fixed. A consumer with tables of its own shape
13
+ * implements {@link MeterStore} instead — it is eight small queries.
14
+ */
15
+ import { assertCreditEntry } from './store.js';
16
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/;
17
+ function identifier(name) {
18
+ /* interpolated into SQL — Postgres has no parameterised identifiers — so only plain names pass */
19
+ if (!IDENTIFIER.test(name))
20
+ throw new Error(`meter store: invalid table name '${name}'`);
21
+ return name;
22
+ }
23
+ const rowOf = (r) => ({ used: Number(r.used), fromCredits: Number(r.fromCredits), onDebt: Number(r.onDebt), alerts: [...(r.alerts ?? [])] });
24
+ export function createPostgresMeterStore(querier, options = {}) {
25
+ const usage = identifier(options.usageTable ?? 'vk_billing_meter_usage');
26
+ const ledger = identifier(options.ledgerTable ?? 'vk_billing_credit_ledger');
27
+ const COLUMNS = `used, from_credits AS "fromCredits", on_debt AS "onDebt", alerts`;
28
+ return {
29
+ async lockPeriod(subjectId, meterKey, period, included) {
30
+ const rows = await querier(`INSERT INTO ${usage} (subject_id, meter_key, period, included)
31
+ VALUES ($1, $2, $3, $4)
32
+ ON CONFLICT (subject_id, meter_key, period) DO UPDATE SET included = EXCLUDED.included, updated_at = NOW()
33
+ RETURNING ${COLUMNS}`, [subjectId, meterKey, period, included]);
34
+ if (!rows[0])
35
+ throw new Error(`meter ${meterKey}: no period row returned for ${subjectId} ${period}`);
36
+ return rowOf(rows[0]);
37
+ },
38
+ async readPeriod(subjectId, meterKey, period) {
39
+ const rows = await querier(`SELECT ${COLUMNS} FROM ${usage} WHERE subject_id = $1 AND meter_key = $2 AND period = $3`, [subjectId, meterKey, period]);
40
+ return rows[0] ? rowOf(rows[0]) : null;
41
+ },
42
+ async history(subjectId, meterKey, period) {
43
+ const rows = await querier(`SELECT period, included, ${COLUMNS} FROM ${usage} WHERE subject_id = $1 AND meter_key = $2 AND period < $3 ORDER BY period`, [subjectId, meterKey, period]);
44
+ return rows.map((r) => ({ period: r.period, included: Number(r.included), used: Number(r.used), fromCredits: Number(r.fromCredits), onDebt: Number(r.onDebt) }));
45
+ },
46
+ async drawnBefore(subjectId, meterKey, period) {
47
+ const rows = await querier(`SELECT COALESCE(SUM(from_credits + on_debt), 0) AS n FROM ${usage} WHERE subject_id = $1 AND meter_key = $2 AND period < $3`, [subjectId, meterKey, period]);
48
+ return Number(rows[0]?.n ?? 0);
49
+ },
50
+ async addUsage(subjectId, meterKey, period, split, alerts) {
51
+ await querier(`UPDATE ${usage}
52
+ SET used = used + $4 + $5 + $6, from_credits = from_credits + $5, on_debt = on_debt + $6,
53
+ alerts = ARRAY(SELECT DISTINCT unnest(alerts || $7::text[])), updated_at = NOW()
54
+ WHERE subject_id = $1 AND meter_key = $2 AND period = $3`, [subjectId, meterKey, period, split.included, split.credits, split.debt, [...alerts]]);
55
+ },
56
+ async releaseUsage(subjectId, meterKey, period, split) {
57
+ await querier(`UPDATE ${usage}
58
+ SET used = GREATEST(0, used - ($4 + $5 + $6)), from_credits = GREATEST(0, from_credits - $5), on_debt = GREATEST(0, on_debt - $6), updated_at = NOW()
59
+ WHERE subject_id = $1 AND meter_key = $2 AND period = $3`, [subjectId, meterKey, period, split.included, split.credits, split.debt]);
60
+ },
61
+ async lots(subjectId, meterKey) {
62
+ const rows = await querier(`SELECT units, kind, created_at AS "createdAt", expires_after_periods AS "expiresAfterPeriods"
63
+ FROM ${ledger} WHERE subject_id = $1 AND meter_key = $2 ORDER BY created_at, id`, [subjectId, meterKey]);
64
+ return rows.map((r) => ({
65
+ units: Number(r.units),
66
+ kind: r.kind,
67
+ createdAt: r.createdAt instanceof Date ? r.createdAt : new Date(r.createdAt),
68
+ expiresAfterPeriods: r.expiresAfterPeriods === null ? null : Number(r.expiresAfterPeriods),
69
+ }));
70
+ },
71
+ async addCredits(entry) {
72
+ assertCreditEntry(entry);
73
+ const rows = await querier(`INSERT INTO ${ledger} (subject_id, meter_key, units, kind, reference, idempotency_key, expires_after_periods, created_at)
74
+ VALUES ($1, $2, $3, $4, $5, $6, $7, COALESCE($8::timestamptz, NOW()))
75
+ ON CONFLICT (subject_id, meter_key, idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
76
+ RETURNING id`, [entry.subjectId, entry.meterKey, entry.units, entry.kind, entry.reference ?? null, entry.idempotencyKey ?? null, entry.expiresAfterPeriods ?? null, entry.at?.toISOString() ?? null]);
77
+ return { added: rows.length > 0 };
78
+ },
79
+ };
80
+ }
81
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/meters/postgres.ts"],"names":[],"mappings":"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"}