openvibers/openvibe.billing Commits
Truthful readiness and status: /api/ready answers in the openvibe-shared/ready shape (named db and network_jwks checks, money_writes shown without failing it, release) instead of a bare {ready:true}; STATUS.json describes what production runs (release 421beb4, /metrics, hourly reconciliation, external receipts deployed; shadow import counts; the owner-blocked cutover)
Changes
@@@ -6,7 +6,7 @@"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","adr": "ADR-012","authoritative": false,-- "deployment": "Deployed in shadow on openvibe-ovh: /opt/openvibe.billing (release b59725e), unit openvibe-billing, API on 127.0.0.1:4600 (loopback only; /api/v1 is denied on the public host). Public at https://billing.openvibe.network: the staff console (Network SSO + PKCE, BILLING_STAFF_SUBJECTS = the owner only), /api/health and /webhooks/*. No provider secret is configured, so every provider is off and /webhooks/* answers 404 billing.provider_disabled. Pins openvibe-contracts v0.19.0.",-- "notes": "Runtime, journal, adapters, import, reconciliation and the staff console (cashout approve/deny, review queue, reconciliation, freeze, audit log) are built and tested. Capabilities (8 billing.*) and events are registered in OpenVibe.Contracts (v0.8.0; billing.staff.action in v0.17.0). Production holds a shadow import of a Live snapshot (322 transactions: 304 imported plus 18 opening adjustments; 3 reconciliation runs, the last 2026-09-23 01:21 UTC, all checks ok), 0 subscriptions and 0 entitlements; the console has never been used (staff_audit 0). The restore drill passed on the host 2026-09-23 05:22 UTC. The Live side (Billing client behind BILLING_AUTHORITY, money freeze) is deployed in Live since c384787, but BILLING_AUTHORITY is not set in live.env, so Live stays the money authority. The cutover (README runbook, docs/live-cutover.md) has not been executed: it waits on the owner putting the PowerChat secret in billing.env and re-pointing the PowerChat webhook to https://billing.openvibe.network/webhooks/powerchat. Built, not yet deployed (after b59725e): /metrics (openvibe-shared v1.3.0, loopback only); hourly scheduled reconciliation stored with its trigger and exposed at GET /api/v1/admin/reconciliations, in the console and on /metrics, with ledger-vs-receipt checks (receipts.ledger, receipts.external, receipts.stale); EXTERNAL PowerChat tips stored in external_receipts and announced as billing.receipt.external under BILLING_AUTHORITY=billing (default live: recorded only), attributed through provider_accounts (imported from Live's powerchat_connections), consumed by OpenVibe.Tips 6d14574; overflow guards (safe integers in the journal, $1M per receipt); settlement under an IMMEDIATE transaction (multi-process races settle once without lock errors). billing.receipt.external is not yet in an openvibe-contracts release. Live's payouts, refunds and plans have no tables of their own; where they map is docs/live-mapping.md.",-- "updated": "2026-09-23"++ "deployment": "Deployed in shadow on openvibe-ovh: /opt/openvibe.billing (release 421beb4, openvibe-contracts 0.33.0, openvibe-shared 1.5.1), unit openvibe-billing, API on 127.0.0.1:4600 (loopback only; /api/v1 is denied on the public host). Public at https://billing.openvibe.network: the staff console (Network SSO + PKCE, BILLING_STAFF_SUBJECTS = the owner only), /api/health, /api/ready (openvibe-shared/ready shape), /release.json and /webhooks/*. No provider secret is configured, so every provider is off and /webhooks/* answers 404 billing.provider_disabled.",++ "notes": "Runtime, journal, adapters, import, reconciliation and the staff console (cashout approve/deny, review queue, reconciliation, freeze, audit log) are built, tested and deployed, with /metrics (loopback only), hourly scheduled reconciliation (GET /api/v1/admin/reconciliations; 16 runs by 2026-09-24 05:06 UTC, ledger-vs-receipt checks), EXTERNAL PowerChat receipts (billing.receipt.external under BILLING_AUTHORITY=billing; recorded only while Live is the authority), overflow guards and IMMEDIATE-transaction settlement. Production holds the shadow import of Live (322 transactions, 996 ledger entries), 0 subscriptions and 0 entitlements; the console has never been used (staff_audit 0). BILLING_AUTHORITY is not set in live.env, so Live stays the money authority. The cutover (README runbook, docs/live-cutover.md) waits on the owner putting the PowerChat secret in billing.env and re-pointing the PowerChat webhook to https://billing.openvibe.network/webhooks/powerchat; a duplicate-webhook proof in production follows it. Live's payouts, refunds and plans have no tables of their own; where they map is docs/live-mapping.md.",++ "updated": "2026-09-24"}@@@ -64,13 +64,17 @@ function createApp(opts = {}) {providers: Object.fromEntries(Object.values(adapters).map((a) => [a.name, a.enabled])),events_relay: !!config.events.url,}));-- app.get('/api/ready', (req, res) => {-- const problems = [];-- try { db.prepare('SELECT 1 FROM settings WHERE id = 1').get(); } catch (e) { problems.push(`database: ${e.message}`); }-- if (!keys.get()) problems.push('Network public key not loaded');-- if (problems.length) return http.sendProblem(res, 503, 'service.not_ready', { detail: problems.join('; '), ctx: req.ov });-- return res.json({ ready: true });++ // Readiness in the openvibe-shared/ready shape (status, named checks, release): 503 when the ledger++ // database or the Network key fails; the money freeze and the authority show without failing it.++ const readiness = require('openvibe-shared/ready').createReadiness({++ service: 'billing', release: release.release,++ checks: [++ { name: 'db', required: true, check: () => db.prepare('SELECT 1 AS ok FROM settings WHERE id = 1').get().ok === 1 || 'settings row missing' },++ { name: 'network_jwks', required: true, check: () => Boolean(keys.get()) || 'Network public key not loaded' },++ { name: 'money_writes', required: false, check: () => (isFrozen(db) ? 'frozen' : true) },++ ],});++ app.get('/api/ready', readiness.handler);app.use('/webhooks', webhooksRouter({ ctx, adapters }));app.use('/api/v1', express.json({ limit: '64kb' }), v1Router({ ctx, auth, adapters }));@@@ -19,7 +19,11 @@ const { boot, fund, check, done } = require('./helpers/app');assert.deepStrictEqual(h.json.providers, { powerchat: true, stripe: false, paypal: false, ccbill: false, nowpayments: false });const r = await t.call('GET', '/api/v1/rates', { token: null });assert.strictEqual(r.json.packages.find((p) => p.bits === 1000).price_cents, 1300);-- assert.strictEqual((await t.call('GET', '/api/ready', { token: null })).status, 200);++ const ready = await t.call('GET', '/api/ready', { token: null });++ assert.strictEqual(ready.status, 200);++ assert.strictEqual(ready.json.status, 'ready', 'the openvibe-shared/ready shape');++ assert.strictEqual(ready.json.service, 'billing');++ assert.ok(ready.json.checks && ready.json.checks.db && ready.json.checks.network_jwks, 'named checks');const rel = await t.call('GET', '/release.json', { token: null });assert.strictEqual(rel.status, 200, 'the release manifest (D43) is served');assert.strictEqual(rel.json.service, 'billing');
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"adr": "ADR-012",
"adr": "ADR-012",
"authoritative": false,
"authoritative": false,
- "deployment": "Deployed in shadow on openvibe-ovh: /opt/openvibe.billing (release b59725e), unit openvibe-billing, API on 127.0.0.1:4600 (loopback only; /api/v1 is denied on the public host). Public at https://billing.openvibe.network: the staff console (Network SSO + PKCE, BILLING_STAFF_SUBJECTS = the owner only), /api/health and /webhooks/*. No provider secret is configured, so every provider is off and /webhooks/* answers 404 billing.provider_disabled. Pins openvibe-contracts v0.19.0.",
- "notes": "Runtime, journal, adapters, import, reconciliation and the staff console (cashout approve/deny, review queue, reconciliation, freeze, audit log) are built and tested. Capabilities (8 billing.*) and events are registered in OpenVibe.Contracts (v0.8.0; billing.staff.action in v0.17.0). Production holds a shadow import of a Live snapshot (322 transactions: 304 imported plus 18 opening adjustments; 3 reconciliation runs, the last 2026-09-23 01:21 UTC, all checks ok), 0 subscriptions and 0 entitlements; the console has never been used (staff_audit 0). The restore drill passed on the host 2026-09-23 05:22 UTC. The Live side (Billing client behind BILLING_AUTHORITY, money freeze) is deployed in Live since c384787, but BILLING_AUTHORITY is not set in live.env, so Live stays the money authority. The cutover (README runbook, docs/live-cutover.md) has not been executed: it waits on the owner putting the PowerChat secret in billing.env and re-pointing the PowerChat webhook to https://billing.openvibe.network/webhooks/powerchat. Built, not yet deployed (after b59725e): /metrics (openvibe-shared v1.3.0, loopback only); hourly scheduled reconciliation stored with its trigger and exposed at GET /api/v1/admin/reconciliations, in the console and on /metrics, with ledger-vs-receipt checks (receipts.ledger, receipts.external, receipts.stale); EXTERNAL PowerChat tips stored in external_receipts and announced as billing.receipt.external under BILLING_AUTHORITY=billing (default live: recorded only), attributed through provider_accounts (imported from Live's powerchat_connections), consumed by OpenVibe.Tips 6d14574; overflow guards (safe integers in the journal, $1M per receipt); settlement under an IMMEDIATE transaction (multi-process races settle once without lock errors). billing.receipt.external is not yet in an openvibe-contracts release. Live's payouts, refunds and plans have no tables of their own; where they map is docs/live-mapping.md.",
- "updated": "2026-09-23"
+ "deployment": "Deployed in shadow on openvibe-ovh: /opt/openvibe.billing (release 421beb4, openvibe-contracts 0.33.0, openvibe-shared 1.5.1), unit openvibe-billing, API on 127.0.0.1:4600 (loopback only; /api/v1 is denied on the public host). Public at https://billing.openvibe.network: the staff console (Network SSO + PKCE, BILLING_STAFF_SUBJECTS = the owner only), /api/health, /api/ready (openvibe-shared/ready shape), /release.json and /webhooks/*. No provider secret is configured, so every provider is off and /webhooks/* answers 404 billing.provider_disabled.",
+ "notes": "Runtime, journal, adapters, import, reconciliation and the staff console (cashout approve/deny, review queue, reconciliation, freeze, audit log) are built, tested and deployed, with /metrics (loopback only), hourly scheduled reconciliation (GET /api/v1/admin/reconciliations; 16 runs by 2026-09-24 05:06 UTC, ledger-vs-receipt checks), EXTERNAL PowerChat receipts (billing.receipt.external under BILLING_AUTHORITY=billing; recorded only while Live is the authority), overflow guards and IMMEDIATE-transaction settlement. Production holds the shadow import of Live (322 transactions, 996 ledger entries), 0 subscriptions and 0 entitlements; the console has never been used (staff_audit 0). BILLING_AUTHORITY is not set in live.env, so Live stays the money authority. The cutover (README runbook, docs/live-cutover.md) waits on the owner putting the PowerChat secret in billing.env and re-pointing the PowerChat webhook to https://billing.openvibe.network/webhooks/powerchat; a duplicate-webhook proof in production follows it. Live's payouts, refunds and plans have no tables of their own; where they map is docs/live-mapping.md.",
+ "updated": "2026-09-24"
}
}
@@ -64,13 +64,17 @@ function createApp(opts = {}) {
@@ -64,13 +64,17 @@ function createApp(opts = {}) {
providers: Object.fromEntries(Object.values(adapters).map((a) => [a.name, a.enabled])),
providers: Object.fromEntries(Object.values(adapters).map((a) => [a.name, a.enabled])),
events_relay: !!config.events.url,
events_relay: !!config.events.url,
}));
}));
- app.get('/api/ready', (req, res) => {
- const problems = [];
- try { db.prepare('SELECT 1 FROM settings WHERE id = 1').get(); } catch (e) { problems.push(`database: ${e.message}`); }
- if (!keys.get()) problems.push('Network public key not loaded');
- if (problems.length) return http.sendProblem(res, 503, 'service.not_ready', { detail: problems.join('; '), ctx: req.ov });
- return res.json({ ready: true });
+ // Readiness in the openvibe-shared/ready shape (status, named checks, release): 503 when the ledger
+ // database or the Network key fails; the money freeze and the authority show without failing it.
+ const readiness = require('openvibe-shared/ready').createReadiness({
+ service: 'billing', release: release.release,
+ checks: [
+ { name: 'db', required: true, check: () => db.prepare('SELECT 1 AS ok FROM settings WHERE id = 1').get().ok === 1 || 'settings row missing' },
+ { name: 'network_jwks', required: true, check: () => Boolean(keys.get()) || 'Network public key not loaded' },
+ { name: 'money_writes', required: false, check: () => (isFrozen(db) ? 'frozen' : true) },
+ ],
});
});
+ app.get('/api/ready', readiness.handler);
app.use('/webhooks', webhooksRouter({ ctx, adapters }));
app.use('/webhooks', webhooksRouter({ ctx, adapters }));
app.use('/api/v1', express.json({ limit: '64kb' }), v1Router({ ctx, auth, adapters }));
app.use('/api/v1', express.json({ limit: '64kb' }), v1Router({ ctx, auth, adapters }));
@@ -19,7 +19,11 @@ const { boot, fund, check, done } = require('./helpers/app');
@@ -19,7 +19,11 @@ const { boot, fund, check, done } = require('./helpers/app');
assert.deepStrictEqual(h.json.providers, { powerchat: true, stripe: false, paypal: false, ccbill: false, nowpayments: false });
assert.deepStrictEqual(h.json.providers, { powerchat: true, stripe: false, paypal: false, ccbill: false, nowpayments: false });
const r = await t.call('GET', '/api/v1/rates', { token: null });
const r = await t.call('GET', '/api/v1/rates', { token: null });
assert.strictEqual(r.json.packages.find((p) => p.bits === 1000).price_cents, 1300);
assert.strictEqual(r.json.packages.find((p) => p.bits === 1000).price_cents, 1300);
- assert.strictEqual((await t.call('GET', '/api/ready', { token: null })).status, 200);
+ const ready = await t.call('GET', '/api/ready', { token: null });
+ assert.strictEqual(ready.status, 200);
+ assert.strictEqual(ready.json.status, 'ready', 'the openvibe-shared/ready shape');
+ assert.strictEqual(ready.json.service, 'billing');
+ assert.ok(ready.json.checks && ready.json.checks.db && ready.json.checks.network_jwks, 'named checks');
const rel = await t.call('GET', '/release.json', { token: null });
const rel = await t.call('GET', '/release.json', { token: null });
assert.strictEqual(rel.status, 200, 'the release manifest (D43) is served');
assert.strictEqual(rel.status, 200, 'the release manifest (D43) is served');
assert.strictEqual(rel.json.service, 'billing');
assert.strictEqual(rel.json.service, 'billing');