@noodleseed/one 0.52.1 → 0.54.0
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/node_modules/@noodle-borg/service/dist/billing/active-commercial-entitlement.d.ts +25 -0
- package/node_modules/@noodle-borg/service/dist/billing/active-commercial-entitlement.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/active-commercial-entitlement.js +94 -0
- package/node_modules/@noodle-borg/service/dist/billing/active-commercial-entitlement.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/commercial-entitlements.d.ts +8 -0
- package/node_modules/@noodle-borg/service/dist/billing/commercial-entitlements.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/commercial-entitlements.js +49 -0
- package/node_modules/@noodle-borg/service/dist/billing/commercial-entitlements.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/in-memory.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/in-memory.js +3 -2
- package/node_modules/@noodle-borg/service/dist/billing/in-memory.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/integration.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/integration.js +0 -3
- package/node_modules/@noodle-borg/service/dist/billing/integration.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/model.d.ts +16 -3
- package/node_modules/@noodle-borg/service/dist/billing/model.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/model.js +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/model.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/paid-plan-catalog.d.ts +26 -0
- package/node_modules/@noodle-borg/service/dist/billing/paid-plan-catalog.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/paid-plan-catalog.js +94 -0
- package/node_modules/@noodle-borg/service/dist/billing/paid-plan-catalog.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-authoritative-authority.d.ts +4 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-authoritative-authority.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-authoritative-authority.js +21 -7
- package/node_modules/@noodle-borg/service/dist/billing/postgres-authoritative-authority.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-commercial-enforcement-counters.js +4 -5
- package/node_modules/@noodle-borg/service/dist/billing/postgres-commercial-enforcement-counters.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-commercial-enforcement-projection.d.ts +0 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-commercial-enforcement-projection.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-commercial-enforcement-projection.js +92 -27
- package/node_modules/@noodle-borg/service/dist/billing/postgres-commercial-enforcement-projection.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-meter-admission.d.ts +0 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-meter-admission.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-meter-admission.js +72 -27
- package/node_modules/@noodle-borg/service/dist/billing/postgres-meter-admission.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-production-app-admission.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-production-app-admission.js +56 -26
- package/node_modules/@noodle-borg/service/dist/billing/postgres-production-app-admission.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-records.d.ts +4 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-records.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-records.js +38 -9
- package/node_modules/@noodle-borg/service/dist/billing/postgres-records.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-schema.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-schema.js +28 -21
- package/node_modules/@noodle-borg/service/dist/billing/postgres-schema.js.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-authority.d.ts +5 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-authority.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-authority.js +97 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-authority.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-persistence.d.ts +50 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-persistence.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-persistence.js +221 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-persistence.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-support.d.ts +8 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-support.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-support.js +42 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-support.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation.d.ts +15 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation.js +123 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-schema.d.ts +4 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-schema.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-schema.js +419 -0
- package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-schema.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/subscription-model.d.ts +40 -0
- package/node_modules/@noodle-borg/service/dist/billing/subscription-model.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/subscription-model.js +388 -0
- package/node_modules/@noodle-borg/service/dist/billing/subscription-model.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/subscription-reconciliation.d.ts +45 -0
- package/node_modules/@noodle-borg/service/dist/billing/subscription-reconciliation.d.ts.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing/subscription-reconciliation.js +165 -0
- package/node_modules/@noodle-borg/service/dist/billing/subscription-reconciliation.js.map +1 -0
- package/node_modules/@noodle-borg/service/dist/billing-public.d.ts +6 -1
- package/node_modules/@noodle-borg/service/dist/billing-public.d.ts.map +1 -1
- package/node_modules/@noodle-borg/service/dist/billing-public.js +5 -0
- package/node_modules/@noodle-borg/service/dist/billing-public.js.map +1 -1
- package/package.json +1 -1
package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-subscription-reconciliation.d.ts","sourceRoot":"","sources":["../../src/billing/postgres-subscription-reconciliation.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAc,MAAM,IAAI,CAAC;AA6B3C,OAAO,EACL,KAAK,wCAAwC,EAC7C,KAAK,uCAAuC,EAC5C,KAAK,sCAAsC,EAM5C,MAAM,kCAAkC,CAAC;AAK1C,MAAM,WAAW,qDAAqD;IACpE,QAAQ,CAAC,0BAA0B,EAAE,MAAM,CAAC;IAC5C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,mFAAmF;AACnF,qBAAa,8CACX,YAAW,sCAAsC;;gBAQrC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,qDAAqD;IAchF,yBAAyB,CAC7B,UAAU,EAAE,wCAAwC,GACnD,OAAO,CAAC,uCAAuC,CAAC;CAiHpD"}
|
package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { insertAuditEvent } from '@noodle-borg/module-audit';
|
|
3
|
+
import { readBillingMeterTransactionTime } from './postgres-meter-clock.js';
|
|
4
|
+
import { BILLING_METER_EPOCH_LIFECYCLE_LOCK_KEY } from './postgres-meter-epochs.js';
|
|
5
|
+
import { requireActiveBillingAccount, requireEnforcedBillingAccount, requirePaidAuthority, } from './postgres-subscription-reconciliation-authority.js';
|
|
6
|
+
import { compareAndSwapPointers, findOperation, insertOperation, insertPaidSnapshot, insertRevision, readActiveSnapshotForUpdate, readCurrentSubscriptionForUpdate, replayOperation, requireNoLaterSnapshot, validateCurrentSubscription, validateSubscriptionHistory, } from './postgres-subscription-reconciliation-persistence.js';
|
|
7
|
+
import { conflict, keyGeneration, rollbackQuietly, timeout, translateTransactionError, } from './postgres-subscription-reconciliation-support.js';
|
|
8
|
+
import { createPaidEntitlementSnapshot } from './subscription-model.js';
|
|
9
|
+
import { billingSubscriptionReconciliationContractError, billingSubscriptionReconciliationRequestFingerprint, billingSubscriptionReconciliationSuccessAudit, normalizeBillingSubscriptionReconciliationRequest, requireCurrentlyEffectivePaidSubscription, } from './subscription-reconciliation.js';
|
|
10
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 5_000;
|
|
11
|
+
const DEFAULT_STATEMENT_TIMEOUT_MS = 10_000;
|
|
12
|
+
/** Atomic provider-neutral paid history and entitlement-pointer reconciliation. */
|
|
13
|
+
export class PostgresBillingSubscriptionReconciliationStore {
|
|
14
|
+
#pool;
|
|
15
|
+
#keyGeneration;
|
|
16
|
+
#lockTimeoutMs;
|
|
17
|
+
#statementTimeoutMs;
|
|
18
|
+
#id;
|
|
19
|
+
constructor(pool, options) {
|
|
20
|
+
this.#pool = pool;
|
|
21
|
+
this.#keyGeneration = keyGeneration(options.authoritativeKeyGeneration);
|
|
22
|
+
this.#lockTimeoutMs = timeout('billing subscription reconciliation lock timeout', options.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS);
|
|
23
|
+
this.#statementTimeoutMs = timeout('billing subscription reconciliation statement timeout', options.statementTimeoutMs ?? DEFAULT_STATEMENT_TIMEOUT_MS);
|
|
24
|
+
this.#id = options.id ?? randomUUID;
|
|
25
|
+
}
|
|
26
|
+
async reconcilePaidSubscription(rawRequest) {
|
|
27
|
+
const request = normalizeRequest(rawRequest);
|
|
28
|
+
const fingerprint = billingSubscriptionReconciliationRequestFingerprint(request);
|
|
29
|
+
const idempotencyHash = createHash('sha256').update(request.idempotencyKey).digest('hex');
|
|
30
|
+
const client = await this.#pool.connect();
|
|
31
|
+
let transactionOpen = false;
|
|
32
|
+
try {
|
|
33
|
+
await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
|
|
34
|
+
transactionOpen = true;
|
|
35
|
+
await client.query(`SELECT set_config('lock_timeout', $1, true),
|
|
36
|
+
set_config('statement_timeout', $2, true)`, [`${this.#lockTimeoutMs}ms`, `${this.#statementTimeoutMs}ms`]);
|
|
37
|
+
await client.query('SELECT pg_advisory_xact_lock_shared(hashtextextended($1, 0))', [
|
|
38
|
+
BILLING_METER_EPOCH_LIFECYCLE_LOCK_KEY,
|
|
39
|
+
]);
|
|
40
|
+
await advisoryLock(client, `billing-subscription-reconciliation-key:${idempotencyHash}`);
|
|
41
|
+
const operation = await findOperation(client, idempotencyHash);
|
|
42
|
+
if (operation !== undefined) {
|
|
43
|
+
const replay = replayOperation(operation, fingerprint);
|
|
44
|
+
await client.query('COMMIT');
|
|
45
|
+
transactionOpen = false;
|
|
46
|
+
return replay;
|
|
47
|
+
}
|
|
48
|
+
await advisoryLock(client, `billing-subscription-reconciliation-account:${request.billingAccountId}`);
|
|
49
|
+
await advisoryLock(client, `billing-subscription-reconciliation-subscription:${request.revision.subscriptionId}`);
|
|
50
|
+
await requirePaidAuthority(client, this.#keyGeneration);
|
|
51
|
+
await requireActiveBillingAccount(client, request.billingAccountId);
|
|
52
|
+
await requireEnforcedBillingAccount(client, request.billingAccountId);
|
|
53
|
+
const previousSnapshot = await readActiveSnapshotForUpdate(client, request.billingAccountId);
|
|
54
|
+
if (previousSnapshot === undefined ||
|
|
55
|
+
previousSnapshot.version !== request.expectedActiveSnapshotVersion) {
|
|
56
|
+
throw conflict('active_snapshot_conflict', 'the active entitlement snapshot changed; retry from the current billing account state');
|
|
57
|
+
}
|
|
58
|
+
await requireNoLaterSnapshot(client, request.billingAccountId, previousSnapshot.version);
|
|
59
|
+
const currentSubscription = await readCurrentSubscriptionForUpdate(client, request.billingAccountId);
|
|
60
|
+
validateCurrentSubscription(currentSubscription, request.expectedCurrentBaseSubscription, previousSnapshot);
|
|
61
|
+
await validateSubscriptionHistory(client, request, currentSubscription);
|
|
62
|
+
const reconciledAt = await readBillingMeterTransactionTime(client);
|
|
63
|
+
requireCurrentlyEffectivePaidSubscription(request.revision, reconciledAt);
|
|
64
|
+
const paidSnapshot = createPaidEntitlementSnapshot({
|
|
65
|
+
id: request.snapshotId,
|
|
66
|
+
version: previousSnapshot.version + 1,
|
|
67
|
+
sourceSubscription: request.revision,
|
|
68
|
+
previousSnapshot,
|
|
69
|
+
});
|
|
70
|
+
if (currentSubscription === undefined) {
|
|
71
|
+
await client.query(`INSERT INTO billing_base_subscriptions (id, billing_account_id, created_at)
|
|
72
|
+
VALUES ($1, $2, $3)`, [request.revision.subscriptionId, request.billingAccountId, request.revision.createdAt]);
|
|
73
|
+
}
|
|
74
|
+
await insertRevision(client, request.revision);
|
|
75
|
+
await insertPaidSnapshot(client, paidSnapshot);
|
|
76
|
+
await compareAndSwapPointers(client, request, previousSnapshot.id, currentSubscription);
|
|
77
|
+
const result = {
|
|
78
|
+
schemaVersion: 1,
|
|
79
|
+
billingAccountId: request.billingAccountId,
|
|
80
|
+
subscriptionId: request.revision.subscriptionId,
|
|
81
|
+
subscriptionRevisionVersion: request.revision.version,
|
|
82
|
+
snapshotId: paidSnapshot.id,
|
|
83
|
+
snapshotVersion: paidSnapshot.version,
|
|
84
|
+
planVersionId: paidSnapshot.planVersionId,
|
|
85
|
+
reconciledAt,
|
|
86
|
+
replayed: false,
|
|
87
|
+
};
|
|
88
|
+
await insertOperation(client, {
|
|
89
|
+
operationId: `bsro_${this.#id()}`,
|
|
90
|
+
idempotencyHash,
|
|
91
|
+
fingerprint,
|
|
92
|
+
result,
|
|
93
|
+
createdAt: reconciledAt,
|
|
94
|
+
});
|
|
95
|
+
await insertAuditEvent(client, billingSubscriptionReconciliationSuccessAudit(result), {
|
|
96
|
+
now: () => new Date(reconciledAt),
|
|
97
|
+
});
|
|
98
|
+
await client.query('COMMIT');
|
|
99
|
+
transactionOpen = false;
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (transactionOpen)
|
|
104
|
+
await rollbackQuietly(client);
|
|
105
|
+
throw translateTransactionError(error);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
client.release();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function normalizeRequest(rawRequest) {
|
|
113
|
+
try {
|
|
114
|
+
return normalizeBillingSubscriptionReconciliationRequest(rawRequest);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw billingSubscriptionReconciliationContractError();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function advisoryLock(client, identity) {
|
|
121
|
+
await client.query('SELECT pg_advisory_xact_lock(hashtextextended($1, 0))', [identity]);
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=postgres-subscription-reconciliation.js.map
|
package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-subscription-reconciliation.js","sourceRoot":"","sources":["../../src/billing/postgres-subscription-reconciliation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Pool } from 'pg';
|
|
2
|
+
/** Additive, dormant paid-subscription history. This schema never advances an active snapshot. */
|
|
3
|
+
export declare function ensureBillingSubscriptionSchema(pool: Pool): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=postgres-subscription-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-subscription-schema.d.ts","sourceRoot":"","sources":["../../src/billing/postgres-subscription-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAc,MAAM,IAAI,CAAC;AAE3C,kGAAkG;AAClG,wBAAsB,+BAA+B,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB/E"}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/** Additive, dormant paid-subscription history. This schema never advances an active snapshot. */
|
|
2
|
+
export async function ensureBillingSubscriptionSchema(pool) {
|
|
3
|
+
const client = await pool.connect();
|
|
4
|
+
try {
|
|
5
|
+
await client.query('BEGIN');
|
|
6
|
+
try {
|
|
7
|
+
// Service replicas may all start against the same database. Serialize this migration so the
|
|
8
|
+
// catalog checks and trigger installation below cannot race one another.
|
|
9
|
+
await client.query(`SELECT pg_advisory_xact_lock(hashtext('noodle.billing.subscription-schema.v1'))`);
|
|
10
|
+
await ensureBillingSubscriptionSchemaTx(client);
|
|
11
|
+
await client.query('COMMIT');
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
await client.query('ROLLBACK');
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
client.release();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function ensureBillingSubscriptionSchemaTx(client) {
|
|
23
|
+
await client.query(`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS billing_base_subscriptions (
|
|
25
|
+
id text PRIMARY KEY,
|
|
26
|
+
billing_account_id text NOT NULL REFERENCES billing_accounts(id) ON DELETE RESTRICT,
|
|
27
|
+
created_at timestamptz NOT NULL,
|
|
28
|
+
UNIQUE (billing_account_id, id)
|
|
29
|
+
)
|
|
30
|
+
`);
|
|
31
|
+
await client.query(`
|
|
32
|
+
CREATE TABLE IF NOT EXISTS billing_base_subscription_revisions (
|
|
33
|
+
schema_version integer NOT NULL CHECK (schema_version = 1),
|
|
34
|
+
subscription_id text NOT NULL,
|
|
35
|
+
billing_account_id text NOT NULL,
|
|
36
|
+
version integer NOT NULL CHECK (version > 0),
|
|
37
|
+
plan_version_id text NOT NULL
|
|
38
|
+
CHECK (plan_version_id IN ('pro@1', 'scale@1'))
|
|
39
|
+
REFERENCES billing_plan_versions(id) ON DELETE RESTRICT,
|
|
40
|
+
state text NOT NULL CHECK (state IN ('active', 'payment_grace', 'ended')),
|
|
41
|
+
billing_interval text NOT NULL CHECK (billing_interval IN ('month', 'year')),
|
|
42
|
+
currency text NOT NULL CHECK (currency = 'usd'),
|
|
43
|
+
base_amount_minor bigint NOT NULL
|
|
44
|
+
CHECK (base_amount_minor >= 0 AND base_amount_minor <= 9007199254740991),
|
|
45
|
+
CONSTRAINT billing_base_subscription_revisions_price_check CHECK (
|
|
46
|
+
(plan_version_id = 'pro@1' AND billing_interval = 'month' AND base_amount_minor = 3000) OR
|
|
47
|
+
(plan_version_id = 'pro@1' AND billing_interval = 'year' AND base_amount_minor = 30000) OR
|
|
48
|
+
(plan_version_id = 'scale@1' AND billing_interval = 'month' AND base_amount_minor = 30000) OR
|
|
49
|
+
(plan_version_id = 'scale@1' AND billing_interval = 'year' AND base_amount_minor = 300000)
|
|
50
|
+
),
|
|
51
|
+
current_period_start timestamptz NOT NULL,
|
|
52
|
+
current_period_end timestamptz NOT NULL CHECK (current_period_end > current_period_start),
|
|
53
|
+
effective_from timestamptz NOT NULL,
|
|
54
|
+
effective_until timestamptz NOT NULL CHECK (effective_until > effective_from),
|
|
55
|
+
renews boolean NOT NULL,
|
|
56
|
+
content_checksum text NOT NULL,
|
|
57
|
+
verified_at timestamptz NOT NULL,
|
|
58
|
+
created_at timestamptz NOT NULL,
|
|
59
|
+
PRIMARY KEY (subscription_id, version),
|
|
60
|
+
UNIQUE (billing_account_id, subscription_id, version),
|
|
61
|
+
UNIQUE (billing_account_id, subscription_id, version, plan_version_id),
|
|
62
|
+
CONSTRAINT billing_base_subscription_revisions_snapshot_source_key
|
|
63
|
+
UNIQUE (
|
|
64
|
+
billing_account_id,
|
|
65
|
+
subscription_id,
|
|
66
|
+
version,
|
|
67
|
+
plan_version_id,
|
|
68
|
+
effective_from,
|
|
69
|
+
effective_until
|
|
70
|
+
),
|
|
71
|
+
FOREIGN KEY (billing_account_id, subscription_id)
|
|
72
|
+
REFERENCES billing_base_subscriptions(billing_account_id, id) ON DELETE RESTRICT,
|
|
73
|
+
CHECK (verified_at >= effective_from),
|
|
74
|
+
CHECK (created_at >= verified_at),
|
|
75
|
+
CHECK (state <> 'active' OR effective_until = current_period_end),
|
|
76
|
+
CONSTRAINT billing_base_subscription_revisions_payment_grace_check CHECK (
|
|
77
|
+
state <> 'payment_grace' OR
|
|
78
|
+
(
|
|
79
|
+
effective_until = current_period_end + INTERVAL '168 hours' AND
|
|
80
|
+
verified_at >= current_period_end
|
|
81
|
+
)
|
|
82
|
+
),
|
|
83
|
+
CHECK (state <> 'ended' OR (NOT renews AND verified_at >= effective_until))
|
|
84
|
+
)
|
|
85
|
+
`);
|
|
86
|
+
await client.query(`
|
|
87
|
+
DO $billing_subscription_price_constraint$
|
|
88
|
+
BEGIN
|
|
89
|
+
IF NOT EXISTS (
|
|
90
|
+
SELECT 1 FROM pg_constraint
|
|
91
|
+
WHERE conname = 'billing_base_subscription_revisions_price_check'
|
|
92
|
+
AND conrelid = 'billing_base_subscription_revisions'::regclass
|
|
93
|
+
) THEN
|
|
94
|
+
ALTER TABLE billing_base_subscription_revisions
|
|
95
|
+
ADD CONSTRAINT billing_base_subscription_revisions_price_check CHECK (
|
|
96
|
+
(plan_version_id = 'pro@1' AND billing_interval = 'month' AND base_amount_minor = 3000) OR
|
|
97
|
+
(plan_version_id = 'pro@1' AND billing_interval = 'year' AND base_amount_minor = 30000) OR
|
|
98
|
+
(plan_version_id = 'scale@1' AND billing_interval = 'month' AND base_amount_minor = 30000) OR
|
|
99
|
+
(plan_version_id = 'scale@1' AND billing_interval = 'year' AND base_amount_minor = 300000)
|
|
100
|
+
);
|
|
101
|
+
END IF;
|
|
102
|
+
END
|
|
103
|
+
$billing_subscription_price_constraint$
|
|
104
|
+
`);
|
|
105
|
+
await client.query(`
|
|
106
|
+
CREATE TABLE IF NOT EXISTS billing_account_current_base_subscriptions (
|
|
107
|
+
billing_account_id text PRIMARY KEY REFERENCES billing_accounts(id) ON DELETE RESTRICT,
|
|
108
|
+
subscription_id text NOT NULL,
|
|
109
|
+
revision_version integer NOT NULL CHECK (revision_version > 0),
|
|
110
|
+
FOREIGN KEY (billing_account_id, subscription_id, revision_version)
|
|
111
|
+
REFERENCES billing_base_subscription_revisions
|
|
112
|
+
(billing_account_id, subscription_id, version)
|
|
113
|
+
ON DELETE RESTRICT
|
|
114
|
+
)
|
|
115
|
+
`);
|
|
116
|
+
await client.query(`
|
|
117
|
+
CREATE TABLE IF NOT EXISTS billing_subscription_reconciliation_operations (
|
|
118
|
+
operation_id text PRIMARY KEY,
|
|
119
|
+
schema_version integer NOT NULL CHECK (schema_version = 1),
|
|
120
|
+
action text NOT NULL CHECK (action = 'paid_reconcile'),
|
|
121
|
+
billing_account_id text NOT NULL REFERENCES billing_accounts(id) ON DELETE RESTRICT,
|
|
122
|
+
idempotency_key_hash text NOT NULL UNIQUE
|
|
123
|
+
CHECK (idempotency_key_hash ~ '^[0-9a-f]{64}$'),
|
|
124
|
+
request_fingerprint text NOT NULL
|
|
125
|
+
CHECK (request_fingerprint ~ '^[0-9a-f]{64}$'),
|
|
126
|
+
subscription_id text NOT NULL,
|
|
127
|
+
revision_version integer NOT NULL CHECK (revision_version > 0),
|
|
128
|
+
snapshot_id text NOT NULL,
|
|
129
|
+
result jsonb NOT NULL,
|
|
130
|
+
created_at timestamptz NOT NULL,
|
|
131
|
+
UNIQUE (billing_account_id, subscription_id, revision_version),
|
|
132
|
+
UNIQUE (billing_account_id, snapshot_id),
|
|
133
|
+
FOREIGN KEY (billing_account_id, snapshot_id)
|
|
134
|
+
REFERENCES billing_entitlement_snapshots(billing_account_id, id)
|
|
135
|
+
ON DELETE RESTRICT
|
|
136
|
+
)
|
|
137
|
+
`);
|
|
138
|
+
await client.query(`
|
|
139
|
+
ALTER TABLE billing_entitlement_snapshots
|
|
140
|
+
ADD COLUMN IF NOT EXISTS schema_version integer NOT NULL DEFAULT 1,
|
|
141
|
+
ADD COLUMN IF NOT EXISTS effective_until timestamptz,
|
|
142
|
+
ADD COLUMN IF NOT EXISTS source_subscription_id text,
|
|
143
|
+
ADD COLUMN IF NOT EXISTS source_subscription_version integer
|
|
144
|
+
`);
|
|
145
|
+
await client.query(`
|
|
146
|
+
DO $billing_reconciliation_snapshot_binding$
|
|
147
|
+
BEGIN
|
|
148
|
+
IF NOT EXISTS (
|
|
149
|
+
SELECT 1 FROM pg_constraint
|
|
150
|
+
WHERE conname = 'billing_entitlement_snapshots_reconciliation_source_key'
|
|
151
|
+
AND conrelid = 'billing_entitlement_snapshots'::regclass
|
|
152
|
+
) THEN
|
|
153
|
+
ALTER TABLE billing_entitlement_snapshots
|
|
154
|
+
ADD CONSTRAINT billing_entitlement_snapshots_reconciliation_source_key
|
|
155
|
+
UNIQUE (
|
|
156
|
+
billing_account_id,
|
|
157
|
+
id,
|
|
158
|
+
source_subscription_id,
|
|
159
|
+
source_subscription_version
|
|
160
|
+
);
|
|
161
|
+
END IF;
|
|
162
|
+
IF NOT EXISTS (
|
|
163
|
+
SELECT 1 FROM pg_constraint
|
|
164
|
+
WHERE conname = 'billing_reconciliation_operation_snapshot_source_fk'
|
|
165
|
+
AND conrelid = 'billing_subscription_reconciliation_operations'::regclass
|
|
166
|
+
) THEN
|
|
167
|
+
ALTER TABLE billing_subscription_reconciliation_operations
|
|
168
|
+
ADD CONSTRAINT billing_reconciliation_operation_snapshot_source_fk
|
|
169
|
+
FOREIGN KEY (
|
|
170
|
+
billing_account_id,
|
|
171
|
+
snapshot_id,
|
|
172
|
+
subscription_id,
|
|
173
|
+
revision_version
|
|
174
|
+
) REFERENCES billing_entitlement_snapshots (
|
|
175
|
+
billing_account_id,
|
|
176
|
+
id,
|
|
177
|
+
source_subscription_id,
|
|
178
|
+
source_subscription_version
|
|
179
|
+
) ON DELETE RESTRICT;
|
|
180
|
+
END IF;
|
|
181
|
+
END
|
|
182
|
+
$billing_reconciliation_snapshot_binding$
|
|
183
|
+
`);
|
|
184
|
+
await client.query(`
|
|
185
|
+
DO $billing_snapshot_anchor_consistency$
|
|
186
|
+
BEGIN
|
|
187
|
+
IF EXISTS (
|
|
188
|
+
SELECT 1
|
|
189
|
+
FROM billing_entitlement_snapshots
|
|
190
|
+
GROUP BY billing_account_id
|
|
191
|
+
HAVING MIN(monthly_usage_anchor) <> MAX(monthly_usage_anchor)
|
|
192
|
+
) THEN
|
|
193
|
+
RAISE EXCEPTION 'persisted billing account has inconsistent monthly usage anchors';
|
|
194
|
+
END IF;
|
|
195
|
+
END
|
|
196
|
+
$billing_snapshot_anchor_consistency$
|
|
197
|
+
`);
|
|
198
|
+
await client.query(`
|
|
199
|
+
CREATE OR REPLACE FUNCTION billing_enforce_entitlement_anchor_stability()
|
|
200
|
+
RETURNS trigger
|
|
201
|
+
LANGUAGE plpgsql
|
|
202
|
+
AS $billing_snapshot_anchor_guard$
|
|
203
|
+
BEGIN
|
|
204
|
+
IF TG_OP = 'UPDATE' THEN
|
|
205
|
+
IF NEW.billing_account_id IS DISTINCT FROM OLD.billing_account_id THEN
|
|
206
|
+
RAISE EXCEPTION 'billing entitlement snapshot account is immutable'
|
|
207
|
+
USING ERRCODE = '23514';
|
|
208
|
+
END IF;
|
|
209
|
+
IF NEW.monthly_usage_anchor IS DISTINCT FROM OLD.monthly_usage_anchor THEN
|
|
210
|
+
RAISE EXCEPTION 'billing account monthly usage anchor is immutable'
|
|
211
|
+
USING ERRCODE = '23514';
|
|
212
|
+
END IF;
|
|
213
|
+
RETURN NEW;
|
|
214
|
+
END IF;
|
|
215
|
+
|
|
216
|
+
PERFORM 1 FROM billing_accounts
|
|
217
|
+
WHERE id = NEW.billing_account_id
|
|
218
|
+
FOR UPDATE;
|
|
219
|
+
IF EXISTS (
|
|
220
|
+
SELECT 1 FROM billing_entitlement_snapshots
|
|
221
|
+
WHERE billing_account_id = NEW.billing_account_id
|
|
222
|
+
AND monthly_usage_anchor IS DISTINCT FROM NEW.monthly_usage_anchor
|
|
223
|
+
) THEN
|
|
224
|
+
RAISE EXCEPTION 'billing account monthly usage anchor is immutable'
|
|
225
|
+
USING ERRCODE = '23514';
|
|
226
|
+
END IF;
|
|
227
|
+
RETURN NEW;
|
|
228
|
+
END
|
|
229
|
+
$billing_snapshot_anchor_guard$
|
|
230
|
+
`);
|
|
231
|
+
await client.query(`
|
|
232
|
+
DO $billing_snapshot_anchor_trigger$
|
|
233
|
+
BEGIN
|
|
234
|
+
IF NOT EXISTS (
|
|
235
|
+
SELECT 1 FROM pg_trigger
|
|
236
|
+
WHERE tgname = 'billing_entitlement_snapshots_anchor_stability'
|
|
237
|
+
AND tgrelid = 'billing_entitlement_snapshots'::regclass
|
|
238
|
+
AND NOT tgisinternal
|
|
239
|
+
) THEN
|
|
240
|
+
BEGIN
|
|
241
|
+
CREATE TRIGGER billing_entitlement_snapshots_anchor_stability
|
|
242
|
+
BEFORE INSERT OR UPDATE ON billing_entitlement_snapshots
|
|
243
|
+
FOR EACH ROW EXECUTE FUNCTION billing_enforce_entitlement_anchor_stability();
|
|
244
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
245
|
+
NULL;
|
|
246
|
+
END;
|
|
247
|
+
END IF;
|
|
248
|
+
END
|
|
249
|
+
$billing_snapshot_anchor_trigger$
|
|
250
|
+
`);
|
|
251
|
+
await client.query(`
|
|
252
|
+
DO $billing_paid_snapshot_constraints$
|
|
253
|
+
BEGIN
|
|
254
|
+
IF NOT EXISTS (
|
|
255
|
+
SELECT 1 FROM pg_constraint
|
|
256
|
+
WHERE conname = 'billing_base_subscription_revisions_snapshot_source_key'
|
|
257
|
+
AND conrelid = 'billing_base_subscription_revisions'::regclass
|
|
258
|
+
) THEN
|
|
259
|
+
BEGIN
|
|
260
|
+
ALTER TABLE billing_base_subscription_revisions
|
|
261
|
+
ADD CONSTRAINT billing_base_subscription_revisions_snapshot_source_key
|
|
262
|
+
UNIQUE (
|
|
263
|
+
billing_account_id,
|
|
264
|
+
subscription_id,
|
|
265
|
+
version,
|
|
266
|
+
plan_version_id,
|
|
267
|
+
effective_from,
|
|
268
|
+
effective_until
|
|
269
|
+
);
|
|
270
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
271
|
+
NULL;
|
|
272
|
+
END;
|
|
273
|
+
END IF;
|
|
274
|
+
IF NOT EXISTS (
|
|
275
|
+
SELECT 1 FROM pg_constraint
|
|
276
|
+
WHERE conname = 'billing_entitlement_snapshots_provenance_v2_check'
|
|
277
|
+
AND conrelid = 'billing_entitlement_snapshots'::regclass
|
|
278
|
+
) THEN
|
|
279
|
+
BEGIN
|
|
280
|
+
ALTER TABLE billing_entitlement_snapshots
|
|
281
|
+
ADD CONSTRAINT billing_entitlement_snapshots_provenance_v2_check
|
|
282
|
+
CHECK (
|
|
283
|
+
verification_status = 'verified' AND (
|
|
284
|
+
(
|
|
285
|
+
schema_version = 1 AND
|
|
286
|
+
source_kind = 'system-free' AND
|
|
287
|
+
generation_reason = 'default-account-created' AND
|
|
288
|
+
effective_until IS NULL AND
|
|
289
|
+
source_subscription_id IS NULL AND
|
|
290
|
+
source_subscription_version IS NULL
|
|
291
|
+
) OR (
|
|
292
|
+
schema_version = 2 AND
|
|
293
|
+
source_kind = 'subscription' AND
|
|
294
|
+
generation_reason = 'subscription-reconciled' AND
|
|
295
|
+
effective_until > effective_from AND
|
|
296
|
+
source_subscription_id IS NOT NULL AND
|
|
297
|
+
source_subscription_version > 0
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
302
|
+
NULL;
|
|
303
|
+
END;
|
|
304
|
+
END IF;
|
|
305
|
+
END
|
|
306
|
+
$billing_paid_snapshot_constraints$
|
|
307
|
+
`);
|
|
308
|
+
// Replace the earlier four-column draft FK inside the same transaction. Including both effective
|
|
309
|
+
// bounds makes the snapshot an exact projection of the referenced immutable revision.
|
|
310
|
+
await client.query(`
|
|
311
|
+
DO $billing_paid_snapshot_revision_fk$
|
|
312
|
+
BEGIN
|
|
313
|
+
IF EXISTS (
|
|
314
|
+
SELECT 1 FROM pg_constraint
|
|
315
|
+
WHERE conname = 'billing_entitlement_snapshots_subscription_revision_fk'
|
|
316
|
+
AND conrelid = 'billing_entitlement_snapshots'::regclass
|
|
317
|
+
AND array_length(conkey, 1) <> 6
|
|
318
|
+
) THEN
|
|
319
|
+
ALTER TABLE billing_entitlement_snapshots
|
|
320
|
+
DROP CONSTRAINT billing_entitlement_snapshots_subscription_revision_fk;
|
|
321
|
+
END IF;
|
|
322
|
+
IF NOT EXISTS (
|
|
323
|
+
SELECT 1 FROM pg_constraint
|
|
324
|
+
WHERE conname = 'billing_entitlement_snapshots_subscription_revision_fk'
|
|
325
|
+
AND conrelid = 'billing_entitlement_snapshots'::regclass
|
|
326
|
+
) THEN
|
|
327
|
+
BEGIN
|
|
328
|
+
ALTER TABLE billing_entitlement_snapshots
|
|
329
|
+
ADD CONSTRAINT billing_entitlement_snapshots_subscription_revision_fk
|
|
330
|
+
FOREIGN KEY (
|
|
331
|
+
billing_account_id,
|
|
332
|
+
source_subscription_id,
|
|
333
|
+
source_subscription_version,
|
|
334
|
+
plan_version_id,
|
|
335
|
+
effective_from,
|
|
336
|
+
effective_until
|
|
337
|
+
) REFERENCES billing_base_subscription_revisions (
|
|
338
|
+
billing_account_id,
|
|
339
|
+
subscription_id,
|
|
340
|
+
version,
|
|
341
|
+
plan_version_id,
|
|
342
|
+
effective_from,
|
|
343
|
+
effective_until
|
|
344
|
+
) ON DELETE RESTRICT;
|
|
345
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
346
|
+
NULL;
|
|
347
|
+
END;
|
|
348
|
+
END IF;
|
|
349
|
+
END
|
|
350
|
+
$billing_paid_snapshot_revision_fk$
|
|
351
|
+
`);
|
|
352
|
+
// Install the replacement provenance guard before retiring the legacy Free-only checks. PostgreSQL
|
|
353
|
+
// exposes all of this transactional DDL atomically, so readers never observe an unguarded table.
|
|
354
|
+
await client.query(`
|
|
355
|
+
ALTER TABLE billing_entitlement_snapshots
|
|
356
|
+
DROP CONSTRAINT IF EXISTS billing_entitlement_snapshots_source_kind_check,
|
|
357
|
+
DROP CONSTRAINT IF EXISTS billing_entitlement_snapshots_generation_reason_check,
|
|
358
|
+
DROP CONSTRAINT IF EXISTS billing_entitlement_snapshots_verification_status_check
|
|
359
|
+
`);
|
|
360
|
+
await client.query(`
|
|
361
|
+
CREATE OR REPLACE FUNCTION billing_reject_subscription_history_mutation()
|
|
362
|
+
RETURNS trigger
|
|
363
|
+
LANGUAGE plpgsql
|
|
364
|
+
AS $billing_subscription_history_guard$
|
|
365
|
+
BEGIN
|
|
366
|
+
RAISE EXCEPTION 'billing subscription history is append-only' USING ERRCODE = '55000';
|
|
367
|
+
END
|
|
368
|
+
$billing_subscription_history_guard$
|
|
369
|
+
`);
|
|
370
|
+
await client.query(`
|
|
371
|
+
DO $billing_subscription_history_triggers$
|
|
372
|
+
BEGIN
|
|
373
|
+
IF NOT EXISTS (
|
|
374
|
+
SELECT 1 FROM pg_trigger
|
|
375
|
+
WHERE tgname = 'billing_base_subscriptions_append_only'
|
|
376
|
+
AND tgrelid = 'billing_base_subscriptions'::regclass
|
|
377
|
+
AND NOT tgisinternal
|
|
378
|
+
) THEN
|
|
379
|
+
BEGIN
|
|
380
|
+
CREATE TRIGGER billing_base_subscriptions_append_only
|
|
381
|
+
BEFORE UPDATE OR DELETE ON billing_base_subscriptions
|
|
382
|
+
FOR EACH ROW EXECUTE FUNCTION billing_reject_subscription_history_mutation();
|
|
383
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
384
|
+
NULL;
|
|
385
|
+
END;
|
|
386
|
+
END IF;
|
|
387
|
+
IF NOT EXISTS (
|
|
388
|
+
SELECT 1 FROM pg_trigger
|
|
389
|
+
WHERE tgname = 'billing_base_subscription_revisions_append_only'
|
|
390
|
+
AND tgrelid = 'billing_base_subscription_revisions'::regclass
|
|
391
|
+
AND NOT tgisinternal
|
|
392
|
+
) THEN
|
|
393
|
+
BEGIN
|
|
394
|
+
CREATE TRIGGER billing_base_subscription_revisions_append_only
|
|
395
|
+
BEFORE UPDATE OR DELETE ON billing_base_subscription_revisions
|
|
396
|
+
FOR EACH ROW EXECUTE FUNCTION billing_reject_subscription_history_mutation();
|
|
397
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
398
|
+
NULL;
|
|
399
|
+
END;
|
|
400
|
+
END IF;
|
|
401
|
+
IF NOT EXISTS (
|
|
402
|
+
SELECT 1 FROM pg_trigger
|
|
403
|
+
WHERE tgname = 'billing_subscription_reconciliation_operations_append_only'
|
|
404
|
+
AND tgrelid = 'billing_subscription_reconciliation_operations'::regclass
|
|
405
|
+
AND NOT tgisinternal
|
|
406
|
+
) THEN
|
|
407
|
+
BEGIN
|
|
408
|
+
CREATE TRIGGER billing_subscription_reconciliation_operations_append_only
|
|
409
|
+
BEFORE UPDATE OR DELETE ON billing_subscription_reconciliation_operations
|
|
410
|
+
FOR EACH ROW EXECUTE FUNCTION billing_reject_subscription_history_mutation();
|
|
411
|
+
EXCEPTION WHEN duplicate_object THEN
|
|
412
|
+
NULL;
|
|
413
|
+
END;
|
|
414
|
+
END IF;
|
|
415
|
+
END
|
|
416
|
+
$billing_subscription_history_triggers$
|
|
417
|
+
`);
|
|
418
|
+
}
|
|
419
|
+
//# sourceMappingURL=postgres-subscription-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-subscription-schema.js","sourceRoot":"","sources":["../../src/billing/postgres-subscription-schema.ts"],"names":[],"mappings":"AAEA,kGAAkG;AAClG,MAAM,CAAC,KAAK,UAAU,+BAA+B,CAAC,IAAU;IAC9D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC5B,IAAI,CAAC;YACH,4FAA4F;YAC5F,yEAAyE;YACzE,MAAM,MAAM,CAAC,KAAK,CAChB,iFAAiF,CAClF,CAAC;YACF,MAAM,iCAAiC,CAAC,MAAM,CAAC,CAAC;YAChD,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAC/B,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,iCAAiC,CAAC,MAAkB;IACjE,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;GAOlB,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDlB,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;GAkBlB,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;GAUlB,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;GAqBlB,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;GAMlB,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsClB,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;GAalB,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgClB,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;GAmBlB,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDlB,CAAC,CAAC;IAEH,iGAAiG;IACjG,sFAAsF;IACtF,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyClB,CAAC,CAAC;IAEH,mGAAmG;IACnG,iGAAiG;IACjG,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;GAKlB,CAAC,CAAC;IAEH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;GASlB,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+ClB,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type EntitlementSnapshotRecord, type PaidEntitlementSnapshotRecord } from './model.js';
|
|
2
|
+
export type BillingBaseSubscriptionState = 'active' | 'payment_grace' | 'ended';
|
|
3
|
+
export type BillingInterval = 'month' | 'year';
|
|
4
|
+
export interface BillingBaseSubscriptionRecord {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly billingAccountId: string;
|
|
7
|
+
readonly createdAt: string;
|
|
8
|
+
}
|
|
9
|
+
export interface BillingBaseSubscriptionRevisionRecord {
|
|
10
|
+
readonly schemaVersion: 1;
|
|
11
|
+
readonly subscriptionId: string;
|
|
12
|
+
readonly billingAccountId: string;
|
|
13
|
+
readonly version: number;
|
|
14
|
+
readonly planVersionId: string;
|
|
15
|
+
readonly state: BillingBaseSubscriptionState;
|
|
16
|
+
readonly billingInterval: BillingInterval;
|
|
17
|
+
readonly currency: 'usd';
|
|
18
|
+
readonly baseAmountMinor: number;
|
|
19
|
+
readonly currentPeriodStart: string;
|
|
20
|
+
readonly currentPeriodEnd: string;
|
|
21
|
+
readonly effectiveFrom: string;
|
|
22
|
+
readonly effectiveUntil: string;
|
|
23
|
+
readonly renews: boolean;
|
|
24
|
+
readonly contentChecksum: string;
|
|
25
|
+
readonly verifiedAt: string;
|
|
26
|
+
readonly createdAt: string;
|
|
27
|
+
}
|
|
28
|
+
export type BillingBaseSubscriptionRevisionInput = Omit<BillingBaseSubscriptionRevisionRecord, 'schemaVersion' | 'contentChecksum'>;
|
|
29
|
+
export declare function createBillingBaseSubscriptionRevision(input: BillingBaseSubscriptionRevisionInput): BillingBaseSubscriptionRevisionRecord;
|
|
30
|
+
export declare function parseBillingBaseSubscriptionRevision(value: unknown): BillingBaseSubscriptionRevisionRecord;
|
|
31
|
+
export interface CreatePaidEntitlementSnapshotInput {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly version: number;
|
|
34
|
+
readonly sourceSubscription: BillingBaseSubscriptionRevisionRecord;
|
|
35
|
+
readonly previousSnapshot: EntitlementSnapshotRecord;
|
|
36
|
+
}
|
|
37
|
+
export declare function createPaidEntitlementSnapshot(input: CreatePaidEntitlementSnapshotInput): PaidEntitlementSnapshotRecord;
|
|
38
|
+
export declare function parsePaidEntitlementSnapshot(value: unknown): PaidEntitlementSnapshotRecord;
|
|
39
|
+
export declare function validateBillingSubscriptionId(value: string): string;
|
|
40
|
+
//# sourceMappingURL=subscription-model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription-model.d.ts","sourceRoot":"","sources":["../../src/billing/subscription-model.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,yBAAyB,EAC9B,KAAK,6BAA6B,EAEnC,MAAM,YAAY,CAAC;AAMpB,MAAM,MAAM,4BAA4B,GAAG,QAAQ,GAAG,eAAe,GAAG,OAAO,CAAC;AAChF,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,CAAC;AAI/C,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,qCAAqC;IACpD,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC;IAC1B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,4BAA4B,CAAC;IAC7C,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAC1C,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IACzB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,oCAAoC,GAAG,IAAI,CACrD,qCAAqC,EACrC,eAAe,GAAG,iBAAiB,CACpC,CAAC;AAOF,wBAAgB,qCAAqC,CACnD,KAAK,EAAE,oCAAoC,GAC1C,qCAAqC,CAMvC;AAED,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,OAAO,GACb,qCAAqC,CAyBvC;AAED,MAAM,WAAW,kCAAkC;IACjD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,kBAAkB,EAAE,qCAAqC,CAAC;IACnE,QAAQ,CAAC,gBAAgB,EAAE,yBAAyB,CAAC;CACtD;AAED,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,kCAAkC,GACxC,6BAA6B,CAsC/B;AAmDD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,6BAA6B,CAmC1F;AA6LD,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGnE"}
|