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
Changes
@@@ -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 };})();@@@ -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 () => {
@@ -30,6 +30,17 @@ function present(c) {
@@ -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) {
function payoutMethod(v) {
if (!v || typeof v !== 'object') fail(422, 'billing.invalid_input', "payout_method is required, e.g. { type: 'paypal', address: '[email protected]' }");
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();
const type = String(v.type || '').toLowerCase();
@@ -63,7 +74,7 @@ function request(ctx, input) {
@@ -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)
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));
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);
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 };
return { cashout, replay: false };
})();
})();
}
}
@@ -96,7 +107,7 @@ function approve(ctx, input) {
@@ -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 = ?`)
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);
.run(txn.id, provider, ref, JSON.stringify(input.actor || {}), iso(ctx.now()), c.id);
const cashout = find(db, 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 };
return { cashout, replay: false };
})();
})();
}
}
@@ -121,7 +132,7 @@ function deny(ctx, input) {
@@ -121,7 +132,7 @@ function deny(ctx, input) {
db.prepare(`UPDATE cashouts SET status = 'denied', settle_txn = ?, reason = ?, decided_by = ?, updated_at = ? WHERE id = ?`)
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);
.run(txn.id, reason, JSON.stringify(input.actor || {}), iso(ctx.now()), c.id);
const cashout = find(db, 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 } });
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 };
return { cashout, replay: false };
})();
})();
@@ -133,6 +133,11 @@ const { boot, fund, check, done } = require('./helpers/app');
@@ -133,6 +133,11 @@ const { boot, fund, check, done } = require('./helpers/app');
const b = await t.balances(streamer.id);
const b = await t.balances(streamer.id);
assert.deepStrictEqual([b.payable, b.pending_payouts], [250, 1000]);
assert.deepStrictEqual([b.payable, b.pending_payouts], [250, 1000]);
t.assertReconciled('after cashout request');
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 () => {
await check('approval needs a payout reference and waits for the escrow period', async () => {