openvibers/openvibe.live Commits

PowerChat reconcile: treat source=manual_test rows as test money

OP OpenVibers committed 27 Aug 2026 · f0f6b6fe9d · 2 files, +10 −3

Verified against the live instance: /paid-messages rows carry no isTest
field but include source='manual_test' rows (the no-payment-provider test
tip path) with our appExternalRef attached. PowerChat's own webhook rule is
isTest = event.isTest || source === 'manual_test'; the feed only filters
the column. Mirror the webhook rule so a test tip is never reconciled as
real money when test fulfillment is off.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017AyeSFu7qdLdR64Zv99QNm

Changes

ddiff --git a/server/integrations/powerchat-reconcile.js b/server/integrations/powerchat-reconcile.js
iindex a105ed14..33c9fe57 100644
---- a/server/integrations/powerchat-reconcile.js
++++ b/server/integrations/powerchat-reconcile.js
@@@ -133,14 +133,19 @@ async function reconcileOnce() {
const order = orders.get(ref);
if (!order) continue; // already credited, or not on this sweep's list
if (row.source === 'developer_app') continue; // our own forwarded tip echo, never money in
-- if (row.isTest && !allowTest) { summary.skippedTest++; continue; }
++ // /paid-messages rows carry NO isTest field, yet include source='manual_test'
++ // rows (the no-payment-provider test tip path). PowerChat's own webhook
++ // rule is isTest = event.isTest || source === 'manual_test' — mirror it, or
++ // a test tip would be reconciled as real money on a production PowerChat.
++ const isTest = !!row.isTest || row.source === 'manual_test';
++ if (isTest && !allowTest) { summary.skippedTest++; continue; }
// Same shape the webhook hands to the checkout module (money in integer cents).
const data = {
eventId: row.eventId, occurredAt: row.occurredAt, source: row.source,
donorName: row.donorName, isAnonymous: !!row.isAnonymous, message: row.message,
amountCents: row.amountCents, currency: row.currency,
amountUsdCents: row.amountUsdCents != null ? row.amountUsdCents : row.amountCents,
-- appExternalRef: ref, isTest: !!row.isTest, reconciled: true,
++ appExternalRef: ref, isTest, reconciled: true,
};
let consumed = false;
try { consumed = checkout.handleAttributedDonation(host.receivingUserId, data); }
ddiff --git a/test/powerchat-reconcile.test.js b/test/powerchat-reconcile.test.js
iindex a190577b..4a42b54f 100644
---- a/test/powerchat-reconcile.test.js
++++ b/test/powerchat-reconcile.test.js
@@@ -62,6 +62,8 @@ const reconcile = require('../server/integrations/powerchat-reconcile');
{ eventId: 'e-other', occurredAt: '2026-08-27T09:30:00Z', source: 'powerchat', amountCents: 300, amountUsdCents: 300, appExternalRef: null },
{ eventId: 'e-echo', occurredAt: '2026-08-27T09:20:00Z', source: 'developer_app', amountCents: 500, amountUsdCents: 500, appExternalRef: 'pcorder:41' },
{ eventId: 'e-test', occurredAt: '2026-08-27T09:19:00Z', source: 'powerchat', isTest: true, amountCents: 500, amountUsdCents: 500, appExternalRef: 'pcorder:41' },
++ // Real feed rows have NO isTest field; manual_test is PowerChat's no-provider test tip → must be treated as test.
++ { eventId: 'e-manual', occurredAt: '2026-08-27T09:18:00Z', source: 'manual_test', amountCents: 500, amountUsdCents: 500, appExternalRef: 'pcorder:41' },
{ eventId: 'e-41', occurredAt: '2026-08-27T09:10:00Z', source: 'powerchat', donorName: 'Pat', amountCents: 500, currency: 'USD', amountUsdCents: 500, appExternalRef: 'pcorder:41' },
] }, nextCursor: 'cur-2' });
api.pages.push({ data: { rows: [
@@@ -72,7 +74,7 @@ const reconcile = require('../server/integrations/powerchat-reconcile');
assert.strictEqual(sum.hosts, 1, 'alex hosts both 41 (site) and 42 (direct, streamer 1 = alex)');
assert.strictEqual(sum.skippedHosts, 1, "'other' has no paid_messages:read → skipped, no call");
assert.strictEqual(sum.pages, 2, 'stopped after page 2: no orders left for that host despite a cursor');
-- assert.strictEqual(sum.skippedTest, 1);
++ assert.strictEqual(sum.skippedTest, 2, 'explicit isTest AND source=manual_test are both skipped');
assert.deepStrictEqual(sum.credited.map((c) => c.ref).sort(), ['pcorder:41', 'pcsub:42']);
assert.deepStrictEqual(sum.underpaid, []);
assert.strictEqual(orders.get(41).status, 'credited');