@substrat-run/engine-metering 0.0.0 → 0.1.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.
@@ -0,0 +1,224 @@
1
+ import { z } from 'zod';
2
+ import { type EntityRef } from '@substrat-run/contracts';
3
+ import { type ModuleRegistration, type OperationContext } from '@substrat-run/kernel';
4
+ export declare const PERM: {
5
+ read: string & z.$brand<"PermissionKey">;
6
+ record: string & z.$brand<"PermissionKey">;
7
+ configure: string & z.$brand<"PermissionKey">;
8
+ close: string & z.$brand<"PermissionKey">;
9
+ };
10
+ export declare const meteringManifest: {
11
+ id: string & z.$brand<"ModuleId">;
12
+ version: string;
13
+ kernelContract: string;
14
+ permissions: {
15
+ key: string & z.$brand<"PermissionKey">;
16
+ description: string;
17
+ }[];
18
+ events: {
19
+ emits: {
20
+ type: string;
21
+ schemaVersion: number;
22
+ }[];
23
+ consumes: {
24
+ type: string;
25
+ schemaVersion: number;
26
+ }[];
27
+ };
28
+ migrations: {
29
+ journalDir: string;
30
+ compatibleFrom: string;
31
+ };
32
+ attachmentTargets: {
33
+ entityType: string;
34
+ readPermission: string & z.$brand<"PermissionKey">;
35
+ writePermission?: (string & z.$brand<"PermissionKey">) | undefined;
36
+ }[];
37
+ entityRelations?: {
38
+ entityType: string;
39
+ parentType: string;
40
+ }[] | undefined;
41
+ guards?: {
42
+ before: string;
43
+ predicate: string;
44
+ config: Record<string, unknown>;
45
+ }[] | undefined;
46
+ schedules?: {
47
+ operation: string;
48
+ cadence: {
49
+ everyMinutes: number;
50
+ };
51
+ input?: Record<string, unknown> | undefined;
52
+ permissions: (string & z.$brand<"PermissionKey">)[];
53
+ }[] | undefined;
54
+ withdraws?: string[] | undefined;
55
+ entitlementKey: string;
56
+ envSpec?: {
57
+ key: string;
58
+ label?: string | undefined;
59
+ description: string;
60
+ placeholder?: string | undefined;
61
+ required: boolean;
62
+ secret: boolean;
63
+ default?: string | undefined;
64
+ group?: string | undefined;
65
+ }[] | undefined;
66
+ ownerGrants?: (string & z.$brand<"PermissionKey">)[] | undefined;
67
+ entitlements?: string[] | undefined;
68
+ provides?: string[] | undefined;
69
+ requires?: string[] | undefined;
70
+ api?: string | undefined;
71
+ searchables?: {
72
+ entityType: string;
73
+ fields: string[];
74
+ }[] | undefined;
75
+ ui?: {
76
+ routes?: {
77
+ path: string;
78
+ screen: string;
79
+ permission: string & z.$brand<"PermissionKey">;
80
+ }[] | undefined;
81
+ nav?: {
82
+ label: string;
83
+ icon?: string | undefined;
84
+ to: string;
85
+ permission: string & z.$brand<"PermissionKey">;
86
+ }[] | undefined;
87
+ entityViews?: {
88
+ entityType: string;
89
+ view: string;
90
+ }[] | undefined;
91
+ widgets?: {
92
+ slot: string;
93
+ component: string;
94
+ permission: string & z.$brand<"PermissionKey">;
95
+ }[] | undefined;
96
+ settingsPanels?: {
97
+ label: string;
98
+ component: string;
99
+ permission: string & z.$brand<"PermissionKey">;
100
+ }[] | undefined;
101
+ } | undefined;
102
+ };
103
+ export declare const meteringMigrations: {
104
+ version: string;
105
+ sql: string;
106
+ }[];
107
+ export type MeterKind = 'counter' | 'gauge';
108
+ export interface Meter {
109
+ key: string;
110
+ kind: MeterKind;
111
+ unit: string;
112
+ description: string | null;
113
+ active: boolean;
114
+ createdAt: string;
115
+ }
116
+ export interface UsageEntry {
117
+ id: string;
118
+ meterKey: string;
119
+ qty: string;
120
+ subject: EntityRef | null;
121
+ occurredAt: string;
122
+ dedupeKey: string;
123
+ note: string | null;
124
+ createdBy: string;
125
+ createdAt: string;
126
+ }
127
+ export interface MeteringPeriod {
128
+ id: string;
129
+ from: string;
130
+ to: string;
131
+ closedBy: string;
132
+ closedAt: string;
133
+ }
134
+ /** One meter's frozen aggregate for a closed period. Unpriced, by design (D-E). */
135
+ export interface PeriodLine {
136
+ meterKey: string;
137
+ kind: MeterKind;
138
+ unit: string;
139
+ qty: string;
140
+ entryCount: number;
141
+ }
142
+ export declare const configureMeterInput: z.ZodObject<{
143
+ key: z.ZodString;
144
+ kind: z.ZodEnum<{
145
+ counter: "counter";
146
+ gauge: "gauge";
147
+ }>;
148
+ unit: z.ZodString;
149
+ description: z.ZodOptional<z.ZodString>;
150
+ active: z.ZodOptional<z.ZodBoolean>;
151
+ }, z.core.$strip>;
152
+ export type ConfigureMeterInput = z.infer<typeof configureMeterInput>;
153
+ /**
154
+ * Register or update a meter. `kind` and `unit` are FROZEN after creation:
155
+ * changing a meter's unit mid-period corrupts every aggregate that spans the
156
+ * change — a new unit is a new meter key (D-B). `description`/`active` update.
157
+ */
158
+ export declare function configureMeter(ctx: OperationContext, rawInput: ConfigureMeterInput): Meter;
159
+ export declare function listMeters(ctx: OperationContext): Meter[];
160
+ export declare const recordUsageInput: z.ZodObject<{
161
+ meter: z.ZodString;
162
+ qty: z.ZodString;
163
+ subject: z.ZodOptional<z.ZodObject<{
164
+ entityType: z.ZodString;
165
+ entityId: z.ZodString;
166
+ }, z.core.$strip>>;
167
+ occurredAt: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
168
+ dedupeKey: z.ZodString;
169
+ note: z.ZodOptional<z.ZodString>;
170
+ }, z.core.$strip>;
171
+ export type RecordUsageInput = z.infer<typeof recordUsageInput>;
172
+ /**
173
+ * The idempotent ingest (D-C): a replay with the same (meter, dedupeKey) and
174
+ * the same qty returns the existing entry — no second row, no second event, no
175
+ * double bill. The same key with a DIFFERENT qty throws: that is an upstream
176
+ * bug, and swallowing it would hide exactly the defect the key exists to catch.
177
+ */
178
+ export declare function recordUsage(ctx: OperationContext, rawInput: RecordUsageInput): {
179
+ entry: UsageEntry;
180
+ deduped: boolean;
181
+ };
182
+ /**
183
+ * A window aggregate over half-open [from, to) — the same code path a close
184
+ * would freeze, so a preview and the eventual line can never disagree. Null
185
+ * means the meter has nothing to say for the window (see aggregateMeter).
186
+ */
187
+ export declare function usageTotal(ctx: OperationContext, input: {
188
+ meter: string;
189
+ from: string;
190
+ to: string;
191
+ }): {
192
+ qty: string;
193
+ entryCount: number;
194
+ } | null;
195
+ export declare function listEntries(ctx: OperationContext, input?: {
196
+ meter?: string;
197
+ subject?: EntityRef;
198
+ from?: string;
199
+ to?: string;
200
+ }): UsageEntry[];
201
+ export declare const closePeriodInput: z.ZodObject<{
202
+ from: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
203
+ to: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
204
+ }, z.core.$strip>;
205
+ export type ClosePeriodInput = z.infer<typeof closePeriodInput>;
206
+ /**
207
+ * Freeze [from, to): aggregate EVERY meter (inactive ones too — usage recorded
208
+ * while a meter lived still bills after deactivation), write immutable period +
209
+ * line rows, advance the close horizon, and emit ONE fat event carrying the
210
+ * unpriced lines — the vertical prices them and feeds invoicing (D-E), the
211
+ * same wiring as timesheet.period-closed. Closes are monotonic and
212
+ * non-overlapping; gaps are allowed (metering may start mid-life), rewinds are
213
+ * not (D-D).
214
+ */
215
+ export declare function closePeriod(ctx: OperationContext, rawInput: ClosePeriodInput): {
216
+ period: MeteringPeriod;
217
+ lines: PeriodLine[];
218
+ };
219
+ export declare function listPeriods(ctx: OperationContext): MeteringPeriod[];
220
+ export declare function periodLines(ctx: OperationContext, input: {
221
+ periodId: string;
222
+ }): PeriodLine[];
223
+ export declare const meteringModule: ModuleRegistration;
224
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,456 @@
1
+ import { z } from 'zod';
2
+ import { addDecimal, compareDecimal, entityRef, moduleManifest, permissionKey, } from '@substrat-run/contracts';
3
+ import { assertAllowed, ulid, } from '@substrat-run/kernel';
4
+ // ============================================================================
5
+ // The metering engine (docs/design/engine-metering.md, #646). Owns an
6
+ // APPEND-ONLY usage ledger over configured meters, idempotent ingest keyed by
7
+ // (meter, dedupe key), window aggregation (counters sum; gauges sample and
8
+ // carry forward), and an append-only period-close journal whose latest close is
9
+ // a hard horizon no new entry may land behind — a closed period's lines stay
10
+ // reproducible from its entries forever.
11
+ //
12
+ // It owns QUANTITIES, never prices: no currency, no rates, no plans (D-E) —
13
+ // pricing is vertical vocabulary, and the vertical feeds invoicing from the fat
14
+ // `metering.period-closed` event exactly like the timesheet hand-off. It is the
15
+ // BILLABLE plane; platform metering stays Analytics Engine (D-A).
16
+ // ============================================================================
17
+ export const PERM = {
18
+ read: permissionKey.parse('metering:read'),
19
+ record: permissionKey.parse('metering:record'),
20
+ configure: permissionKey.parse('metering:configure'),
21
+ close: permissionKey.parse('metering:close'),
22
+ };
23
+ export const meteringManifest = moduleManifest.parse({
24
+ id: '@substrat-run/engine-metering',
25
+ version: '0.0.1',
26
+ kernelContract: '^0.0.1',
27
+ permissions: [
28
+ { key: 'metering:read', description: 'Read meters, usage entries, totals and closed periods' },
29
+ { key: 'metering:record', description: 'Record usage entries against a meter' },
30
+ { key: 'metering:configure', description: 'Configure meters (register keys; kind/unit are frozen after creation)' },
31
+ { key: 'metering:close', description: 'Close a billing period, freezing its aggregates' },
32
+ ],
33
+ events: {
34
+ emits: [
35
+ { type: 'metering.meter-configured', schemaVersion: 1 },
36
+ { type: 'metering.usage-recorded', schemaVersion: 1 },
37
+ { type: 'metering.period-closed', schemaVersion: 1 },
38
+ ],
39
+ consumes: [],
40
+ },
41
+ migrations: { journalDir: './migrations', compatibleFrom: '0.0.1' },
42
+ attachmentTargets: [],
43
+ entityRelations: [],
44
+ entitlementKey: 'metering',
45
+ // No schedules: WHEN to close a period is billing policy — vertical
46
+ // vocabulary. The vertical calls closePeriod from its own schedule.
47
+ schedules: [],
48
+ });
49
+ export const meteringMigrations = [
50
+ {
51
+ version: '0001-init',
52
+ sql: `
53
+ CREATE TABLE metering_meters (
54
+ key TEXT PRIMARY KEY,
55
+ kind TEXT NOT NULL CHECK (kind IN ('counter','gauge')),
56
+ unit TEXT NOT NULL,
57
+ description TEXT,
58
+ active INTEGER NOT NULL DEFAULT 1,
59
+ created_at TEXT NOT NULL
60
+ );
61
+ CREATE TABLE metering_entries (
62
+ id TEXT PRIMARY KEY,
63
+ meter_key TEXT NOT NULL,
64
+ qty TEXT NOT NULL,
65
+ subject_type TEXT,
66
+ subject_id TEXT,
67
+ occurred_at TEXT NOT NULL,
68
+ dedupe_key TEXT NOT NULL,
69
+ note TEXT,
70
+ created_by TEXT NOT NULL,
71
+ created_at TEXT NOT NULL,
72
+ UNIQUE (meter_key, dedupe_key)
73
+ );
74
+ CREATE INDEX metering_entries_by_meter_time
75
+ ON metering_entries (meter_key, occurred_at);
76
+ CREATE TABLE metering_periods (
77
+ id TEXT PRIMARY KEY,
78
+ from_at TEXT NOT NULL,
79
+ to_at TEXT NOT NULL,
80
+ closed_by TEXT NOT NULL,
81
+ closed_at TEXT NOT NULL
82
+ );
83
+ CREATE TABLE metering_period_lines (
84
+ id TEXT PRIMARY KEY,
85
+ period_id TEXT NOT NULL,
86
+ meter_key TEXT NOT NULL,
87
+ kind TEXT NOT NULL,
88
+ unit TEXT NOT NULL,
89
+ qty TEXT NOT NULL,
90
+ entry_count INTEGER NOT NULL
91
+ );
92
+ `,
93
+ },
94
+ ];
95
+ // ---------------------------------------------------------------------------
96
+ // Schemas & shapes
97
+ // ---------------------------------------------------------------------------
98
+ /**
99
+ * Instants are UTC ISO-8601, normalized to millisecond precision so
100
+ * lexicographic order IS chronological order — mixed precision would break
101
+ * every string comparison below ('…T00:00:00Z' sorts AFTER '…T00:00:00.000Z').
102
+ */
103
+ const isoInstant = z
104
+ .string()
105
+ .regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/, 'must be a UTC ISO-8601 instant (…Z)')
106
+ .transform((s) => new Date(s).toISOString());
107
+ const nonNegDecimal = z.string().regex(/^\d+(\.\d{1,6})?$/, 'must be a non-negative decimal');
108
+ const signedDecimal = z.string().regex(/^-?\d+(\.\d{1,6})?$/, 'must be a decimal');
109
+ const toMeter = (r) => ({
110
+ key: r.key,
111
+ kind: r.kind,
112
+ unit: r.unit,
113
+ description: r.description,
114
+ active: r.active === 1,
115
+ createdAt: r.created_at,
116
+ });
117
+ const toEntry = (r) => ({
118
+ id: r.id,
119
+ meterKey: r.meter_key,
120
+ qty: r.qty,
121
+ subject: r.subject_type !== null && r.subject_id !== null
122
+ ? { entityType: r.subject_type, entityId: r.subject_id }
123
+ : null,
124
+ occurredAt: r.occurred_at,
125
+ dedupeKey: r.dedupe_key,
126
+ note: r.note,
127
+ createdBy: r.created_by,
128
+ createdAt: r.created_at,
129
+ });
130
+ const toPeriod = (r) => ({
131
+ id: r.id,
132
+ from: r.from_at,
133
+ to: r.to_at,
134
+ closedBy: r.closed_by,
135
+ closedAt: r.closed_at,
136
+ });
137
+ const toLine = (r) => ({
138
+ meterKey: r.meter_key,
139
+ kind: r.kind,
140
+ unit: r.unit,
141
+ qty: r.qty,
142
+ entryCount: r.entry_count,
143
+ });
144
+ function getMeterRow(ctx, key) {
145
+ const row = ctx.sql.query('SELECT * FROM metering_meters WHERE key = ?', [key])[0];
146
+ if (!row)
147
+ throw new Error(`meter not found: ${key}`);
148
+ return row;
149
+ }
150
+ /** The latest closed `to` — nothing may be recorded behind it (D-D). */
151
+ function closeHorizon(ctx) {
152
+ return (ctx.sql.query('SELECT MAX(to_at) AS horizon FROM metering_periods')[0]?.horizon ?? null);
153
+ }
154
+ /**
155
+ * One meter's aggregate over half-open [from, to) — the SINGLE code path shared
156
+ * by `usageTotal` and `closePeriod`, so a preview can never disagree with the
157
+ * frozen line. Returns null when the meter has nothing to say for the window:
158
+ * a counter with no entries (a zero sum bills nothing), or a gauge never
159
+ * sampled at all. A gauge with no in-window samples carries the latest earlier
160
+ * sample forward with entryCount 0 — a level persists between observations.
161
+ */
162
+ function aggregateMeter(ctx, meter, from, to) {
163
+ const rows = ctx.sql.query(`SELECT qty FROM metering_entries
164
+ WHERE meter_key = ? AND occurred_at >= ? AND occurred_at < ?
165
+ ORDER BY occurred_at, id`, [meter.key, from, to]);
166
+ if (meter.kind === 'counter') {
167
+ if (rows.length === 0)
168
+ return null;
169
+ return { qty: rows.reduce((sum, r) => addDecimal(sum, r.qty), '0'), entryCount: rows.length };
170
+ }
171
+ if (rows.length > 0) {
172
+ const max = rows.reduce((m, r) => (compareDecimal(r.qty, m) > 0 ? r.qty : m), rows[0].qty);
173
+ return { qty: max, entryCount: rows.length };
174
+ }
175
+ const carried = ctx.sql.query(`SELECT qty FROM metering_entries
176
+ WHERE meter_key = ? AND occurred_at < ?
177
+ ORDER BY occurred_at DESC, id DESC LIMIT 1`, [meter.key, from])[0];
178
+ return carried ? { qty: carried.qty, entryCount: 0 } : null;
179
+ }
180
+ // ---------------------------------------------------------------------------
181
+ // In-scope functions (K-16) — composable from vertical operations, same
182
+ // transaction. The registered operations below are their default bindings.
183
+ // The CALLER is responsible for the permission check.
184
+ // ---------------------------------------------------------------------------
185
+ export const configureMeterInput = z.object({
186
+ key: z.string().min(1),
187
+ kind: z.enum(['counter', 'gauge']),
188
+ unit: z.string().min(1),
189
+ description: z.string().optional(),
190
+ active: z.boolean().optional(),
191
+ });
192
+ /**
193
+ * Register or update a meter. `kind` and `unit` are FROZEN after creation:
194
+ * changing a meter's unit mid-period corrupts every aggregate that spans the
195
+ * change — a new unit is a new meter key (D-B). `description`/`active` update.
196
+ */
197
+ export function configureMeter(ctx, rawInput) {
198
+ const input = configureMeterInput.parse(rawInput);
199
+ const existing = ctx.sql.query('SELECT * FROM metering_meters WHERE key = ?', [
200
+ input.key,
201
+ ])[0];
202
+ if (existing) {
203
+ if (existing.kind !== input.kind || existing.unit !== input.unit) {
204
+ throw new Error(`meter '${input.key}' is ${existing.kind}/${existing.unit} — kind and unit are frozen after creation; a new unit is a new meter key`);
205
+ }
206
+ ctx.sql.exec(`UPDATE metering_meters SET description = COALESCE(?, description), active = COALESCE(?, active) WHERE key = ?`, [
207
+ input.description ?? null,
208
+ input.active === undefined ? null : input.active ? 1 : 0,
209
+ input.key,
210
+ ]);
211
+ }
212
+ else {
213
+ ctx.sql.exec(`INSERT INTO metering_meters (key, kind, unit, description, active, created_at)
214
+ VALUES (?, ?, ?, ?, ?, ?)`, [
215
+ input.key,
216
+ input.kind,
217
+ input.unit,
218
+ input.description ?? null,
219
+ input.active === false ? 0 : 1,
220
+ new Date().toISOString(),
221
+ ]);
222
+ }
223
+ const row = getMeterRow(ctx, input.key);
224
+ ctx.emit({
225
+ type: 'metering.meter-configured',
226
+ schemaVersion: 1,
227
+ entity: { entityType: 'metering-meter', entityId: row.key },
228
+ piiClass: 'none',
229
+ payload: { key: row.key, kind: row.kind, unit: row.unit, active: row.active === 1 },
230
+ });
231
+ return toMeter(row);
232
+ }
233
+ export function listMeters(ctx) {
234
+ return ctx.sql.query('SELECT * FROM metering_meters ORDER BY key').map(toMeter);
235
+ }
236
+ export const recordUsageInput = z.object({
237
+ meter: z.string().min(1),
238
+ /** Counters take signed deltas (a correction is a compensating entry, never
239
+ * an edit); gauges take non-negative level samples — enforced per kind. */
240
+ qty: signedDecimal,
241
+ subject: entityRef.optional(),
242
+ /** Defaults to now. Must not fall behind the close horizon (D-D). */
243
+ occurredAt: isoInstant.optional(),
244
+ /** Caller-supplied idempotency key, unique per meter (D-C) — e.g. a turn id. */
245
+ dedupeKey: z.string().min(1),
246
+ note: z.string().optional(),
247
+ });
248
+ /**
249
+ * The idempotent ingest (D-C): a replay with the same (meter, dedupeKey) and
250
+ * the same qty returns the existing entry — no second row, no second event, no
251
+ * double bill. The same key with a DIFFERENT qty throws: that is an upstream
252
+ * bug, and swallowing it would hide exactly the defect the key exists to catch.
253
+ */
254
+ export function recordUsage(ctx, rawInput) {
255
+ const input = recordUsageInput.parse(rawInput);
256
+ const meter = getMeterRow(ctx, input.meter);
257
+ if (meter.active !== 1)
258
+ throw new Error(`meter '${meter.key}' is inactive`);
259
+ if (meter.kind === 'gauge')
260
+ nonNegDecimal.parse(input.qty);
261
+ const existing = ctx.sql.query('SELECT * FROM metering_entries WHERE meter_key = ? AND dedupe_key = ?', [meter.key, input.dedupeKey])[0];
262
+ if (existing) {
263
+ if (compareDecimal(existing.qty, input.qty) !== 0) {
264
+ throw new Error(`dedupe key '${input.dedupeKey}' on meter '${meter.key}' was recorded with qty ${existing.qty}, now ${input.qty} — a dedupe key must name one observation`);
265
+ }
266
+ return { entry: toEntry(existing), deduped: true };
267
+ }
268
+ const occurredAt = input.occurredAt ?? new Date().toISOString();
269
+ const horizon = closeHorizon(ctx);
270
+ if (horizon !== null && occurredAt < horizon) {
271
+ throw new Error(`occurred_at ${occurredAt} is behind the close horizon ${horizon} — the period covering it is closed; record late usage at observation time`);
272
+ }
273
+ const id = ulid();
274
+ ctx.sql.exec(`INSERT INTO metering_entries
275
+ (id, meter_key, qty, subject_type, subject_id, occurred_at, dedupe_key, note, created_by, created_at)
276
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
277
+ id,
278
+ meter.key,
279
+ input.qty,
280
+ input.subject?.entityType ?? null,
281
+ input.subject?.entityId ?? null,
282
+ occurredAt,
283
+ input.dedupeKey,
284
+ input.note ?? null,
285
+ ctx.principal,
286
+ new Date().toISOString(),
287
+ ]);
288
+ const entry = toEntry(ctx.sql.query('SELECT * FROM metering_entries WHERE id = ?', [id])[0]);
289
+ ctx.emit({
290
+ type: 'metering.usage-recorded',
291
+ schemaVersion: 1,
292
+ entity: { entityType: 'metering-entry', entityId: id },
293
+ piiClass: 'none',
294
+ payload: {
295
+ entryId: id,
296
+ meterKey: meter.key,
297
+ kind: meter.kind,
298
+ unit: meter.unit,
299
+ qty: input.qty,
300
+ subject: input.subject ?? null,
301
+ occurredAt,
302
+ dedupeKey: input.dedupeKey,
303
+ },
304
+ });
305
+ return { entry, deduped: false };
306
+ }
307
+ /**
308
+ * A window aggregate over half-open [from, to) — the same code path a close
309
+ * would freeze, so a preview and the eventual line can never disagree. Null
310
+ * means the meter has nothing to say for the window (see aggregateMeter).
311
+ */
312
+ export function usageTotal(ctx, input) {
313
+ const from = isoInstant.parse(input.from);
314
+ const to = isoInstant.parse(input.to);
315
+ if (to <= from)
316
+ throw new Error(`to ${to} must be after from ${from}`);
317
+ return aggregateMeter(ctx, getMeterRow(ctx, input.meter), from, to);
318
+ }
319
+ export function listEntries(ctx, input) {
320
+ const where = [];
321
+ const params = [];
322
+ if (input?.meter) {
323
+ where.push('meter_key = ?');
324
+ params.push(input.meter);
325
+ }
326
+ if (input?.subject) {
327
+ where.push('subject_type = ? AND subject_id = ?');
328
+ params.push(input.subject.entityType, input.subject.entityId);
329
+ }
330
+ if (input?.from) {
331
+ where.push('occurred_at >= ?');
332
+ params.push(isoInstant.parse(input.from));
333
+ }
334
+ if (input?.to) {
335
+ where.push('occurred_at < ?');
336
+ params.push(isoInstant.parse(input.to));
337
+ }
338
+ const sql = `SELECT * FROM metering_entries${where.length ? ` WHERE ${where.join(' AND ')}` : ''} ORDER BY occurred_at, id`;
339
+ return ctx.sql.query(sql, params).map(toEntry);
340
+ }
341
+ export const closePeriodInput = z.object({
342
+ from: isoInstant,
343
+ to: isoInstant,
344
+ });
345
+ /**
346
+ * Freeze [from, to): aggregate EVERY meter (inactive ones too — usage recorded
347
+ * while a meter lived still bills after deactivation), write immutable period +
348
+ * line rows, advance the close horizon, and emit ONE fat event carrying the
349
+ * unpriced lines — the vertical prices them and feeds invoicing (D-E), the
350
+ * same wiring as timesheet.period-closed. Closes are monotonic and
351
+ * non-overlapping; gaps are allowed (metering may start mid-life), rewinds are
352
+ * not (D-D).
353
+ */
354
+ export function closePeriod(ctx, rawInput) {
355
+ const input = closePeriodInput.parse(rawInput);
356
+ if (input.to <= input.from)
357
+ throw new Error(`to ${input.to} must be after from ${input.from}`);
358
+ const horizon = closeHorizon(ctx);
359
+ if (horizon !== null && input.from < horizon) {
360
+ throw new Error(`period from ${input.from} overlaps the close horizon ${horizon} — closes are monotonic and non-overlapping`);
361
+ }
362
+ const id = ulid();
363
+ const now = new Date().toISOString();
364
+ ctx.sql.exec(`INSERT INTO metering_periods (id, from_at, to_at, closed_by, closed_at) VALUES (?, ?, ?, ?, ?)`, [id, input.from, input.to, ctx.principal, now]);
365
+ const lines = [];
366
+ for (const meter of ctx.sql.query('SELECT * FROM metering_meters ORDER BY key')) {
367
+ const agg = aggregateMeter(ctx, meter, input.from, input.to);
368
+ if (!agg)
369
+ continue;
370
+ ctx.sql.exec(`INSERT INTO metering_period_lines (id, period_id, meter_key, kind, unit, qty, entry_count)
371
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [ulid(), id, meter.key, meter.kind, meter.unit, agg.qty, agg.entryCount]);
372
+ lines.push({
373
+ meterKey: meter.key,
374
+ kind: meter.kind,
375
+ unit: meter.unit,
376
+ qty: agg.qty,
377
+ entryCount: agg.entryCount,
378
+ });
379
+ }
380
+ ctx.emit({
381
+ type: 'metering.period-closed',
382
+ schemaVersion: 1,
383
+ entity: { entityType: 'metering-period', entityId: id },
384
+ piiClass: 'none',
385
+ payload: { periodId: id, from: input.from, to: input.to, lines },
386
+ });
387
+ return {
388
+ period: { id, from: input.from, to: input.to, closedBy: String(ctx.principal), closedAt: now },
389
+ lines,
390
+ };
391
+ }
392
+ export function listPeriods(ctx) {
393
+ return ctx.sql
394
+ .query('SELECT * FROM metering_periods ORDER BY from_at, id')
395
+ .map(toPeriod);
396
+ }
397
+ export function periodLines(ctx, input) {
398
+ const period = ctx.sql.query('SELECT * FROM metering_periods WHERE id = ?', [
399
+ input.periodId,
400
+ ])[0];
401
+ if (!period)
402
+ throw new Error(`metering period not found: ${input.periodId}`);
403
+ return ctx.sql
404
+ .query('SELECT * FROM metering_period_lines WHERE period_id = ? ORDER BY meter_key', [input.periodId])
405
+ .map(toLine);
406
+ }
407
+ // ---------------------------------------------------------------------------
408
+ // Default operation bindings — each starts with the permission check.
409
+ // ---------------------------------------------------------------------------
410
+ const configureMeterOp = async (ctx, input) => {
411
+ assertAllowed(await ctx.check(PERM.configure));
412
+ return configureMeter(ctx, input);
413
+ };
414
+ const listMetersOp = async (ctx) => {
415
+ assertAllowed(await ctx.check(PERM.read));
416
+ return listMeters(ctx);
417
+ };
418
+ const recordOp = async (ctx, input) => {
419
+ assertAllowed(await ctx.check(PERM.record));
420
+ return recordUsage(ctx, input);
421
+ };
422
+ const totalOp = async (ctx, input) => {
423
+ assertAllowed(await ctx.check(PERM.read));
424
+ return usageTotal(ctx, input);
425
+ };
426
+ const listEntriesOp = async (ctx, input) => {
427
+ assertAllowed(await ctx.check(PERM.read));
428
+ return listEntries(ctx, input);
429
+ };
430
+ const closePeriodOp = async (ctx, input) => {
431
+ assertAllowed(await ctx.check(PERM.close));
432
+ return closePeriod(ctx, input);
433
+ };
434
+ const listPeriodsOp = async (ctx) => {
435
+ assertAllowed(await ctx.check(PERM.read));
436
+ return listPeriods(ctx);
437
+ };
438
+ const periodLinesOp = async (ctx, input) => {
439
+ assertAllowed(await ctx.check(PERM.read));
440
+ return periodLines(ctx, input);
441
+ };
442
+ export const meteringModule = {
443
+ manifest: meteringManifest,
444
+ migrations: meteringMigrations,
445
+ operations: {
446
+ 'metering/configure-meter': configureMeterOp,
447
+ 'metering/list-meters': listMetersOp,
448
+ 'metering/record': recordOp,
449
+ 'metering/total': totalOp,
450
+ 'metering/list-entries': listEntriesOp,
451
+ 'metering/close-period': closePeriodOp,
452
+ 'metering/list-periods': listPeriodsOp,
453
+ 'metering/period-lines': periodLinesOp,
454
+ },
455
+ };
456
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@substrat-run/engine-metering",
3
- "version": "0.0.0",
3
+ "version": "0.1.1",
4
4
  "description": "Substrat engine: billable usage — an append-only, idempotent meter ledger (counters and gauges) whose closed periods are frozen billing evidence",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -24,19 +24,19 @@
24
24
  "publishConfig": {
25
25
  "access": "public"
26
26
  },
27
- "scripts": {
28
- "build": "tsc -p tsconfig.json",
29
- "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
30
- "test": "vitest run"
31
- },
32
27
  "dependencies": {
33
- "@substrat-run/contracts": "workspace:^",
34
- "@substrat-run/kernel": "workspace:^",
35
- "zod": "^4.4.3"
28
+ "zod": "^4.4.3",
29
+ "@substrat-run/kernel": "^0.66.0",
30
+ "@substrat-run/contracts": "^0.66.0"
36
31
  },
37
32
  "devDependencies": {
38
33
  "typescript": "^7.0.0",
39
- "@substrat-run/engine-test-kit": "workspace:^",
40
- "vitest": "^3.0.0"
34
+ "vitest": "^3.0.0",
35
+ "@substrat-run/engine-test-kit": "^0.0.64"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
40
+ "test": "vitest run"
41
41
  }
42
- }
42
+ }