openvibers/openvibe.billing Commits

Cashout events carry the payout method's type only: the address (e.g. a PayPal email) stays in Billing for the staff console instead of travelling on the retained event stream

OP OpenVibers committed 23 Sept 2026 · c6462798de · 2 files, +19 −3


Changes

ddiff --git a/server/ops/cashouts.js b/server/ops/cashouts.js
iindex 4620dc4..9ff34f9 100644
---- a/server/ops/cashouts.js
++++ b/server/ops/cashouts.js
@@@ -30,6 +30,17 @@ function present(c) {
};
}
++/**
++ * The cashout as an event carries it: the payout method's TYPE only. The address (e.g. a PayPal
++ * email) stays in Billing, where the staff console reads it; the event stream is retained and
++ * read by other services, so it never gets the creator's contact details.
++ */
++function eventView(c) {
++ const v = present(c);
++ if (v) v.payout_method = { type: (c.payout_method && c.payout_method.type) || null };
++ return v;
++}
++
function payoutMethod(v) {
if (!v || typeof v !== 'object') fail(422, 'billing.invalid_input', "payout_method is required, e.g. { type: 'paypal', address: '[email protected]' }");
const type = String(v.type || '').toLowerCase();
@@@ -63,7 +74,7 @@ function request(ctx, input) {
db.prepare(`INSERT INTO cashouts (id, subject, amount_bits, value_cents, status, payout_method, escrow_until, request_txn, created_at, updated_at)
VALUES (?, ?, ?, ?, 'requested', ?, ?, ?, ?, ?)`).run(id, input.subject, amount, rates.valueCents(amount), JSON.stringify(method), escrowUntil, txn.id, iso(ms), iso(ms));
const cashout = find(db, id);
-- enqueue(ctx, { event_type: 'billing.cashout.requested', subject: { type: 'cashout', id }, payload: { cashout: present(cashout), transaction_id: txn.id } });
++ enqueue(ctx, { event_type: 'billing.cashout.requested', subject: { type: 'cashout', id }, payload: { cashout: eventView(cashout), transaction_id: txn.id } });
return { cashout, replay: false };
})();
}
@@@ -96,7 +107,7 @@ function approve(ctx, input) {
db.prepare(`UPDATE cashouts SET status = 'paid', settle_txn = ?, payout_provider = ?, payout_reference = ?, decided_by = ?, updated_at = ? WHERE id = ?`)
.run(txn.id, provider, ref, JSON.stringify(input.actor || {}), iso(ctx.now()), c.id);
const cashout = find(db, c.id);
-- enqueue(ctx, { event_type: 'billing.cashout.paid', subject: { type: 'cashout', id: c.id }, payload: { cashout: present(cashout), transaction_id: txn.id } });
++ enqueue(ctx, { event_type: 'billing.cashout.paid', subject: { type: 'cashout', id: c.id }, payload: { cashout: eventView(cashout), transaction_id: txn.id } });
return { cashout, replay: false };
})();
}
@@@ -121,7 +132,7 @@ function deny(ctx, input) {
db.prepare(`UPDATE cashouts SET status = 'denied', settle_txn = ?, reason = ?, decided_by = ?, updated_at = ? WHERE id = ?`)
.run(txn.id, reason, JSON.stringify(input.actor || {}), iso(ctx.now()), c.id);
const cashout = find(db, c.id);
-- enqueue(ctx, { event_type: 'billing.cashout.denied', subject: { type: 'cashout', id: c.id }, payload: { cashout: present(cashout), transaction_id: txn.id } });
++ enqueue(ctx, { event_type: 'billing.cashout.denied', subject: { type: 'cashout', id: c.id }, payload: { cashout: eventView(cashout), transaction_id: txn.id } });
enqueue(ctx, { event_type: 'billing.transaction.reversed', subject: { type: 'transaction', id: txn.id }, payload: { ...summary(txn), reverses_txn: c.request_txn } });
return { cashout, replay: false };
})();
ddiff --git a/test/operations.test.js b/test/operations.test.js
iindex d0aa49c..66a5f83 100644
---- a/test/operations.test.js
++++ b/test/operations.test.js
@@@ -133,6 +133,11 @@ const { boot, fund, check, done } = require('./helpers/app');
const b = await t.balances(streamer.id);
assert.deepStrictEqual([b.payable, b.pending_payouts], [250, 1000]);
t.assertReconciled('after cashout request');
++ // The event stream gets the payout TYPE only, never the creator's address (a PayPal email).
++ const outboxText = JSON.stringify(t.db.prepare("SELECT event FROM outbox WHERE event LIKE '%billing.cashout.%'").all());
++ assert.ok(outboxText.includes('billing.cashout.requested'), 'the requested event is queued');
++ assert.ok(!outboxText.includes('[email protected]'), 'no payout address in any cashout event');
++ assert.strictEqual(cashout.payout_method.address, '[email protected]', 'the API itself still returns it to the caller');
});
await check('approval needs a payout reference and waits for the escrow period', async () => {