openvibers/openvibe.billing Commits
The SQLite era is gone (on PostgreSQL since 2026-09-28; no compatibility kept): the one-time migrate-to-postgres script, the *_DB_PATH settings (config, .env.example, systemd unit, test helpers), the unused better-sqlite3 dependency and its lockfile entries, and docs, docstrings and STATUS lines about SQLite or its rollback window. Reviewed independently (VERDICT: SHIP). npm test 19/19 test files passed.
Changes
@@@ -18,9 +18,6 @@ DATABASE_URL=DATABASE_DIRECT_URL=# Valkey: the per-person limit counters, kept across restarts (optional; without it they count in the process).VALKEY_URL=--# The SQLite ledger the one-time move reads (scripts/migrate-to-postgres.js). The systemd unit sets--# /var/lib/openvibe-billing/billing.db.--BILLING_DB_PATH=./data/billing.db# Who keeps the money books: live (default — OpenVibe.Live is the ledger and receives the PowerChat# webhook; Billing runs in shadow) or billing (after the cutover, docs/live-cutover.md). Only EXTERNAL@@@ -78,8 +78,7 @@ node scripts/import-live.js --live-db <snapshot> --accounts-only # only PowerC```Production: `/opt/openvibe.billing`, env `/etc/openvibe/billing.env`, unit--[deploy/systemd/openvibe-billing.service](deploy/systemd/openvibe-billing.service) (state in--`/var/lib/openvibe-billing`), vhost [deploy/nginx/billing.openvibe.network.conf](deploy/nginx/billing.openvibe.network.conf).++[deploy/systemd/openvibe-billing.service](deploy/systemd/openvibe-billing.service), vhost [deploy/nginx/billing.openvibe.network.conf](deploy/nginx/billing.openvibe.network.conf).## Design@@@ -285,7 +284,7 @@ Reversing a subscription payment revokes the periods it granted.- **Payouts** are decided in the staff console (or with `billing.cashout.manage` on the API): pay at theprovider, then approve with its payout reference once the escrow has ended.- **Metrics**: `curl -s http://127.0.0.1:4600/metrics` on the host.--- **Backups**: `sqlite3 /var/lib/openvibe-billing/billing.db ".backup billing-$(date +%F).db"`.++- **Backups**: `pg_dump ov_billing > billing-$(date +%F).sql` (the host's data role, ADR-035).- **Mapping of Live's tables** to Billing's (including payouts, refunds and plans, which have no tableof their own on Live): [docs/live-mapping.md](docs/live-mapping.md).@@@ -328,10 +327,8 @@ Production deploys with `sudo ovhost deploy billing` on the host (strategy `git-fast-forward `/opt/openvibe.billing`, install on a lockfile change, restart, wait for `/api/ready`).The unit is `openvibe-billing.service` on `127.0.0.1:4600`, the env file `/etc/openvibe/billing.env`. The ledger is`ov_billing` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh billing` writes its settings);--the release migrates it at boot. The one-time move from SQLite is `scripts/migrate-to-postgres.js` (openvibe-sdk--`runSqliteMigration`, with a `--pglite` rehearsal mode; it refuses to empty a PostgreSQL journal that already has--transactions), run while the service is stopped and frozen; the old `/var/lib/openvibe-billing/billing.db` stays--read-only for 7 days as the rollback. nginx serves `billing.openvibe.network` from++the release applies `migrations/` at boot as the owner (`DATABASE_DIRECT_URL`), and every read and write is async++through `openvibe-sdk/db`. nginx serves `billing.openvibe.network` from[deploy/nginx/billing.openvibe.network.conf](deploy/nginx/billing.openvibe.network.conf) (console, `/policy`,`/webhooks/*`, `/api/health`). Freeze the economy before any risky change (below).Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the@@@ -19,11 +19,10 @@ Environment=PATH=/usr/local/bin:/usr/bin:/binExecStart=/usr/bin/env node /opt/openvibe.billing/server/index.jsRestart=alwaysRestartSec=5--# /var/lib/openvibe-billing keeps the SQLite ledger the service moved from (read-only after the switch: the rollback--# for 7 days; scripts/migrate-to-postgres.js reads it); the code tree stays read-only.++# /var/lib/openvibe-billing holds the Live snapshot the cutover runbook copies in for the import++# (docs/live-cutover.md); the code tree stays read-only.StateDirectory=openvibe-billingStateDirectoryMode=0700--Environment=BILLING_DB_PATH=/var/lib/openvibe-billing/billing.dbUMask=0077NoNewPrivileges=truePrivateTmp=true@@@ -47,7 +47,7 @@ are non-zero.1. **Non-atomic donate.** `donate()` deducts, credits and records in three separate statements with notransaction (`vibes.js:111-128`); `deductVibes` is read-then-write (`database.js:5393-5399`), so twoconcurrent donations can both pass the check. A crash between the deduct and the credit loses money.-- *Billing:* every operation is one SQLite transaction with the funds check inside it.++ *Billing:* every operation is one PostgreSQL transaction with the funds check inside it.2. **Best-effort, mutable ledger.** The ledger row is written after the balances move and failures areswallowed (`payments.js:262-268`, `powerchat-checkout.js:337-343`); rows are later UPDATEd(`vibes.js:204`, `:218`); subscription payments and streamer shares write no ledger row at all@@@ -128,8 +128,8 @@ Shell helpers used below (on the host):```bash# billing '<command>' runs one command with Billing's environment, as Billing's service user (ubuntu)--# so the database files keep their owner — the same way the shadow imports were run.--billing() { sudo bash -c "set -a; . /etc/openvibe/billing.env; set +a; export NODE_ENV=production BILLING_DB_PATH=/var/lib/openvibe-billing/billing.db; cd /opt/openvibe.billing && sudo -E -u ubuntu $1"; }++# so the Live snapshot files keep their owner — the same way the shadow imports were run.++billing() { sudo bash -c "set -a; . /etc/openvibe/billing.env; set +a; export NODE_ENV=production; cd /opt/openvibe.billing && sudo -E -u ubuntu $1"; }LIVE_DB=/opt/openvibe.live/data/live.db # production runs Live's legacy layout (a git checkout); there is no shared/# /var/backups/openvibe is root-only (0700), so Billing's user cannot read a snapshot there: the import reads# its copy from Billing's own state directory, and a second copy stays in /var/backups/openvibe for rollback.@@@ -152,8 +152,8 @@ Live owner calls are made from the browser console on openvibe.live, signed in aA `paid` row is an anomaly to fix first; a `pending` one is carried into Billing as an intent andsettles there if it is ever paid (canonical, un-minted links do not expire).3. **Back up and freeze Billing** so it holds PowerChat deliveries until the final import is in:-- `billing "sqlite3 /var/lib/openvibe-billing/billing.db '.backup /var/lib/openvibe-billing/billing-pre-cutover.db'"`-- (kept for rollback), then `billing "node scripts/freeze.js on 'Live cutover'"` → `frozen: true`.++ take a PostgreSQL snapshot of `ov_billing` (`pg_dump`, kept in `/var/lib/openvibe-billing` for rollback),++ then `billing "node scripts/freeze.js on 'Live cutover'"` → `frozen: true`.Then make Billing the authority for EXTERNAL tips: add `BILLING_AUTHORITY=billing` to`/etc/openvibe/billing.env` and `sudo systemctl restart openvibe-billing`. The freeze is kept in thedatabase, so it survives the restart. `curl -s http://127.0.0.1:4600/api/health` →@@@ -238,8 +238,8 @@ which money moved where.`TIPS_CHAT_ADAPTER=none` in tips.env and restart Tips; re-point PowerChat to Live; unfreeze Live.The billing.env `BILLING_AUTHORITY` goes too, with the Billing restore below. The stragglers Billingsettled are still pending orders on Live and are backfilled by Live's reconciler from PowerChat's-- paid-messages feed — so Billing must forget them: stop `openvibe-billing`, restore `billing-pre-cutover.db` from step 3 over `billing.db` (remove the-- `-wal`/`-shm` files), freeze it (`billing "node scripts/freeze.js on 'rollback'"`), start it again.++ paid-messages feed — so Billing must forget them: stop `openvibe-billing`, restore the step-3 PostgreSQL++ snapshot over `ov_billing`, freeze it (`billing "node scripts/freeze.js on 'rollback'"`), start it again.Otherwise a later attempt would count those payments twice (Live's credit arrives through the import,Billing's own settlement stays).- **After step 9** (people have moved money in Billing): ADR-012's rollback — freeze Billing and Live,@@@ -15,7 +15,6 @@* identity.subject.resolve). The report is printed and stored in import_runs; a reconciliation run* follows every real import. Safe to re-run: a second run over the same snapshot changes nothing.*/--const path = require('path');const Database = require('better-sqlite3');const { loadConfig } = require('../server/config');const { openDb } = require('../server/db');@@@ -32,7 +31,6 @@ async function main() {const livePath = opt('live-db');if (!livePath) { console.error('usage: import-live.js --live-db <snapshot path> [--dry-run] [--json]'); process.exit(2); }const config = loadConfig();-- if (path.resolve(livePath) === path.resolve(config.dbPath)) throw new Error('--live-db points at the Billing database');const live = new Database(livePath, { readonly: true, fileMustExist: true });const db = await openDb(config);const ctx = { db, config, rates: createRates(config.rates), now: () => Date.now(), log: console };ddeleted file mode 100755@@@ -1,42 +0,0 @@--#!/usr/bin/env node--'use strict';--/**-- * The one-time move of Billing's SQLite ledger (BILLING_DB_PATH) into its PostgreSQL schema (ADR-035; the procedure is-- * openvibe-sdk docs/migrating-to-postgresql.md, section 6, carried out by openvibe-sdk/db runSqliteMigration).-- *-- * node scripts/migrate-to-postgres.js [--sqlite <file>] [--pglite] [--json] [--replace-ledger]-- *-- * Applies migrations/ as the owner (DATABASE_DIRECT_URL), copies every table into emptied tables, verifies row counts-- * and checksums, and exits 1 unless everything verified. The SQLite file is opened read-only. Every table keeps its-- * name and columns; the append-only triggers guard UPDATE and DELETE, so the TRUNCATE that empties a table and the-- * copy's INSERTs pass them.-- */--const { runSqliteMigration } = require('openvibe-sdk/db');--const { loadConfig } = require('../server/config');--const { MIGRATIONS } = require('../server/db');----const TABLES = {};----/**-- * The copy empties its target tables first (a rehearsal can be repeated). Once PostgreSQL holds a journal of its own,-- * that would erase money records: it refuses unless --replace-ledger says the rows there are to go.-- */--async function refuseOverLiveLedger(directUrl) {-- if (!directUrl || process.argv.includes('--pglite') || process.argv.includes('--replace-ledger')) return;-- const { createDb } = require('openvibe-sdk/db');-- const db = createDb({ url: directUrl, service: 'billing-migrate-check', max: 1 });-- try {-- const t = await db.prepare("SELECT to_regclass('transactions') IS NOT NULL AS present").get();-- const n = t.present ? Number((await db.prepare('SELECT COUNT(*) AS n FROM transactions').get()).n) : 0;-- if (n > 0) throw new Error(`PostgreSQL already holds ${n} ledger transaction(s): refusing to replace them (--replace-ledger to do it anyway)`);-- } finally { await db.close(); }--}----if (require.main === module) {-- const config = loadConfig();-- refuseOverLiveLedger(config.db.directUrl)-- .then(() => runSqliteMigration({ service: 'billing', sqlite: config.dbPath, directUrl: config.db.directUrl, migrations: MIGRATIONS, tables: TABLES }))-- .then((code) => process.exit(code), (err) => { console.error(`migrate-to-postgres failed: ${err.message}`); process.exit(1); });--}----module.exports = { TABLES };@@@ -69,11 +69,9 @@ function loadConfig(env = process.env) {host: env.HOST || '127.0.0.1',baseUrl,trustProxy: env.TRUST_PROXY != null ? Number(env.TRUST_PROXY) : 1,-- // PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner). dbPath: the SQLite-- // file of releases before PostgreSQL, read once by scripts/migrate-to-postgres.js.++ // PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner).db: { url: env.DATABASE_URL || '', directUrl: env.DATABASE_DIRECT_URL || '' },valkey: { url: env.VALKEY_URL || '', prefix: env.VALKEY_PREFIX || 'ov:billing:' },-- dbPath: env.BILLING_DB_PATH || './data/billing.db',// 'live' (default, shadow): EXTERNAL PowerChat tips are recorded but not announced, because// Live announces them from its own webhook. 'billing': they are announced as// billing.receipt.external (OpenVibe.Tips turns that into the chat line, alert and goal).@@@ -18,7 +18,6 @@ async function boot(opts = {}) {const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'billing-test-'));const env = {NODE_ENV: 'test',-- BILLING_DB_PATH: path.join(dir, 'billing.db'),OV_NETWORK_INTERNAL_URL: network.url,OV_NETWORK_ISSUER: network.url,OV_OAUTH_CLIENT_ID: 'billing',
@@ -18,9 +18,6 @@ DATABASE_URL=
@@ -18,9 +18,6 @@ DATABASE_URL=
DATABASE_DIRECT_URL=
DATABASE_DIRECT_URL=
# Valkey: the per-person limit counters, kept across restarts (optional; without it they count in the process).
# Valkey: the per-person limit counters, kept across restarts (optional; without it they count in the process).
VALKEY_URL=
VALKEY_URL=
-# The SQLite ledger the one-time move reads (scripts/migrate-to-postgres.js). The systemd unit sets
-# /var/lib/openvibe-billing/billing.db.
-BILLING_DB_PATH=./data/billing.db
# Who keeps the money books: live (default — OpenVibe.Live is the ledger and receives the PowerChat
# Who keeps the money books: live (default — OpenVibe.Live is the ledger and receives the PowerChat
# webhook; Billing runs in shadow) or billing (after the cutover, docs/live-cutover.md). Only EXTERNAL
# webhook; Billing runs in shadow) or billing (after the cutover, docs/live-cutover.md). Only EXTERNAL
@@ -78,8 +78,7 @@ node scripts/import-live.js --live-db <snapshot> --accounts-only # only PowerC
@@ -78,8 +78,7 @@ node scripts/import-live.js --live-db <snapshot> --accounts-only # only PowerC
```
```
Production: `/opt/openvibe.billing`, env `/etc/openvibe/billing.env`, unit
Production: `/opt/openvibe.billing`, env `/etc/openvibe/billing.env`, unit
-[deploy/systemd/openvibe-billing.service](deploy/systemd/openvibe-billing.service) (state in
-`/var/lib/openvibe-billing`), vhost [deploy/nginx/billing.openvibe.network.conf](deploy/nginx/billing.openvibe.network.conf).
+[deploy/systemd/openvibe-billing.service](deploy/systemd/openvibe-billing.service), vhost [deploy/nginx/billing.openvibe.network.conf](deploy/nginx/billing.openvibe.network.conf).
## Design
## Design
@@ -285,7 +284,7 @@ Reversing a subscription payment revokes the periods it granted.
@@ -285,7 +284,7 @@ Reversing a subscription payment revokes the periods it granted.
- **Payouts** are decided in the staff console (or with `billing.cashout.manage` on the API): pay at the
- **Payouts** are decided in the staff console (or with `billing.cashout.manage` on the API): pay at the
provider, then approve with its payout reference once the escrow has ended.
provider, then approve with its payout reference once the escrow has ended.
- **Metrics**: `curl -s http://127.0.0.1:4600/metrics` on the host.
- **Metrics**: `curl -s http://127.0.0.1:4600/metrics` on the host.
-- **Backups**: `sqlite3 /var/lib/openvibe-billing/billing.db ".backup billing-$(date +%F).db"`.
+- **Backups**: `pg_dump ov_billing > billing-$(date +%F).sql` (the host's data role, ADR-035).
- **Mapping of Live's tables** to Billing's (including payouts, refunds and plans, which have no table
- **Mapping of Live's tables** to Billing's (including payouts, refunds and plans, which have no table
of their own on Live): [docs/live-mapping.md](docs/live-mapping.md).
of their own on Live): [docs/live-mapping.md](docs/live-mapping.md).
@@ -328,10 +327,8 @@ Production deploys with `sudo ovhost deploy billing` on the host (strategy `git-
@@ -328,10 +327,8 @@ Production deploys with `sudo ovhost deploy billing` on the host (strategy `git-
fast-forward `/opt/openvibe.billing`, install on a lockfile change, restart, wait for `/api/ready`).
fast-forward `/opt/openvibe.billing`, install on a lockfile change, restart, wait for `/api/ready`).
The unit is `openvibe-billing.service` on `127.0.0.1:4600`, the env file `/etc/openvibe/billing.env`. The ledger is
The unit is `openvibe-billing.service` on `127.0.0.1:4600`, the env file `/etc/openvibe/billing.env`. The ledger is
`ov_billing` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh billing` writes its settings);
`ov_billing` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh billing` writes its settings);
-the release migrates it at boot. The one-time move from SQLite is `scripts/migrate-to-postgres.js` (openvibe-sdk
-`runSqliteMigration`, with a `--pglite` rehearsal mode; it refuses to empty a PostgreSQL journal that already has
-transactions), run while the service is stopped and frozen; the old `/var/lib/openvibe-billing/billing.db` stays
-read-only for 7 days as the rollback. nginx serves `billing.openvibe.network` from
+the release applies `migrations/` at boot as the owner (`DATABASE_DIRECT_URL`), and every read and write is async
+through `openvibe-sdk/db`. nginx serves `billing.openvibe.network` from
[deploy/nginx/billing.openvibe.network.conf](deploy/nginx/billing.openvibe.network.conf) (console, `/policy`,
[deploy/nginx/billing.openvibe.network.conf](deploy/nginx/billing.openvibe.network.conf) (console, `/policy`,
`/webhooks/*`, `/api/health`). Freeze the economy before any risky change (below).
`/webhooks/*`, `/api/health`). Freeze the economy before any risky change (below).
Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the
Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the
@@ -19,11 +19,10 @@ Environment=PATH=/usr/local/bin:/usr/bin:/bin
@@ -19,11 +19,10 @@ Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/env node /opt/openvibe.billing/server/index.js
ExecStart=/usr/bin/env node /opt/openvibe.billing/server/index.js
Restart=always
Restart=always
RestartSec=5
RestartSec=5
-# /var/lib/openvibe-billing keeps the SQLite ledger the service moved from (read-only after the switch: the rollback
-# for 7 days; scripts/migrate-to-postgres.js reads it); the code tree stays read-only.
+# /var/lib/openvibe-billing holds the Live snapshot the cutover runbook copies in for the import
+# (docs/live-cutover.md); the code tree stays read-only.
StateDirectory=openvibe-billing
StateDirectory=openvibe-billing
StateDirectoryMode=0700
StateDirectoryMode=0700
-Environment=BILLING_DB_PATH=/var/lib/openvibe-billing/billing.db
UMask=0077
UMask=0077
NoNewPrivileges=true
NoNewPrivileges=true
PrivateTmp=true
PrivateTmp=true
@@ -47,7 +47,7 @@ are non-zero.
@@ -47,7 +47,7 @@ are non-zero.
1. **Non-atomic donate.** `donate()` deducts, credits and records in three separate statements with no
1. **Non-atomic donate.** `donate()` deducts, credits and records in three separate statements with no
transaction (`vibes.js:111-128`); `deductVibes` is read-then-write (`database.js:5393-5399`), so two
transaction (`vibes.js:111-128`); `deductVibes` is read-then-write (`database.js:5393-5399`), so two
concurrent donations can both pass the check. A crash between the deduct and the credit loses money.
concurrent donations can both pass the check. A crash between the deduct and the credit loses money.
- *Billing:* every operation is one SQLite transaction with the funds check inside it.
+ *Billing:* every operation is one PostgreSQL transaction with the funds check inside it.
2. **Best-effort, mutable ledger.** The ledger row is written after the balances move and failures are
2. **Best-effort, mutable ledger.** The ledger row is written after the balances move and failures are
swallowed (`payments.js:262-268`, `powerchat-checkout.js:337-343`); rows are later UPDATEd
swallowed (`payments.js:262-268`, `powerchat-checkout.js:337-343`); rows are later UPDATEd
(`vibes.js:204`, `:218`); subscription payments and streamer shares write no ledger row at all
(`vibes.js:204`, `:218`); subscription payments and streamer shares write no ledger row at all
@@ -128,8 +128,8 @@ Shell helpers used below (on the host):
@@ -128,8 +128,8 @@ Shell helpers used below (on the host):
```bash
```bash
# billing '<command>' runs one command with Billing's environment, as Billing's service user (ubuntu)
# billing '<command>' runs one command with Billing's environment, as Billing's service user (ubuntu)
-# so the database files keep their owner — the same way the shadow imports were run.
-billing() { sudo bash -c "set -a; . /etc/openvibe/billing.env; set +a; export NODE_ENV=production BILLING_DB_PATH=/var/lib/openvibe-billing/billing.db; cd /opt/openvibe.billing && sudo -E -u ubuntu $1"; }
+# so the Live snapshot files keep their owner — the same way the shadow imports were run.
+billing() { sudo bash -c "set -a; . /etc/openvibe/billing.env; set +a; export NODE_ENV=production; cd /opt/openvibe.billing && sudo -E -u ubuntu $1"; }
LIVE_DB=/opt/openvibe.live/data/live.db # production runs Live's legacy layout (a git checkout); there is no shared/
LIVE_DB=/opt/openvibe.live/data/live.db # production runs Live's legacy layout (a git checkout); there is no shared/
# /var/backups/openvibe is root-only (0700), so Billing's user cannot read a snapshot there: the import reads
# /var/backups/openvibe is root-only (0700), so Billing's user cannot read a snapshot there: the import reads
# its copy from Billing's own state directory, and a second copy stays in /var/backups/openvibe for rollback.
# its copy from Billing's own state directory, and a second copy stays in /var/backups/openvibe for rollback.
@@ -152,8 +152,8 @@ Live owner calls are made from the browser console on openvibe.live, signed in a
@@ -152,8 +152,8 @@ Live owner calls are made from the browser console on openvibe.live, signed in a
A `paid` row is an anomaly to fix first; a `pending` one is carried into Billing as an intent and
A `paid` row is an anomaly to fix first; a `pending` one is carried into Billing as an intent and
settles there if it is ever paid (canonical, un-minted links do not expire).
settles there if it is ever paid (canonical, un-minted links do not expire).
3. **Back up and freeze Billing** so it holds PowerChat deliveries until the final import is in:
3. **Back up and freeze Billing** so it holds PowerChat deliveries until the final import is in:
- `billing "sqlite3 /var/lib/openvibe-billing/billing.db '.backup /var/lib/openvibe-billing/billing-pre-cutover.db'"`
- (kept for rollback), then `billing "node scripts/freeze.js on 'Live cutover'"` → `frozen: true`.
+ take a PostgreSQL snapshot of `ov_billing` (`pg_dump`, kept in `/var/lib/openvibe-billing` for rollback),
+ then `billing "node scripts/freeze.js on 'Live cutover'"` → `frozen: true`.
Then make Billing the authority for EXTERNAL tips: add `BILLING_AUTHORITY=billing` to
Then make Billing the authority for EXTERNAL tips: add `BILLING_AUTHORITY=billing` to
`/etc/openvibe/billing.env` and `sudo systemctl restart openvibe-billing`. The freeze is kept in the
`/etc/openvibe/billing.env` and `sudo systemctl restart openvibe-billing`. The freeze is kept in the
database, so it survives the restart. `curl -s http://127.0.0.1:4600/api/health` →
database, so it survives the restart. `curl -s http://127.0.0.1:4600/api/health` →
@@ -238,8 +238,8 @@ which money moved where.
@@ -238,8 +238,8 @@ which money moved where.
`TIPS_CHAT_ADAPTER=none` in tips.env and restart Tips; re-point PowerChat to Live; unfreeze Live.
`TIPS_CHAT_ADAPTER=none` in tips.env and restart Tips; re-point PowerChat to Live; unfreeze Live.
The billing.env `BILLING_AUTHORITY` goes too, with the Billing restore below. The stragglers Billing
The billing.env `BILLING_AUTHORITY` goes too, with the Billing restore below. The stragglers Billing
settled are still pending orders on Live and are backfilled by Live's reconciler from PowerChat's
settled are still pending orders on Live and are backfilled by Live's reconciler from PowerChat's
- paid-messages feed — so Billing must forget them: stop `openvibe-billing`, restore `billing-pre-cutover.db` from step 3 over `billing.db` (remove the
- `-wal`/`-shm` files), freeze it (`billing "node scripts/freeze.js on 'rollback'"`), start it again.
+ paid-messages feed — so Billing must forget them: stop `openvibe-billing`, restore the step-3 PostgreSQL
+ snapshot over `ov_billing`, freeze it (`billing "node scripts/freeze.js on 'rollback'"`), start it again.
Otherwise a later attempt would count those payments twice (Live's credit arrives through the import,
Otherwise a later attempt would count those payments twice (Live's credit arrives through the import,
Billing's own settlement stays).
Billing's own settlement stays).
- **After step 9** (people have moved money in Billing): ADR-012's rollback — freeze Billing and Live,
- **After step 9** (people have moved money in Billing): ADR-012's rollback — freeze Billing and Live,
@@ -15,7 +15,6 @@
@@ -15,7 +15,6 @@
* identity.subject.resolve). The report is printed and stored in import_runs; a reconciliation run
* identity.subject.resolve). The report is printed and stored in import_runs; a reconciliation run
* follows every real import. Safe to re-run: a second run over the same snapshot changes nothing.
* follows every real import. Safe to re-run: a second run over the same snapshot changes nothing.
*/
*/
-const path = require('path');
const Database = require('better-sqlite3');
const Database = require('better-sqlite3');
const { loadConfig } = require('../server/config');
const { loadConfig } = require('../server/config');
const { openDb } = require('../server/db');
const { openDb } = require('../server/db');
@@ -32,7 +31,6 @@ async function main() {
@@ -32,7 +31,6 @@ async function main() {
const livePath = opt('live-db');
const livePath = opt('live-db');
if (!livePath) { console.error('usage: import-live.js --live-db <snapshot path> [--dry-run] [--json]'); process.exit(2); }
if (!livePath) { console.error('usage: import-live.js --live-db <snapshot path> [--dry-run] [--json]'); process.exit(2); }
const config = loadConfig();
const config = loadConfig();
- if (path.resolve(livePath) === path.resolve(config.dbPath)) throw new Error('--live-db points at the Billing database');
const live = new Database(livePath, { readonly: true, fileMustExist: true });
const live = new Database(livePath, { readonly: true, fileMustExist: true });
const db = await openDb(config);
const db = await openDb(config);
const ctx = { db, config, rates: createRates(config.rates), now: () => Date.now(), log: console };
const ctx = { db, config, rates: createRates(config.rates), now: () => Date.now(), log: console };
deleted file mode 100755
deleted file mode 100755
@@ -1,42 +0,0 @@
@@ -1,42 +0,0 @@
-#!/usr/bin/env node
-'use strict';
-/**
- * The one-time move of Billing's SQLite ledger (BILLING_DB_PATH) into its PostgreSQL schema (ADR-035; the procedure is
- * openvibe-sdk docs/migrating-to-postgresql.md, section 6, carried out by openvibe-sdk/db runSqliteMigration).
- *
- * node scripts/migrate-to-postgres.js [--sqlite <file>] [--pglite] [--json] [--replace-ledger]
- *
- * Applies migrations/ as the owner (DATABASE_DIRECT_URL), copies every table into emptied tables, verifies row counts
- * and checksums, and exits 1 unless everything verified. The SQLite file is opened read-only. Every table keeps its
- * name and columns; the append-only triggers guard UPDATE and DELETE, so the TRUNCATE that empties a table and the
- * copy's INSERTs pass them.
- */
-const { runSqliteMigration } = require('openvibe-sdk/db');
-const { loadConfig } = require('../server/config');
-const { MIGRATIONS } = require('../server/db');
-
-const TABLES = {};
-
-/**
- * The copy empties its target tables first (a rehearsal can be repeated). Once PostgreSQL holds a journal of its own,
- * that would erase money records: it refuses unless --replace-ledger says the rows there are to go.
- */
-async function refuseOverLiveLedger(directUrl) {
- if (!directUrl || process.argv.includes('--pglite') || process.argv.includes('--replace-ledger')) return;
- const { createDb } = require('openvibe-sdk/db');
- const db = createDb({ url: directUrl, service: 'billing-migrate-check', max: 1 });
- try {
- const t = await db.prepare("SELECT to_regclass('transactions') IS NOT NULL AS present").get();
- const n = t.present ? Number((await db.prepare('SELECT COUNT(*) AS n FROM transactions').get()).n) : 0;
- if (n > 0) throw new Error(`PostgreSQL already holds ${n} ledger transaction(s): refusing to replace them (--replace-ledger to do it anyway)`);
- } finally { await db.close(); }
-}
-
-if (require.main === module) {
- const config = loadConfig();
- refuseOverLiveLedger(config.db.directUrl)
- .then(() => runSqliteMigration({ service: 'billing', sqlite: config.dbPath, directUrl: config.db.directUrl, migrations: MIGRATIONS, tables: TABLES }))
- .then((code) => process.exit(code), (err) => { console.error(`migrate-to-postgres failed: ${err.message}`); process.exit(1); });
-}
-
-module.exports = { TABLES };
@@ -69,11 +69,9 @@ function loadConfig(env = process.env) {
@@ -69,11 +69,9 @@ function loadConfig(env = process.env) {
host: env.HOST || '127.0.0.1',
host: env.HOST || '127.0.0.1',
baseUrl,
baseUrl,
trustProxy: env.TRUST_PROXY != null ? Number(env.TRUST_PROXY) : 1,
trustProxy: env.TRUST_PROXY != null ? Number(env.TRUST_PROXY) : 1,
- // PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner). dbPath: the SQLite
- // file of releases before PostgreSQL, read once by scripts/migrate-to-postgres.js.
+ // PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner).
db: { url: env.DATABASE_URL || '', directUrl: env.DATABASE_DIRECT_URL || '' },
db: { url: env.DATABASE_URL || '', directUrl: env.DATABASE_DIRECT_URL || '' },
valkey: { url: env.VALKEY_URL || '', prefix: env.VALKEY_PREFIX || 'ov:billing:' },
valkey: { url: env.VALKEY_URL || '', prefix: env.VALKEY_PREFIX || 'ov:billing:' },
- dbPath: env.BILLING_DB_PATH || './data/billing.db',
// 'live' (default, shadow): EXTERNAL PowerChat tips are recorded but not announced, because
// 'live' (default, shadow): EXTERNAL PowerChat tips are recorded but not announced, because
// Live announces them from its own webhook. 'billing': they are announced as
// Live announces them from its own webhook. 'billing': they are announced as
// billing.receipt.external (OpenVibe.Tips turns that into the chat line, alert and goal).
// billing.receipt.external (OpenVibe.Tips turns that into the chat line, alert and goal).
@@ -18,7 +18,6 @@ async function boot(opts = {}) {
@@ -18,7 +18,6 @@ async function boot(opts = {}) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'billing-test-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'billing-test-'));
const env = {
const env = {
NODE_ENV: 'test',
NODE_ENV: 'test',
- BILLING_DB_PATH: path.join(dir, 'billing.db'),
OV_NETWORK_INTERNAL_URL: network.url,
OV_NETWORK_INTERNAL_URL: network.url,
OV_NETWORK_ISSUER: network.url,
OV_NETWORK_ISSUER: network.url,
OV_OAUTH_CLIENT_ID: 'billing',
OV_OAUTH_CLIENT_ID: 'billing',