From ca90045244d1ae5dba71b612ddfa8df20a725181 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 09:54:55 -0700 Subject: [PATCH 1/4] test(billing): require current Stripe invoice projection --- ...stripe-invoice-current-projection.test.mjs | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 tests/unit/stripe-invoice-current-projection.test.mjs diff --git a/tests/unit/stripe-invoice-current-projection.test.mjs b/tests/unit/stripe-invoice-current-projection.test.mjs new file mode 100644 index 00000000..d83216b7 --- /dev/null +++ b/tests/unit/stripe-invoice-current-projection.test.mjs @@ -0,0 +1,134 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { DatabaseSync } from 'node:sqlite'; + +import { createSqliteStripeInvoiceCurrentProjection } from '../../server/stripe_invoice_current_projection.mjs'; + +function fixture() { + const db = new DatabaseSync(':memory:'); + db.exec(` + CREATE TABLE billing_stripe_customers( + customer_id TEXT PRIMARY KEY, + organization_id INTEGER NOT NULL + ); + CREATE TABLE billing_stripe_subscriptions( + subscription_id TEXT PRIMARY KEY, + customer_id TEXT NOT NULL + ); + CREATE TABLE billing_stripe_invoices( + invoice_id TEXT PRIMARY KEY, + subscription_id TEXT NOT NULL + ); + CREATE TABLE billing_stripe_invoice_observations( + observation_id INTEGER PRIMARY KEY, + invoice_id TEXT NOT NULL, + source_subscription_observation_id INTEGER NOT NULL, + source_event_id TEXT, + observed_at_ms INTEGER NOT NULL, + invoice_status TEXT NOT NULL, + paid INTEGER NOT NULL, + currency_code TEXT NOT NULL, + amount_due_minor INTEGER NOT NULL, + amount_paid_minor INTEGER NOT NULL, + amount_remaining_minor INTEGER NOT NULL, + provider_created_at_sec INTEGER NOT NULL, + paid_at_sec INTEGER + ); + INSERT INTO billing_stripe_customers VALUES('cus_42',42),('cus_77',77); + INSERT INTO billing_stripe_subscriptions VALUES('sub_a','cus_42'),('sub_b','cus_42'),('sub_other','cus_77'); + INSERT INTO billing_stripe_invoices VALUES('in_a','sub_a'),('in_b','sub_b'),('in_other','sub_other'); + INSERT INTO billing_stripe_invoice_observations VALUES + (1,'in_a',101,'evt_old',9000,'open',0,'krw',29000,0,29000,5000,NULL), + (2,'in_a',102,'evt_paid',1000,'paid',1,'krw',29000,29000,0,4000,4100), + (3,'in_b',201,NULL,8000,'paid',1,'usd',1200,1200,0,6000,6100), + (4,'in_other',301,'evt_other',9000,'paid',1,'krw',5000,5000,0,7000,7100); + `); + return db; +} + +test('getCurrentInvoice selects append order, preserves provenance, and returns immutable normalized evidence', () => { + const db = fixture(); + const projection = createSqliteStripeInvoiceCurrentProjection(db); + assert.ok(Object.isFrozen(projection)); + const current = projection.getCurrentInvoice({ organizationId: 42, invoiceId: 'in_a' }); + assert.deepEqual(current, { + observationId: 2, + observedAtMs: 1000, + organizationId: 42, + customerId: 'cus_42', + subscriptionId: 'sub_a', + invoiceId: 'in_a', + sourceSubscriptionObservationId: 102, + sourceEventId: 'evt_paid', + status: 'paid', + paid: true, + currency: 'krw', + amountDue: 29000, + amountPaid: 29000, + amountRemaining: 0, + createdSec: 4000, + paidAtSec: 4100, + }); + assert.ok(Object.isFrozen(current)); + db.close(); +}); + +test('tenant isolation returns null for foreign or absent Invoice identities', () => { + const db = fixture(); + const projection = createSqliteStripeInvoiceCurrentProjection(db); + assert.equal(projection.getCurrentInvoice({ organizationId: 42, invoiceId: 'in_other' }), null); + assert.equal(projection.getCurrentInvoice({ organizationId: 77, invoiceId: 'in_a' }), null); + assert.equal(projection.getCurrentInvoice({ organizationId: 42, invoiceId: 'in_missing' }), null); + db.close(); +}); + +test('listCurrentInvoices returns exactly one latest Invoice per tenant and can narrow to one Subscription', () => { + const db = fixture(); + const projection = createSqliteStripeInvoiceCurrentProjection(db); + const all = projection.listCurrentInvoices({ organizationId: '42' }); + assert.ok(Object.isFrozen(all)); + assert.deepEqual(all.map((row) => [row.invoiceId, row.observationId, row.paidAtSec]), [ + ['in_a', 2, 4100], + ['in_b', 3, 6100], + ]); + assert.ok(all.every(Object.isFrozen)); + assert.deepEqual( + projection.listCurrentInvoices({ organizationId: 42, subscriptionId: 'sub_a' }).map((row) => row.invoiceId), + ['in_a'], + ); + assert.deepEqual(projection.listCurrentInvoices({ organizationId: 42, subscriptionId: 'sub_other' }), []); + db.close(); +}); + +test('unpaid projection preserves null paid time and false paid flag', () => { + const db = fixture(); + db.exec(`INSERT INTO billing_stripe_invoice_observations VALUES + (5,'in_b',202,NULL,9000,'void',0,'usd',1200,0,1200,8000,NULL)`); + const current = createSqliteStripeInvoiceCurrentProjection(db) + .getCurrentInvoice({ organizationId: 42, invoiceId: 'in_b' }); + assert.equal(current.status, 'void'); + assert.equal(current.paid, false); + assert.equal(current.paidAtSec, null); + assert.equal(current.sourceEventId, null); + db.close(); +}); + +test('canonical tenant and provider authority validation rejects ambiguous input before SQL use', () => { + const db = fixture(); + const projection = createSqliteStripeInvoiceCurrentProjection(db); + for (const organizationId of [0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, true, null, '0', '042', '+42', '4e1', ' 42 ']) { + assert.throws(() => projection.getCurrentInvoice({ organizationId, invoiceId: 'in_a' }), TypeError); + } + for (const invoiceId of ['', 'in bad', [], null, 'x'.repeat(256)]) { + assert.throws(() => projection.getCurrentInvoice({ organizationId: 42, invoiceId }), TypeError); + } + for (const subscriptionId of ['', 'sub bad', [], 'x'.repeat(256)]) { + assert.throws(() => projection.listCurrentInvoices({ organizationId: 42, subscriptionId }), TypeError); + } + db.close(); +}); + +test('projection dependency contract rejects unusable databases', () => { + assert.throws(() => createSqliteStripeInvoiceCurrentProjection(null), TypeError); + assert.throws(() => createSqliteStripeInvoiceCurrentProjection({}), TypeError); +}); From f56daa783f28e51f28e61d5899e2109b112d765b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 09:57:11 -0700 Subject: [PATCH 2/4] feat(billing): project current Stripe invoice evidence --- .../stripe-invoice-current-projection.md | 37 ++++ package.json | 6 +- server/stripe_invoice_current_projection.mjs | 169 ++++++++++++++++++ 3 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 docs/doctoring/stripe-invoice-current-projection.md create mode 100644 server/stripe_invoice_current_projection.mjs diff --git a/docs/doctoring/stripe-invoice-current-projection.md b/docs/doctoring/stripe-invoice-current-projection.md new file mode 100644 index 00000000..63d5442f --- /dev/null +++ b/docs/doctoring/stripe-invoice-current-projection.md @@ -0,0 +1,37 @@ +# Current authoritative Stripe Invoice projection + +## Status and authority + +**Status: active stacked PR evidence, not protected-`develop` shipped truth.** + +This #488 slice is stacked on the authoritative Invoice observation ledger. It exposes current accepted Invoice evidence to later entitlement reconciliation without treating webhook delivery order, provider wall-clock fields, or one payment status as local authorization authority. Protected `develop` remains the shipped truth until the prerequisite stack is independently reviewed and integrated. + +## Decision boundary + +`server/stripe_invoice_current_projection.mjs` is read-only. It selects current Invoice state by the highest append-only local `observation_id`, because that identity records the order in which ScopeWeave accepted authoritative provider reads. It does not choose by webhook receipt, provider creation time, paid transition time, or `observed_at_ms`; those values remain evidence, not sequencing authority. + +Each read is tenant-scoped through Invoice → Subscription → Customer → organization. The caller must provide a canonical positive organization ID. String tenant IDs are accepted only in canonical positive decimal form, rejecting leading zeroes, plus signs, exponent notation, whitespace, and other JavaScript-coercible spellings. Invoice and optional Subscription filters must be bounded provider identifiers. + +## Projection contract + +`getCurrentInvoice` returns one immutable latest accepted observation for an exact tenant-owned Invoice or `null` when the Invoice is absent or belongs to another organization. + +`listCurrentInvoices` returns one immutable latest observation per Invoice owned by the organization, ordered by Invoice identity. An optional Subscription filter narrows the result without weakening tenant isolation. + +Each result preserves the accepted evidence needed by the entitlement-policy layer: local observation identity/time, organization/Customer/Subscription/Invoice identity, exact source Subscription observation, optional verified webhook-event provenance, Invoice lifecycle state, paid flag, currency, minor-unit amounts, provider creation time, and paid transition time. No raw provider JSON, secrets, customer contact data, entitlement claim, or `orgs.plan` mutation is introduced. + +## TDD and acceptance evidence + +A test-only commit introduces `tests/unit/stripe-invoice-current-projection.test.mjs` before the production module exists, establishing RED module-resolution evidence. The focused acceptance suite proves append-order selection even when `observed_at_ms` and provider timestamps move in the opposite direction, tenant isolation, one-current-row-per-Invoice listing, Subscription filtering, immutable outputs, unpaid/null-paid-time behavior, canonical authority validation, and fail-closed dependency contracts. + +Private focused execution after the production implementation produced 100% line, branch, and function coverage for both the projection module and its focused suite. Hosted exact-head CI, browser, security, dependency, and independent review evidence remains authoritative for integration. + +## Rollback + +Rollback removes the read-only projection, focused test/coverage registration, this doctoring record, and the matching active Unreleased changelog entry. No schema migration or entitlement mutation is introduced by this slice. + +## References + +SQLite. (n.d.). *SELECT*. https://www.sqlite.org/lang_select.html + +Stripe. (n.d.). *The Invoice object*. Stripe API Reference. https://docs.stripe.com/api/invoices/object diff --git a/package.json b/package.json index 6492d1f3..6e2bee25 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "coverage": "npm run test:coverage", "server": "node server/server.mjs", "test:api": "node tests/api/auth-secret.test.mjs && node --env-file=tests/api/smoke.env tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs && node tests/api/orchestrator-attribution.test.mjs && node tests/api/billing-checkout.test.mjs && node tests/api/billing-live-checkout.test.mjs && node tests/api/stripe-webhook.test.mjs", - "test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/stripe-webhook-boundary.test.mjs && node tests/unit/stripe-webhook-event-ledger.test.mjs && node tests/unit/stripe-webhook-recorder-integration.test.mjs && node tests/unit/stripe-subscription-provider.test.mjs && node tests/unit/stripe-subscription-metadata-propagation.test.mjs && node tests/unit/stripe-subscription-observation-ledger.test.mjs && node tests/unit/stripe-subscription-current-projection.test.mjs && node tests/unit/stripe-entitlement-policy.test.mjs && node tests/unit/stripe-entitlement-policy-edge.test.mjs && node tests/unit/stripe-entitlement-policy-duplicate-claims.test.mjs && node tests/unit/stripe-invoice-provider.test.mjs && node tests/unit/stripe-invoice-provider-edge.test.mjs && node tests/unit/stripe-invoice-observation-ledger.test.mjs && node tests/unit/toast-accessibility.test.mjs", - "test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/application_routes.mjs --include=server/auth.mjs --include=server/billing.mjs --include=server/billing_checkout_attempt.mjs --include=server/billing_configuration.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --include=server/stripe_webhook.mjs --include=server/stripe_webhook_event_ledger.mjs --include=server/stripe_subscription_provider.mjs --include=server/stripe_subscription_observation_ledger.mjs --include=server/stripe_subscription_current_projection.mjs --include=server/stripe_entitlement_policy.mjs --include=server/stripe_invoice_provider.mjs --include=server/stripe_invoice_observation_ledger.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases", - "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/stripe-webhook-boundary.test.mjs && node tests/unit/stripe-webhook-event-ledger.test.mjs && node tests/unit/stripe-webhook-recorder-integration.test.mjs && node tests/unit/stripe-subscription-provider.test.mjs && node tests/unit/stripe-subscription-metadata-propagation.test.mjs && node tests/unit/stripe-subscription-observation-ledger.test.mjs && node tests/unit/stripe-subscription-current-projection.test.mjs && node tests/unit/stripe-entitlement-policy.test.mjs && node tests/unit/stripe-entitlement-policy-edge.test.mjs && node tests/unit/stripe-entitlement-policy-duplicate-claims.test.mjs && node tests/unit/stripe-invoice-provider.test.mjs && node tests/unit/stripe-invoice-provider-edge.test.mjs && node tests/unit/stripe-invoice-observation-ledger.test.mjs && npm run test:api", + "test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/stripe-webhook-boundary.test.mjs && node tests/unit/stripe-webhook-event-ledger.test.mjs && node tests/unit/stripe-webhook-recorder-integration.test.mjs && node tests/unit/stripe-subscription-provider.test.mjs && node tests/unit/stripe-subscription-metadata-propagation.test.mjs && node tests/unit/stripe-subscription-observation-ledger.test.mjs && node tests/unit/stripe-subscription-current-projection.test.mjs && node tests/unit/stripe-entitlement-policy.test.mjs && node tests/unit/stripe-entitlement-policy-edge.test.mjs && node tests/unit/stripe-entitlement-policy-duplicate-claims.test.mjs && node tests/unit/stripe-invoice-provider.test.mjs && node tests/unit/stripe-invoice-provider-edge.test.mjs && node tests/unit/stripe-invoice-observation-ledger.test.mjs && node tests/unit/stripe-invoice-current-projection.test.mjs && node tests/unit/toast-accessibility.test.mjs", + "test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/application_routes.mjs --include=server/auth.mjs --include=server/billing.mjs --include=server/billing_checkout_attempt.mjs --include=server/billing_configuration.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --include=server/stripe_webhook.mjs --include=server/stripe_webhook_event_ledger.mjs --include=server/stripe_subscription_provider.mjs --include=server/stripe_subscription_observation_ledger.mjs --include=server/stripe_subscription_current_projection.mjs --include=server/stripe_entitlement_policy.mjs --include=server/stripe_invoice_provider.mjs --include=server/stripe_invoice_observation_ledger.mjs --include=server/stripe_invoice_current_projection.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases", + "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/stripe-webhook-boundary.test.mjs && node tests/unit/stripe-webhook-event-ledger.test.mjs && node tests/unit/stripe-webhook-recorder-integration.test.mjs && node tests/unit/stripe-subscription-provider.test.mjs && node tests/unit/stripe-subscription-metadata-propagation.test.mjs && node tests/unit/stripe-subscription-observation-ledger.test.mjs && node tests/unit/stripe-subscription-current-projection.test.mjs && node tests/unit/stripe-entitlement-policy.test.mjs && node tests/unit/stripe-entitlement-policy-edge.test.mjs && node tests/unit/stripe-entitlement-policy-duplicate-claims.test.mjs && node tests/unit/stripe-invoice-provider.test.mjs && node tests/unit/stripe-invoice-provider-edge.test.mjs && node tests/unit/stripe-invoice-observation-ledger.test.mjs && node tests/unit/stripe-invoice-current-projection.test.mjs && npm run test:api", "test:e2e": "playwright test", "test:e2e:headed": "playwright test --headed", "test:e2e:cloud": "playwright install chromium && playwright test tests/e2e/cloud.spec.js tests/e2e/toast-accessibility.spec.js", diff --git a/server/stripe_invoice_current_projection.mjs b/server/stripe_invoice_current_projection.mjs new file mode 100644 index 00000000..a535e9ad --- /dev/null +++ b/server/stripe_invoice_current_projection.mjs @@ -0,0 +1,169 @@ +const MAX_PROVIDER_ID_LENGTH = 255; +const PROVIDER_IDENTIFIER_PATTERN = /^[A-Za-z0-9_:-]+$/u; +const CANONICAL_ORGANIZATION_ID_PATTERN = /^[1-9][0-9]*$/u; + +function positiveOrganizationId(value) { + if (typeof value === 'string' && !CANONICAL_ORGANIZATION_ID_PATTERN.test(value)) { + throw new TypeError('organizationId must be a positive safe integer'); + } + if (typeof value !== 'number' && typeof value !== 'string') { + throw new TypeError('organizationId must be a positive safe integer'); + } + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || parsed <= 0) { + throw new TypeError('organizationId must be a positive safe integer'); + } + return parsed; +} + +function requiredInvoiceId(value) { + if (typeof value !== 'string' + || value.length === 0 + || value.length > MAX_PROVIDER_ID_LENGTH + || !PROVIDER_IDENTIFIER_PATTERN.test(value)) { + throw new TypeError('invoiceId must be a bounded Stripe identifier'); + } + return value; +} + +function requiredSubscriptionId(value) { + if (typeof value !== 'string' + || value.length === 0 + || value.length > MAX_PROVIDER_ID_LENGTH + || !PROVIDER_IDENTIFIER_PATTERN.test(value)) { + throw new TypeError('subscriptionId must be a bounded Stripe identifier'); + } + return value; +} + +function freezeProjection(row) { + return Object.freeze({ + observationId: Number(row.observation_id), + observedAtMs: Number(row.observed_at_ms), + organizationId: Number(row.organization_id), + customerId: row.customer_id, + subscriptionId: row.subscription_id, + invoiceId: row.invoice_id, + sourceSubscriptionObservationId: Number(row.source_subscription_observation_id), + sourceEventId: row.source_event_id ?? null, + status: row.invoice_status, + paid: Number(row.paid) === 1, + currency: row.currency_code, + amountDue: Number(row.amount_due_minor), + amountPaid: Number(row.amount_paid_minor), + amountRemaining: Number(row.amount_remaining_minor), + createdSec: Number(row.provider_created_at_sec), + paidAtSec: row.paid_at_sec == null ? null : Number(row.paid_at_sec), + }); +} + +/** + * Create a tenant-scoped read-only projection over accepted authoritative Stripe + * Invoice observations. + * + * Current state is selected by append-only observation identity rather than + * webhook arrival time, provider creation time, or mutable wall-clock values. + * Every row was already linked to an accepted Subscription observation and is + * re-scoped through Subscription -> Customer -> organization on read. The + * projection never mutates plan or entitlement state. + * + * @param {import('node:sqlite').DatabaseSync} database bootstrapped database + * @returns {{ + * getCurrentInvoice(input: {organizationId: number|string, invoiceId: string}): Readonly>|null, + * listCurrentInvoices(input: {organizationId: number|string, subscriptionId?: string|null}): readonly Readonly>[] + * }} tenant-scoped immutable Invoice projection + */ +export function createSqliteStripeInvoiceCurrentProjection(database) { + if (!database || typeof database.prepare !== 'function') { + throw new TypeError('database must provide SQLite prepare operations'); + } + + const selectCurrentInvoice = database.prepare(` + SELECT + observations.observation_id, + observations.observed_at_ms, + customers.organization_id, + subscriptions.customer_id, + invoices.subscription_id, + observations.invoice_id, + observations.source_subscription_observation_id, + observations.source_event_id, + observations.invoice_status, + observations.paid, + observations.currency_code, + observations.amount_due_minor, + observations.amount_paid_minor, + observations.amount_remaining_minor, + observations.provider_created_at_sec, + observations.paid_at_sec + FROM billing_stripe_invoice_observations AS observations + JOIN billing_stripe_invoices AS invoices + ON invoices.invoice_id = observations.invoice_id + JOIN billing_stripe_subscriptions AS subscriptions + ON subscriptions.subscription_id = invoices.subscription_id + JOIN billing_stripe_customers AS customers + ON customers.customer_id = subscriptions.customer_id + WHERE customers.organization_id = ? + AND observations.invoice_id = ? + ORDER BY observations.observation_id DESC + LIMIT 1 + `); + + const selectCurrentInvoices = database.prepare(` + SELECT + observations.observation_id, + observations.observed_at_ms, + customers.organization_id, + subscriptions.customer_id, + invoices.subscription_id, + observations.invoice_id, + observations.source_subscription_observation_id, + observations.source_event_id, + observations.invoice_status, + observations.paid, + observations.currency_code, + observations.amount_due_minor, + observations.amount_paid_minor, + observations.amount_remaining_minor, + observations.provider_created_at_sec, + observations.paid_at_sec + FROM billing_stripe_invoice_observations AS observations + JOIN ( + SELECT invoice_id, MAX(observation_id) AS observation_id + FROM billing_stripe_invoice_observations + GROUP BY invoice_id + ) AS current_observations + ON current_observations.observation_id = observations.observation_id + JOIN billing_stripe_invoices AS invoices + ON invoices.invoice_id = observations.invoice_id + JOIN billing_stripe_subscriptions AS subscriptions + ON subscriptions.subscription_id = invoices.subscription_id + JOIN billing_stripe_customers AS customers + ON customers.customer_id = subscriptions.customer_id + WHERE customers.organization_id = ? + AND (? IS NULL OR invoices.subscription_id = ?) + ORDER BY observations.invoice_id ASC + `); + + return Object.freeze({ + /** Return the latest accepted observation for one tenant-owned Invoice. */ + getCurrentInvoice({ organizationId, invoiceId }) { + const normalizedOrganizationId = positiveOrganizationId(organizationId); + const normalizedInvoiceId = requiredInvoiceId(invoiceId); + const row = selectCurrentInvoice.get(normalizedOrganizationId, normalizedInvoiceId); + return row ? freezeProjection(row) : null; + }, + + /** Return one latest accepted observation per Invoice, optionally for one Subscription. */ + listCurrentInvoices({ organizationId, subscriptionId = null }) { + const normalizedOrganizationId = positiveOrganizationId(organizationId); + const normalizedSubscriptionId = subscriptionId == null ? null : requiredSubscriptionId(subscriptionId); + const rows = selectCurrentInvoices.all( + normalizedOrganizationId, + normalizedSubscriptionId, + normalizedSubscriptionId, + ); + return Object.freeze(rows.map(freezeProjection)); + }, + }); +} From 843ac4ad295e5c13217cb5f12321508b8112cdd1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 09:58:31 -0700 Subject: [PATCH 3/4] test(coverage): lock current Invoice projection into canonical suites --- package.json | 4 ++-- ...rrent-projection-package-contract.test.mjs | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/unit/stripe-invoice-current-projection-package-contract.test.mjs diff --git a/package.json b/package.json index 6e2bee25..db9eac54 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "coverage": "npm run test:coverage", "server": "node server/server.mjs", "test:api": "node tests/api/auth-secret.test.mjs && node --env-file=tests/api/smoke.env tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs && node tests/api/attachment-status.test.mjs && node tests/api/session-revocation.test.mjs && node tests/api/orchestrator-attribution.test.mjs && node tests/api/billing-checkout.test.mjs && node tests/api/billing-live-checkout.test.mjs && node tests/api/stripe-webhook.test.mjs", - "test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/stripe-webhook-boundary.test.mjs && node tests/unit/stripe-webhook-event-ledger.test.mjs && node tests/unit/stripe-webhook-recorder-integration.test.mjs && node tests/unit/stripe-subscription-provider.test.mjs && node tests/unit/stripe-subscription-metadata-propagation.test.mjs && node tests/unit/stripe-subscription-observation-ledger.test.mjs && node tests/unit/stripe-subscription-current-projection.test.mjs && node tests/unit/stripe-entitlement-policy.test.mjs && node tests/unit/stripe-entitlement-policy-edge.test.mjs && node tests/unit/stripe-entitlement-policy-duplicate-claims.test.mjs && node tests/unit/stripe-invoice-provider.test.mjs && node tests/unit/stripe-invoice-provider-edge.test.mjs && node tests/unit/stripe-invoice-observation-ledger.test.mjs && node tests/unit/stripe-invoice-current-projection.test.mjs && node tests/unit/toast-accessibility.test.mjs", + "test:unit": "node tests/unit/opencode-config.test.mjs && node tests/unit/changelog-release-notes.test.mjs && node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/coverage-script-contract.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/stripe-webhook-boundary.test.mjs && node tests/unit/stripe-webhook-event-ledger.test.mjs && node tests/unit/stripe-webhook-recorder-integration.test.mjs && node tests/unit/stripe-subscription-provider.test.mjs && node tests/unit/stripe-subscription-metadata-propagation.test.mjs && node tests/unit/stripe-subscription-observation-ledger.test.mjs && node tests/unit/stripe-subscription-current-projection.test.mjs && node tests/unit/stripe-entitlement-policy.test.mjs && node tests/unit/stripe-entitlement-policy-edge.test.mjs && node tests/unit/stripe-entitlement-policy-duplicate-claims.test.mjs && node tests/unit/stripe-invoice-provider.test.mjs && node tests/unit/stripe-invoice-provider-edge.test.mjs && node tests/unit/stripe-invoice-observation-ledger.test.mjs && node tests/unit/stripe-invoice-current-projection-package-contract.test.mjs && node tests/unit/stripe-invoice-current-projection.test.mjs && node tests/unit/toast-accessibility.test.mjs", "test:coverage": "c8 --all --include=app.js --include=cloud-sync.js --include=scripts/ci/static_coverage_evidence.mjs --include=server/attachment_status.mjs --include=server/app.mjs --include=server/application_routes.mjs --include=server/auth.mjs --include=server/billing.mjs --include=server/billing_checkout_attempt.mjs --include=server/billing_configuration.mjs --include=server/clearfolio.mjs --include=server/orchestrator.mjs --include=server/stripe_webhook.mjs --include=server/stripe_webhook_event_ledger.mjs --include=server/stripe_subscription_provider.mjs --include=server/stripe_subscription_observation_ledger.mjs --include=server/stripe_subscription_current_projection.mjs --include=server/stripe_entitlement_policy.mjs --include=server/stripe_invoice_provider.mjs --include=server/stripe_invoice_observation_ledger.mjs --include=server/stripe_invoice_current_projection.mjs --reporter=json --reporter=json-summary npm run test:coverage:cases", - "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/stripe-webhook-boundary.test.mjs && node tests/unit/stripe-webhook-event-ledger.test.mjs && node tests/unit/stripe-webhook-recorder-integration.test.mjs && node tests/unit/stripe-subscription-provider.test.mjs && node tests/unit/stripe-subscription-metadata-propagation.test.mjs && node tests/unit/stripe-subscription-observation-ledger.test.mjs && node tests/unit/stripe-subscription-current-projection.test.mjs && node tests/unit/stripe-entitlement-policy.test.mjs && node tests/unit/stripe-entitlement-policy-edge.test.mjs && node tests/unit/stripe-entitlement-policy-duplicate-claims.test.mjs && node tests/unit/stripe-invoice-provider.test.mjs && node tests/unit/stripe-invoice-provider-edge.test.mjs && node tests/unit/stripe-invoice-observation-ledger.test.mjs && node tests/unit/stripe-invoice-current-projection.test.mjs && npm run test:api", + "test:coverage:cases": "node tests/unit/coverage-script-contract.test.mjs && node tests/unit/attachment-status.test.mjs && node tests/unit/clearfolio-status-signal.test.mjs && node tests/unit/clearfolio-adapter-mock-hmac.test.mjs && node tests/unit/orchestrator.test.mjs && node tests/unit/orchestrator-coverage.test.mjs && node tests/unit/orchestrator-attribution.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/auth-password.test.mjs && node tests/unit/editor-unsaved.test.mjs && node tests/unit/static-coverage-evidence.test.mjs && node tests/unit/billing-configuration.test.mjs && node tests/unit/billing-checkout-attempt.test.mjs && node tests/unit/billing-checkout-attempt-authority.test.mjs && node tests/unit/billing-checkout-reconciliation.test.mjs && node tests/unit/billing-checkout-reconciliation-authority.test.mjs && node tests/unit/billing-checkout.test.mjs && node tests/unit/billing-provider-boundary.test.mjs && node tests/unit/billing-checkout-review-regressions.test.mjs && node tests/unit/stripe-webhook-boundary.test.mjs && node tests/unit/stripe-webhook-event-ledger.test.mjs && node tests/unit/stripe-webhook-recorder-integration.test.mjs && node tests/unit/stripe-subscription-provider.test.mjs && node tests/unit/stripe-subscription-metadata-propagation.test.mjs && node tests/unit/stripe-subscription-observation-ledger.test.mjs && node tests/unit/stripe-subscription-current-projection.test.mjs && node tests/unit/stripe-entitlement-policy.test.mjs && node tests/unit/stripe-entitlement-policy-edge.test.mjs && node tests/unit/stripe-entitlement-policy-duplicate-claims.test.mjs && node tests/unit/stripe-invoice-provider.test.mjs && node tests/unit/stripe-invoice-provider-edge.test.mjs && node tests/unit/stripe-invoice-observation-ledger.test.mjs && node tests/unit/stripe-invoice-current-projection-package-contract.test.mjs && node tests/unit/stripe-invoice-current-projection.test.mjs && npm run test:api", "test:e2e": "playwright test", "test:e2e:headed": "playwright test --headed", "test:e2e:cloud": "playwright install chromium && playwright test tests/e2e/cloud.spec.js tests/e2e/toast-accessibility.spec.js", diff --git a/tests/unit/stripe-invoice-current-projection-package-contract.test.mjs b/tests/unit/stripe-invoice-current-projection-package-contract.test.mjs new file mode 100644 index 00000000..eac30d65 --- /dev/null +++ b/tests/unit/stripe-invoice-current-projection-package-contract.test.mjs @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; + +const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')); +const scripts = packageJson.scripts; + +assert.match( + scripts['test:coverage'], + /--include=server\/stripe_invoice_current_projection\.mjs/, + 'the current Stripe Invoice projection remains in owned production coverage', +); +assert.match( + scripts['test:coverage:cases'], + /tests\/unit\/stripe-invoice-current-projection\.test\.mjs/, + 'the current Stripe Invoice projection regression executes under c8', +); +assert.match( + scripts['test:unit'], + /tests\/unit\/stripe-invoice-current-projection\.test\.mjs/, + 'normal unit CI executes the current Stripe Invoice projection regression', +); + +console.log('✓ Stripe Invoice projection package contract passed'); From 50c1a4005d8fcd408ccdbd49672204d571df37e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 09:59:51 -0700 Subject: [PATCH 4/4] docs(changelog): record current Invoice projection --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a325de68..68dc5009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security +- Added a tenant-scoped read-only projection over accepted authoritative Stripe + Invoice observations, selecting current state by append identity rather than + webhook/provider time, preserving payment/provenance facts, and rejecting + ambiguous tenant or provider filters without mutating entitlement state. - Persist authoritative Stripe Invoice reads as normalized append-only evidence linked to the exact accepted Subscription observation that named the Invoice; tenant/customer/subscription/invoice rebinding fails closed, local observation