@venturekit-pro/billing 0.0.41 → 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.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Where a meter keeps its counters: one row per (subject, meter, period) and a ledger of
3
+ * credits. {@link createMemoryMeterStore} for tests and local runs;
4
+ * `createPostgresMeterStore` (`postgres.ts`) for `vk_billing_meter_usage` /
5
+ * `vk_billing_credit_ledger`; or the consumer's own tables behind the same interface — a
6
+ * product whose counters live in its own schema, under its own row-level security, writes
7
+ * the SQL and keeps every rule here.
8
+ */
9
+ import type { MeterSplit, PeriodUsage } from './rules.js';
10
+ /** One period's counters, as stored. */
11
+ export interface MeterPeriodRow {
12
+ used: number;
13
+ fromCredits: number;
14
+ onDebt: number;
15
+ /** Alert keys already sent this period (`rules.ts` `alertsCrossed`). */
16
+ alerts: string[];
17
+ }
18
+ export type CreditKind = 'purchase' | 'grant' | 'auto_topup' | 'refund' | 'void' | 'adjustment';
19
+ export interface CreditEntry {
20
+ subjectId: string;
21
+ meterKey: string;
22
+ /** Signed: a purchase adds, a void takes back. Never zero. */
23
+ units: number;
24
+ kind: CreditKind;
25
+ /** What the entry is for — an order, an invoice — free text the consumer reads back. */
26
+ reference?: string | null;
27
+ /** Makes the entry once-only: a second add with the same key changes nothing. */
28
+ idempotencyKey?: string | null;
29
+ /**
30
+ * This lot's life in periods, overriding the meter's (`1` = this period only, `0` = never
31
+ * expires): a promotional grant good for three months on a meter whose purchases roll over.
32
+ * `null` / absent = the meter's policy.
33
+ */
34
+ expiresAfterPeriods?: number | null;
35
+ /** When it was made — the period it counts from. Default now. */
36
+ at?: Date;
37
+ }
38
+ /** A ledger entry as the meter reads it back. */
39
+ export interface CreditEntryRow {
40
+ units: number;
41
+ kind: CreditKind;
42
+ createdAt: Date;
43
+ expiresAfterPeriods: number | null;
44
+ }
45
+ export interface MeterStore {
46
+ /**
47
+ * The period's row, created at zero if absent, and **held** for the rest of the
48
+ * transaction (a Postgres row lock): what a consumption reads here cannot be read by a
49
+ * concurrent one until it commits. `included` is recorded on the row as it stands now.
50
+ */
51
+ lockPeriod(subjectId: string, meterKey: string, period: string, included: number): Promise<MeterPeriodRow>;
52
+ /** The period's row without taking it, or `null` when nothing was consumed in it. */
53
+ readPeriod(subjectId: string, meterKey: string, period: string): Promise<MeterPeriodRow | null>;
54
+ /** Every period before `period`, oldest first — what the replay reads when credits expire or allowances carry over. */
55
+ history(subjectId: string, meterKey: string, period: string): Promise<PeriodUsage[]>;
56
+ /** Credits drawn in every period before `period` (`fromCredits + onDebt`) — all a meter whose credits never expire needs. */
57
+ drawnBefore(subjectId: string, meterKey: string, period: string): Promise<number>;
58
+ /** Add a consumption's split to the period, and record the alerts it claimed. */
59
+ addUsage(subjectId: string, meterKey: string, period: string, split: MeterSplit, alerts: readonly string[]): Promise<void>;
60
+ /** Take a split back off the period (clamped at zero). */
61
+ releaseUsage(subjectId: string, meterKey: string, period: string, split: MeterSplit): Promise<void>;
62
+ /** Every ledger entry of the subject's meter, oldest first. */
63
+ lots(subjectId: string, meterKey: string): Promise<CreditEntryRow[]>;
64
+ /** Append to the ledger. `added: false` when the idempotency key was already used. */
65
+ addCredits(entry: CreditEntry): Promise<{
66
+ added: boolean;
67
+ }>;
68
+ }
69
+ /** The shape every store refuses the same way. */
70
+ export declare function assertCreditEntry(entry: CreditEntry): void;
71
+ /** In-memory store. Single-process only: no lock is needed because nothing interleaves inside one call. */
72
+ export declare function createMemoryMeterStore(): MeterStore & {
73
+ ledger: Array<CreditEntry & {
74
+ createdAt: Date;
75
+ }>;
76
+ };
77
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/meters/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Where a meter keeps its counters: one row per (subject, meter, period) and a ledger of
3
+ * credits. {@link createMemoryMeterStore} for tests and local runs;
4
+ * `createPostgresMeterStore` (`postgres.ts`) for `vk_billing_meter_usage` /
5
+ * `vk_billing_credit_ledger`; or the consumer's own tables behind the same interface — a
6
+ * product whose counters live in its own schema, under its own row-level security, writes
7
+ * the SQL and keeps every rule here.
8
+ */
9
+ /** The shape every store refuses the same way. */
10
+ export function assertCreditEntry(entry) {
11
+ if (!Number.isInteger(entry.units) || entry.units === 0)
12
+ throw new Error('meter credits: units must be a non-zero integer');
13
+ if (entry.expiresAfterPeriods !== null && entry.expiresAfterPeriods !== undefined && !(Number.isInteger(entry.expiresAfterPeriods) && entry.expiresAfterPeriods >= 0)) {
14
+ throw new Error('meter credits: expiresAfterPeriods is a whole number of periods (0 = never)');
15
+ }
16
+ }
17
+ const keyOf = (...parts) => parts.join('\u0000');
18
+ /** In-memory store. Single-process only: no lock is needed because nothing interleaves inside one call. */
19
+ export function createMemoryMeterStore() {
20
+ const periods = new Map();
21
+ const ledger = [];
22
+ const row = (subjectId, meterKey, period) => periods.get(keyOf(subjectId, meterKey, period));
23
+ const copy = (r) => ({ used: r.used, fromCredits: r.fromCredits, onDebt: r.onDebt, alerts: [...r.alerts] });
24
+ const earlier = (subjectId, meterKey, period) => [...periods.values()].filter((r) => r.subjectId === subjectId && r.meterKey === meterKey && r.period < period).sort((a, b) => (a.period < b.period ? -1 : 1));
25
+ return {
26
+ ledger,
27
+ async lockPeriod(subjectId, meterKey, period, included) {
28
+ const k = keyOf(subjectId, meterKey, period);
29
+ const existing = periods.get(k) ?? { subjectId, meterKey, period, included, used: 0, fromCredits: 0, onDebt: 0, alerts: [] };
30
+ existing.included = included;
31
+ periods.set(k, existing);
32
+ return copy(existing);
33
+ },
34
+ async readPeriod(subjectId, meterKey, period) {
35
+ const r = row(subjectId, meterKey, period);
36
+ return r ? copy(r) : null;
37
+ },
38
+ async history(subjectId, meterKey, period) {
39
+ return earlier(subjectId, meterKey, period).map((r) => ({ period: r.period, included: r.included, used: r.used, fromCredits: r.fromCredits, onDebt: r.onDebt }));
40
+ },
41
+ async drawnBefore(subjectId, meterKey, period) {
42
+ return earlier(subjectId, meterKey, period).reduce((n, r) => n + r.fromCredits + r.onDebt, 0);
43
+ },
44
+ async addUsage(subjectId, meterKey, period, split, alerts) {
45
+ const r = row(subjectId, meterKey, period);
46
+ if (!r)
47
+ throw new Error(`meter ${meterKey}: addUsage before lockPeriod (${subjectId}, ${period})`);
48
+ r.used += split.included + split.credits + split.debt;
49
+ r.fromCredits += split.credits;
50
+ r.onDebt += split.debt;
51
+ for (const a of alerts)
52
+ if (!r.alerts.includes(a))
53
+ r.alerts.push(a);
54
+ },
55
+ async releaseUsage(subjectId, meterKey, period, split) {
56
+ const r = row(subjectId, meterKey, period);
57
+ if (!r)
58
+ return;
59
+ r.used = Math.max(0, r.used - (split.included + split.credits + split.debt));
60
+ r.fromCredits = Math.max(0, r.fromCredits - split.credits);
61
+ r.onDebt = Math.max(0, r.onDebt - split.debt);
62
+ },
63
+ async lots(subjectId, meterKey) {
64
+ return ledger
65
+ .filter((e) => e.subjectId === subjectId && e.meterKey === meterKey)
66
+ .map((e) => ({ units: e.units, kind: e.kind, createdAt: e.createdAt, expiresAfterPeriods: e.expiresAfterPeriods ?? null }));
67
+ },
68
+ async addCredits(entry) {
69
+ assertCreditEntry(entry);
70
+ if (entry.idempotencyKey && ledger.some((e) => e.subjectId === entry.subjectId && e.meterKey === entry.meterKey && e.idempotencyKey === entry.idempotencyKey)) {
71
+ return { added: false };
72
+ }
73
+ ledger.push({ ...entry, createdAt: entry.at ?? new Date() });
74
+ return { added: true };
75
+ },
76
+ };
77
+ }
78
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/meters/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ -- @venturekit-pro/billing — metered allowances (`src/meters`).
2
+ --
3
+ -- `vk_billing_meter_usage`: one row per subject, meter and period. `used` is everything
4
+ -- consumed in the period; `from_credits` the part paid with credits; `on_debt` the part
5
+ -- consumed with nothing left (exempt consumptions only), owed and taken from the next
6
+ -- credits. `included` records the period's allowance as it stood at the latest
7
+ -- consumption. `alerts` are the alert keys already sent this period.
8
+ --
9
+ -- `vk_billing_credit_ledger`: every credit movement — a purchase, a grant, an automatic
10
+ -- top-up, a void — signed, each a lot dated by `created_at`. What the credits are worth in a
11
+ -- period is a replay of the lots against the periods' draws (`src/meters/rules.ts`
12
+ -- `openingCredits`), so a lot can expire: `expires_after_periods` is its life in periods
13
+ -- (1 = the period it was made in, 0 = never), NULL = the meter's policy. Nothing is ever
14
+ -- updated in place, so the balance can be explained entry by entry. An idempotency key
15
+ -- makes an entry once-only (an automatic top-up noticed by several concurrent consumptions).
16
+ --
17
+ -- No RLS policy is installed: `vk_install_tenant_guards()` needs the consumer's
18
+ -- application role. Call it yourself if `subject_id` is a tenant you scope by.
19
+
20
+ CREATE TABLE IF NOT EXISTS vk_billing_meter_usage (
21
+ subject_id VARCHAR(128) NOT NULL,
22
+ meter_key VARCHAR(128) NOT NULL,
23
+ period VARCHAR(16) NOT NULL,
24
+ included INTEGER NOT NULL DEFAULT 0 CHECK (included >= 0),
25
+ used INTEGER NOT NULL DEFAULT 0 CHECK (used >= 0),
26
+ from_credits INTEGER NOT NULL DEFAULT 0 CHECK (from_credits >= 0),
27
+ on_debt INTEGER NOT NULL DEFAULT 0 CHECK (on_debt >= 0),
28
+ alerts TEXT[] NOT NULL DEFAULT '{}',
29
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
30
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
31
+ PRIMARY KEY (subject_id, meter_key, period),
32
+ CONSTRAINT vk_billing_meter_usage_buckets CHECK (from_credits + on_debt <= used)
33
+ );
34
+
35
+ CREATE TABLE IF NOT EXISTS vk_billing_credit_ledger (
36
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
37
+ subject_id VARCHAR(128) NOT NULL,
38
+ meter_key VARCHAR(128) NOT NULL,
39
+ units INTEGER NOT NULL CHECK (units <> 0),
40
+ kind VARCHAR(20) NOT NULL CHECK (kind IN ('purchase', 'grant', 'auto_topup', 'refund', 'void', 'adjustment')),
41
+ reference TEXT,
42
+ idempotency_key VARCHAR(200),
43
+ expires_after_periods INTEGER CHECK (expires_after_periods >= 0),
44
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
45
+ );
46
+
47
+ CREATE INDEX IF NOT EXISTS idx_vk_billing_credit_ledger_subject ON vk_billing_credit_ledger (subject_id, meter_key, created_at);
48
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_vk_billing_credit_ledger_idempotency
49
+ ON vk_billing_credit_ledger (subject_id, meter_key, idempotency_key)
50
+ WHERE idempotency_key IS NOT NULL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venturekit-pro/billing",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
4
4
  "description": "Invoicing, plans, usage tracking, and feature gating for VentureKit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,7 +30,7 @@
30
30
  }
31
31
  },
32
32
  "dependencies": {
33
- "@venturekit/core": "0.0.41"
33
+ "@venturekit/core": "0.0.43"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^26.5.1",
@@ -0,0 +1,50 @@
1
+ -- @venturekit-pro/billing — metered allowances (`src/meters`).
2
+ --
3
+ -- `vk_billing_meter_usage`: one row per subject, meter and period. `used` is everything
4
+ -- consumed in the period; `from_credits` the part paid with credits; `on_debt` the part
5
+ -- consumed with nothing left (exempt consumptions only), owed and taken from the next
6
+ -- credits. `included` records the period's allowance as it stood at the latest
7
+ -- consumption. `alerts` are the alert keys already sent this period.
8
+ --
9
+ -- `vk_billing_credit_ledger`: every credit movement — a purchase, a grant, an automatic
10
+ -- top-up, a void — signed, each a lot dated by `created_at`. What the credits are worth in a
11
+ -- period is a replay of the lots against the periods' draws (`src/meters/rules.ts`
12
+ -- `openingCredits`), so a lot can expire: `expires_after_periods` is its life in periods
13
+ -- (1 = the period it was made in, 0 = never), NULL = the meter's policy. Nothing is ever
14
+ -- updated in place, so the balance can be explained entry by entry. An idempotency key
15
+ -- makes an entry once-only (an automatic top-up noticed by several concurrent consumptions).
16
+ --
17
+ -- No RLS policy is installed: `vk_install_tenant_guards()` needs the consumer's
18
+ -- application role. Call it yourself if `subject_id` is a tenant you scope by.
19
+
20
+ CREATE TABLE IF NOT EXISTS vk_billing_meter_usage (
21
+ subject_id VARCHAR(128) NOT NULL,
22
+ meter_key VARCHAR(128) NOT NULL,
23
+ period VARCHAR(16) NOT NULL,
24
+ included INTEGER NOT NULL DEFAULT 0 CHECK (included >= 0),
25
+ used INTEGER NOT NULL DEFAULT 0 CHECK (used >= 0),
26
+ from_credits INTEGER NOT NULL DEFAULT 0 CHECK (from_credits >= 0),
27
+ on_debt INTEGER NOT NULL DEFAULT 0 CHECK (on_debt >= 0),
28
+ alerts TEXT[] NOT NULL DEFAULT '{}',
29
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
30
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
31
+ PRIMARY KEY (subject_id, meter_key, period),
32
+ CONSTRAINT vk_billing_meter_usage_buckets CHECK (from_credits + on_debt <= used)
33
+ );
34
+
35
+ CREATE TABLE IF NOT EXISTS vk_billing_credit_ledger (
36
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
37
+ subject_id VARCHAR(128) NOT NULL,
38
+ meter_key VARCHAR(128) NOT NULL,
39
+ units INTEGER NOT NULL CHECK (units <> 0),
40
+ kind VARCHAR(20) NOT NULL CHECK (kind IN ('purchase', 'grant', 'auto_topup', 'refund', 'void', 'adjustment')),
41
+ reference TEXT,
42
+ idempotency_key VARCHAR(200),
43
+ expires_after_periods INTEGER CHECK (expires_after_periods >= 0),
44
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
45
+ );
46
+
47
+ CREATE INDEX IF NOT EXISTS idx_vk_billing_credit_ledger_subject ON vk_billing_credit_ledger (subject_id, meter_key, created_at);
48
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_vk_billing_credit_ledger_idempotency
49
+ ON vk_billing_credit_ledger (subject_id, meter_key, idempotency_key)
50
+ WHERE idempotency_key IS NOT NULL;