@bongos/core 1.24.22 → 1.24.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +218 -63
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +42 -0
- package/clients/bongos-client/index.cjs +42 -0
- package/clients/bongos-client/index.d.ts +55 -1
- package/clients/bongos-client/index.mjs +42 -0
- package/docs/api/openapi.json +1181 -207
- package/docs/api-reference.md +30 -5
- package/docs/architecture.md +42 -0
- package/docs/copy-inventory.md +18 -8
- package/docs/copy-registry.json +99 -9
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +64 -25
- package/migrations/core_291_payout_nda_tax.sql +75 -0
- package/migrations/core_292_module_sale_payouts.sql +170 -0
- package/migrations/core_293_cash_outs.sql +184 -0
- package/migrations/core_294_cash_out_attempts_sent_by_time.sql +21 -0
- package/modules/economy/ledger-check.js +360 -0
- package/modules/economy/migrations/economy_003_user_balances.sql +116 -0
- package/modules/economy/migrations/economy_004_module_share_payouts.sql +22 -0
- package/modules/economy/migrations/economy_005_project_account_holds_index.sql +20 -0
- package/modules/economy/migrations/economy_006_ledger_checks.sql +68 -0
- package/modules/economy/module.json +1 -1
- package/modules/economy/outflow.js +209 -0
- package/modules/economy/pollers/ledger-check.js +87 -0
- package/modules/economy/project-account.js +92 -7
- package/modules/economy/routes/balances.js +79 -0
- package/modules/economy/routes/project-account.js +40 -2
- package/modules/economy/user-balances.js +308 -0
- package/modules/government/decision-rules.js +1 -0
- package/modules/hall-ui/public/project-settings.css +18 -0
- package/modules/hall-ui/public/project-settings.html +15 -2
- package/modules/hall-ui/public/project-settings.js +142 -0
- package/modules/ideas/module.json +2 -1
- package/modules/ideas/routes/blockers.js +26 -0
- package/modules/lifecycle/db-claims.js +22 -0
- package/modules/lifecycle/db-ship.js +5 -5
- package/modules/lifecycle/funding-wall.js +253 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/claims.js +9 -0
- package/modules/lifecycle/routes/pay-type-gates.js +7 -0
- package/modules/lifecycle/routes/project-settings.js +59 -2
- package/modules/onboarding/founding-birth.js +5 -2
- package/modules/onboarding/founding-mode.js +10 -0
- package/modules/onboarding/genesis-home.js +3 -0
- package/modules/provisioning/birth.js +11 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +44 -0
- package/scripts/bongos/government-walk.js +1 -0
- package/scripts/bongos/plain-cards.js +18 -0
- package/scripts/bongos/run-unit-tests.js +28 -0
- package/src/bongos/builder-payouts.js +6 -1
- package/src/bongos/cash-outs.js +399 -0
- package/src/bongos/module-payouts.js +432 -0
- package/src/bongos/payout-approvals.js +5 -2
- package/src/bongos/payout-gates.js +44 -7
- package/src/bongos/project-settings.js +2 -0
- package/src/bongos/routes/module-payouts.js +114 -0
- package/src/bongos/routes/my-cash-outs.js +103 -0
- package/src/bongos/routes/payout-gates.js +42 -1
- package/src/bongos/routes.js +6 -0
- package/src/branding.js +3 -0
- package/src/module-api.js +1 -1
- package/tests/builder_payouts_db.mjs +1 -0
- package/tests/cash_outs.mjs +96 -0
- package/tests/cash_outs_db.mjs +205 -0
- package/tests/founding_birth.mjs +52 -12
- package/tests/founding_mode.mjs +5 -5
- package/tests/funded_project_pays_proof_db.mjs +181 -0
- package/tests/funding_wall.mjs +205 -0
- package/tests/funding_wall_db.mjs +174 -0
- package/tests/genesis_home.mjs +1 -1
- package/tests/government_act_alone_buildings.mjs +1 -0
- package/tests/ledger_check.mjs +189 -0
- package/tests/ledger_check_db.mjs +119 -0
- package/tests/module_payouts.mjs +295 -0
- package/tests/module_payouts_db.mjs +140 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/payout_gates.mjs +67 -6
- package/tests/payout_gates_db.mjs +23 -1
- package/tests/project_account_db.mjs +5 -2
- package/tests/project_outflow.mjs +103 -0
- package/tests/project_outflow_db.mjs +100 -0
- package/tests/provision_settings_apply.mjs +2 -1
- package/tests/provisioning_settings_env.mjs +7 -3
- package/tests/user_balances.mjs +241 -0
- package/tests/user_balances_db.mjs +137 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// tests/user_balances_db.mjs — task 1004907 (goal 1000092 N3). Real DB, TWO PROJECTS.
|
|
2
|
+
//
|
|
3
|
+
// Applies the REAL economy_002 and economy_003 migrations into two schemas, one per project
|
|
4
|
+
// (each project runs its own database, ADR 0100; a schema is that database's stand-in), and
|
|
5
|
+
// proves what Postgres itself refuses: a platform fee, a payment to yourself, one account
|
|
6
|
+
// owned by two people, a second instruction under one id, a second `sent` attempt, and any
|
|
7
|
+
// UPDATE or DELETE. Then the real transfer() moves money inside each project, and neither
|
|
8
|
+
// project's tables, accounts or processor ever see the other's.
|
|
9
|
+
//
|
|
10
|
+
// Self-skips without a reachable Postgres.
|
|
11
|
+
// Run: DATABASE_URL=... node tests/user_balances_db.mjs
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { strict as assert } from 'node:assert';
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const { Pool } = require('pg');
|
|
19
|
+
const U = require('../modules/economy/user-balances.js');
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const mig = (f) => readFileSync(join(here, '..', 'modules', 'economy', 'migrations', f), 'utf8');
|
|
22
|
+
const MIGRATIONS = [mig('economy_002_project_account.sql'), mig('economy_003_user_balances.sql'), mig('economy_004_module_share_payouts.sql')];
|
|
23
|
+
|
|
24
|
+
const pgPool = new Pool({ connectionString: process.env.DATABASE_URL || undefined, max: 2 });
|
|
25
|
+
let probe;
|
|
26
|
+
try { probe = await pgPool.connect(); } catch (e) {
|
|
27
|
+
console.log(`user_balances_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
28
|
+
await pgPool.end().catch(() => {});
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
probe.release();
|
|
32
|
+
|
|
33
|
+
let pass = 0;
|
|
34
|
+
const ok = (c, m) => { assert.ok(c, m); pass++; console.log(` ok - ${m}`); };
|
|
35
|
+
const SCHEMAS = { A: `t1004907a_${process.pid}`, B: `t1004907b_${process.pid}` };
|
|
36
|
+
const clients = {};
|
|
37
|
+
|
|
38
|
+
// one connection per project, pinned to that project's schema
|
|
39
|
+
async function openProject(name) {
|
|
40
|
+
const c = await pgPool.connect();
|
|
41
|
+
const schema = SCHEMAS[name];
|
|
42
|
+
await c.query(`CREATE SCHEMA ${schema}`);
|
|
43
|
+
await c.query(`SET search_path TO ${schema}`);
|
|
44
|
+
await c.query('CREATE TABLE builders (id bigserial PRIMARY KEY, display_name text)');
|
|
45
|
+
await c.query('CREATE TABLE tasks (id bigserial PRIMARY KEY, title text)');
|
|
46
|
+
await c.query('CREATE TABLE schema_migrations (version text PRIMARY KEY)');
|
|
47
|
+
await c.query(`INSERT INTO builders (display_name) VALUES ('${name}-one'), ('${name}-two'), ('${name}-three')`);
|
|
48
|
+
for (const m of MIGRATIONS) await c.query(m);
|
|
49
|
+
clients[name] = c;
|
|
50
|
+
return c;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function processorFor(name, opened) {
|
|
54
|
+
const calls = [];
|
|
55
|
+
let n = 0;
|
|
56
|
+
return {
|
|
57
|
+
calls,
|
|
58
|
+
async confirmMemberAccount({ accountRef, builderId }) { return opened[accountRef] === builderId; },
|
|
59
|
+
async memberTransfer(req) {
|
|
60
|
+
calls.push(req);
|
|
61
|
+
for (const acc of [req.fromAccount, req.toAccount].filter(Boolean)) if (!(acc in opened)) throw new Error(`${name} does not hold ${acc}`);
|
|
62
|
+
return { ref: `tr_${name}_${++n}` };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const rejects = async (c, sql, params) => {
|
|
68
|
+
await c.query('BEGIN');
|
|
69
|
+
let err = null;
|
|
70
|
+
try { await c.query(sql, params); } catch (e) { err = e; }
|
|
71
|
+
await c.query('ROLLBACK');
|
|
72
|
+
return err;
|
|
73
|
+
};
|
|
74
|
+
const codeOf = async (p) => { try { await p; } catch (e) { return e.code; } return null; };
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const a = await openProject('A');
|
|
78
|
+
const b = await openProject('B');
|
|
79
|
+
const pa = processorFor('A', { acct_a1: '1', acct_a2: '2' });
|
|
80
|
+
const pb = processorFor('B', { acct_b1: '1', acct_b3: '3' });
|
|
81
|
+
await U.linkMemberAccount(a, { builderId: '1', accountRef: 'acct_a1' }, { processor: pa });
|
|
82
|
+
await U.linkMemberAccount(a, { builderId: '2', accountRef: 'acct_a2' }, { processor: pa });
|
|
83
|
+
await U.linkMemberAccount(b, { builderId: '1', accountRef: 'acct_b1' }, { processor: pb });
|
|
84
|
+
await U.linkMemberAccount(b, { builderId: '3', accountRef: 'acct_b3' }, { processor: pb });
|
|
85
|
+
|
|
86
|
+
const TI = `INSERT INTO economy_user_transfers (kind, from_builder_id, to_builder_id, from_account, to_account, amount_cents, instruction_id, platform_fee_cents)
|
|
87
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)`;
|
|
88
|
+
ok((await rejects(a, TI, ['pay_user', 1, 2, 'acct_a1', 'acct_a2', 100, 'fee-00001', 1]))?.code === '23514', 'a platform fee is refused by the database');
|
|
89
|
+
ok((await rejects(a, TI, ['pay_user', 1, 1, 'acct_a1', 'acct_a1', 100, 'self-0001', 0]))?.code === '23514', 'a payment to yourself is refused');
|
|
90
|
+
ok((await rejects(a, TI, ['fund_project', 1, 2, 'acct_a1', 'acct_a2', 100, 'fund-0001', 0]))?.code === '23514', 'funding that names a person is refused');
|
|
91
|
+
ok((await rejects(a, TI, ['pay_user', 1, 2, 'acct_a1', 'acct_a2', 0, 'zero-0001', 0]))?.code === '23514', 'a zero amount is refused');
|
|
92
|
+
ok((await rejects(a, 'INSERT INTO economy_member_accounts (builder_id, processor_account) VALUES (3, $1)', ['acct_a1']))?.code === '23505',
|
|
93
|
+
'one account cannot belong to two people');
|
|
94
|
+
ok(await codeOf(U.linkMemberAccount(a, { builderId: '3', accountRef: 'acct_b3' }, { processor: pa })) === 'not_this_projects_account',
|
|
95
|
+
"project A cannot link an account project B opened");
|
|
96
|
+
|
|
97
|
+
const paid = await U.transfer({ kind: 'pay_user', fromBuilderId: '1', toBuilderId: '2', amountCents: 2500, instructionId: 'pay-a-0001' }, { pool: a, processor: pa });
|
|
98
|
+
ok(paid.status === 'sent' && paid.platform_fee_cents === 0, 'a member pays another member in project A, fee-free');
|
|
99
|
+
const again = await U.transfer({ kind: 'pay_user', fromBuilderId: '1', toBuilderId: '2', amountCents: 2500, instructionId: 'pay-a-0001' }, { pool: a, processor: pa });
|
|
100
|
+
ok(again.already === true && pa.calls.length === 1, 'the same instruction is carried out once');
|
|
101
|
+
ok((await rejects(a, TI, ['pay_user', 1, 2, 'acct_a1', 'acct_a2', 9, 'pay-a-0001', 0]))?.code === '23505', 'a second instruction under one id is refused');
|
|
102
|
+
ok((await rejects(a, "INSERT INTO economy_user_transfer_attempts (transfer_id, outcome, processor_ref) VALUES ($1, 'sent', 'tr_dup')", [paid.transfer_id]))?.code === '23505',
|
|
103
|
+
'a second sent attempt is refused');
|
|
104
|
+
|
|
105
|
+
const funded = await U.transfer({ kind: 'fund_project', fromBuilderId: '1', amountCents: 1000, instructionId: 'fund-b-0001' }, { pool: b, processor: pb });
|
|
106
|
+
ok(funded.status === 'sent', 'a member funds project B');
|
|
107
|
+
|
|
108
|
+
for (const table of ['economy_member_accounts', 'economy_user_transfers', 'economy_user_transfer_attempts']) {
|
|
109
|
+
ok(/append-only/.test((await rejects(a, `UPDATE ${table} SET created_at = now()`))?.message || ''), `${table} refuses an UPDATE`);
|
|
110
|
+
ok(/append-only/.test((await rejects(a, `DELETE FROM ${table}`))?.message || ''), `${table} refuses a DELETE`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ISOLATION — each project's tables hold only its own movements
|
|
114
|
+
const count = async (c, sql, params) => Number((await c.query(sql, params)).rows[0].n);
|
|
115
|
+
ok(await count(a, 'SELECT count(*) AS n FROM economy_user_transfers') === 1 && await count(b, 'SELECT count(*) AS n FROM economy_user_transfers') === 1,
|
|
116
|
+
'each project records only its own instruction');
|
|
117
|
+
ok(await count(a, 'SELECT count(*) AS n FROM economy_project_account_movements') === 0, "project B's funding is not on project A's account");
|
|
118
|
+
ok(await count(b, "SELECT count(*) AS n FROM economy_project_account_movements WHERE kind = 'funding' AND builder_id = $1", ['1']) === 1,
|
|
119
|
+
"project B's funding is on project B's account, naming the funder");
|
|
120
|
+
ok(await codeOf(U.transfer({ kind: 'pay_user', fromBuilderId: '1', toBuilderId: '3', amountCents: 5, instructionId: 'cross-0001' }, { pool: a, processor: pa })) === 'recipient_has_no_account',
|
|
121
|
+
"project A cannot pay project B's member");
|
|
122
|
+
ok(pa.calls.every((c) => c.fromAccount.startsWith('acct_a') && (!c.toAccount || c.toAccount.startsWith('acct_a'))) &&
|
|
123
|
+
pb.calls.every((c) => c.fromAccount.startsWith('acct_b') && (!c.toAccount || c.toAccount.startsWith('acct_b'))),
|
|
124
|
+
"each project's processor is only ever asked to move that project's accounts");
|
|
125
|
+
const meA = await U.readMyBalance(a, '1', {});
|
|
126
|
+
const meB = await U.readMyBalance(b, '1', {});
|
|
127
|
+
ok(meA.transfers.length === 1 && meA.transfers[0].kind === 'pay_user' && meB.transfers.length === 1 && meB.transfers[0].kind === 'fund_project',
|
|
128
|
+
"the same person sees only that project's payments in each project");
|
|
129
|
+
|
|
130
|
+
console.log(`user_balances_db.mjs: ${pass} passed`);
|
|
131
|
+
} finally {
|
|
132
|
+
for (const [name, c] of Object.entries(clients)) {
|
|
133
|
+
await c.query(`DROP SCHEMA IF EXISTS ${SCHEMAS[name]} CASCADE`).catch(() => {});
|
|
134
|
+
c.release();
|
|
135
|
+
}
|
|
136
|
+
await pgPool.end();
|
|
137
|
+
}
|