@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.
Files changed (79) hide show
  1. package/node_modules/@noodle-borg/service/dist/billing/active-commercial-entitlement.d.ts +25 -0
  2. package/node_modules/@noodle-borg/service/dist/billing/active-commercial-entitlement.d.ts.map +1 -0
  3. package/node_modules/@noodle-borg/service/dist/billing/active-commercial-entitlement.js +94 -0
  4. package/node_modules/@noodle-borg/service/dist/billing/active-commercial-entitlement.js.map +1 -0
  5. package/node_modules/@noodle-borg/service/dist/billing/commercial-entitlements.d.ts +8 -0
  6. package/node_modules/@noodle-borg/service/dist/billing/commercial-entitlements.d.ts.map +1 -1
  7. package/node_modules/@noodle-borg/service/dist/billing/commercial-entitlements.js +49 -0
  8. package/node_modules/@noodle-borg/service/dist/billing/commercial-entitlements.js.map +1 -1
  9. package/node_modules/@noodle-borg/service/dist/billing/in-memory.d.ts.map +1 -1
  10. package/node_modules/@noodle-borg/service/dist/billing/in-memory.js +3 -2
  11. package/node_modules/@noodle-borg/service/dist/billing/in-memory.js.map +1 -1
  12. package/node_modules/@noodle-borg/service/dist/billing/integration.d.ts.map +1 -1
  13. package/node_modules/@noodle-borg/service/dist/billing/integration.js +0 -3
  14. package/node_modules/@noodle-borg/service/dist/billing/integration.js.map +1 -1
  15. package/node_modules/@noodle-borg/service/dist/billing/model.d.ts +16 -3
  16. package/node_modules/@noodle-borg/service/dist/billing/model.d.ts.map +1 -1
  17. package/node_modules/@noodle-borg/service/dist/billing/model.js +1 -0
  18. package/node_modules/@noodle-borg/service/dist/billing/model.js.map +1 -1
  19. package/node_modules/@noodle-borg/service/dist/billing/paid-plan-catalog.d.ts +26 -0
  20. package/node_modules/@noodle-borg/service/dist/billing/paid-plan-catalog.d.ts.map +1 -0
  21. package/node_modules/@noodle-borg/service/dist/billing/paid-plan-catalog.js +94 -0
  22. package/node_modules/@noodle-borg/service/dist/billing/paid-plan-catalog.js.map +1 -0
  23. package/node_modules/@noodle-borg/service/dist/billing/postgres-authoritative-authority.d.ts +4 -1
  24. package/node_modules/@noodle-borg/service/dist/billing/postgres-authoritative-authority.d.ts.map +1 -1
  25. package/node_modules/@noodle-borg/service/dist/billing/postgres-authoritative-authority.js +21 -7
  26. package/node_modules/@noodle-borg/service/dist/billing/postgres-authoritative-authority.js.map +1 -1
  27. package/node_modules/@noodle-borg/service/dist/billing/postgres-commercial-enforcement-counters.js +4 -5
  28. package/node_modules/@noodle-borg/service/dist/billing/postgres-commercial-enforcement-counters.js.map +1 -1
  29. package/node_modules/@noodle-borg/service/dist/billing/postgres-commercial-enforcement-projection.d.ts +0 -1
  30. package/node_modules/@noodle-borg/service/dist/billing/postgres-commercial-enforcement-projection.d.ts.map +1 -1
  31. package/node_modules/@noodle-borg/service/dist/billing/postgres-commercial-enforcement-projection.js +92 -27
  32. package/node_modules/@noodle-borg/service/dist/billing/postgres-commercial-enforcement-projection.js.map +1 -1
  33. package/node_modules/@noodle-borg/service/dist/billing/postgres-meter-admission.d.ts +0 -1
  34. package/node_modules/@noodle-borg/service/dist/billing/postgres-meter-admission.d.ts.map +1 -1
  35. package/node_modules/@noodle-borg/service/dist/billing/postgres-meter-admission.js +72 -27
  36. package/node_modules/@noodle-borg/service/dist/billing/postgres-meter-admission.js.map +1 -1
  37. package/node_modules/@noodle-borg/service/dist/billing/postgres-production-app-admission.d.ts.map +1 -1
  38. package/node_modules/@noodle-borg/service/dist/billing/postgres-production-app-admission.js +56 -26
  39. package/node_modules/@noodle-borg/service/dist/billing/postgres-production-app-admission.js.map +1 -1
  40. package/node_modules/@noodle-borg/service/dist/billing/postgres-records.d.ts +4 -0
  41. package/node_modules/@noodle-borg/service/dist/billing/postgres-records.d.ts.map +1 -1
  42. package/node_modules/@noodle-borg/service/dist/billing/postgres-records.js +38 -9
  43. package/node_modules/@noodle-borg/service/dist/billing/postgres-records.js.map +1 -1
  44. package/node_modules/@noodle-borg/service/dist/billing/postgres-schema.d.ts.map +1 -1
  45. package/node_modules/@noodle-borg/service/dist/billing/postgres-schema.js +28 -21
  46. package/node_modules/@noodle-borg/service/dist/billing/postgres-schema.js.map +1 -1
  47. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-authority.d.ts +5 -0
  48. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-authority.d.ts.map +1 -0
  49. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-authority.js +97 -0
  50. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-authority.js.map +1 -0
  51. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-persistence.d.ts +50 -0
  52. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-persistence.d.ts.map +1 -0
  53. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-persistence.js +221 -0
  54. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-persistence.js.map +1 -0
  55. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-support.d.ts +8 -0
  56. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-support.d.ts.map +1 -0
  57. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-support.js +42 -0
  58. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation-support.js.map +1 -0
  59. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation.d.ts +15 -0
  60. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation.d.ts.map +1 -0
  61. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation.js +123 -0
  62. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-reconciliation.js.map +1 -0
  63. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-schema.d.ts +4 -0
  64. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-schema.d.ts.map +1 -0
  65. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-schema.js +419 -0
  66. package/node_modules/@noodle-borg/service/dist/billing/postgres-subscription-schema.js.map +1 -0
  67. package/node_modules/@noodle-borg/service/dist/billing/subscription-model.d.ts +40 -0
  68. package/node_modules/@noodle-borg/service/dist/billing/subscription-model.d.ts.map +1 -0
  69. package/node_modules/@noodle-borg/service/dist/billing/subscription-model.js +388 -0
  70. package/node_modules/@noodle-borg/service/dist/billing/subscription-model.js.map +1 -0
  71. package/node_modules/@noodle-borg/service/dist/billing/subscription-reconciliation.d.ts +45 -0
  72. package/node_modules/@noodle-borg/service/dist/billing/subscription-reconciliation.d.ts.map +1 -0
  73. package/node_modules/@noodle-borg/service/dist/billing/subscription-reconciliation.js +165 -0
  74. package/node_modules/@noodle-borg/service/dist/billing/subscription-reconciliation.js.map +1 -0
  75. package/node_modules/@noodle-borg/service/dist/billing-public.d.ts +6 -1
  76. package/node_modules/@noodle-borg/service/dist/billing-public.d.ts.map +1 -1
  77. package/node_modules/@noodle-borg/service/dist/billing-public.js +5 -0
  78. package/node_modules/@noodle-borg/service/dist/billing-public.js.map +1 -1
  79. package/package.json +1 -1
@@ -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"}
@@ -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
@@ -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"}