Files
wownerogue/test/latePaymentReconciliation.test.js
jwinterm fad2a695bd Wownerogue
A browser-based roguelike synchronized with crypto block timing. A run is a race
against the next block: reach the exit before the block lands, or the house wins.
The same codebase runs the solo dungeon, the social Tavern, and operator-selected
PvP and race rulesets, with separate free and paid leaderboards.

Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md
for the design, and docs/DEPLOY.md to operate an instance.
2026-07-27 08:23:01 -04:00

95 lines
4.4 KiB
JavaScript

const GameModeManager = require('../src/game/gameModeManager');
const LatePaymentReconciler = require('../src/services/latePaymentReconciler');
describe('late incoming payment reconciliation', () => {
test('bounded scanner sends an expired native receipt only to manual review', async () => {
const row = {
id: 7,
user_id: 3,
status: 'expired',
subaddress: 'late-address',
address_index: 11,
expected_amount: '100',
provider_id: 'native-monero',
provider_invoice_id: 'late-address'
};
const db = { query: jest.fn().mockResolvedValue({ rows: [row] }) };
const receipt = {
evidenceType: 'chain_output', providerId: 'native-monero',
txHash: 'a'.repeat(64), outputId: 'b'.repeat(64),
evidenceId: `${'a'.repeat(64)}:${'b'.repeat(64)}`,
addressIndex: 11, amount: '100', confirmed: true
};
const walletService = {
accountIndex: 0,
addressToUser: new Map(),
addressToSocket: new Map(),
checkPaymentStatus: jest.fn(async address => {
expect(address).toBe('late-address');
expect(walletService.addressToUser.get(address)).toEqual(expect.objectContaining({
amount: 100,
addressIndex: 11
}));
return { confirmed: true, complete: true, amount: 100, receipts: [receipt] };
}),
stopPaymentMonitoring: jest.fn()
};
const gameModeManager = {
paymentProviders: { get: jest.fn(() => null) },
reconcileLatePaymentForReview: jest.fn().mockResolvedValue({ checked: true, needsReview: true })
};
const service = new LatePaymentReconciler({
db, gameModeManager, walletService, lookbackHours: 24, batchSize: 20,
logger: { warn: jest.fn(), error: jest.fn() }
});
const result = await service.runOnce();
expect(result).toEqual({ skipped: false, checked: 1, reviews: 1, errors: 0 });
expect(db.query.mock.calls[0][1]).toEqual([24, 20]);
expect(gameModeManager.reconcileLatePaymentForReview).toHaveBeenCalledWith(7, [receipt]);
expect(walletService.addressToUser.has('late-address')).toBe(false);
expect(walletService.stopPaymentMonitoring).toHaveBeenCalledWith('late-address');
});
test('durable late receipt stays expired and creates review without any entitlement write', async () => {
const calls = [];
const client = {
query: jest.fn(async (sql, params = []) => {
calls.push({ sql, params });
if (/SELECT id, status, expected_amount/i.test(sql)) {
return { rows: [{
id: 8, status: 'expired', expected_amount: '100',
provider_id: 'native-monero', address_index: 12
}] };
}
if (/COALESCE\(SUM\(amount\)/i.test(sql)) {
return { rows: [{ total: '100', primary_tx_hash: 'c'.repeat(64), receipt_count: '1' }] };
}
return { rows: [], rowCount: 1 };
})
};
const manager = Object.create(GameModeManager.prototype);
manager.db = { withTransaction: async callback => callback(client) };
const outputId = 'd'.repeat(64);
const receipt = {
evidenceType: 'chain_output', providerId: 'native-monero',
txHash: 'c'.repeat(64), outputId,
evidenceId: `${'c'.repeat(64)}:${outputId}`,
addressIndex: 12, amount: '100', confirmed: true
};
const result = await manager.reconcileLatePaymentForReview(8, [receipt]);
expect(result).toEqual(expect.objectContaining({
checked: true,
needsReview: true,
coverageComplete: true
}));
expect(calls.some(call => /INSERT INTO payment_receipts/i.test(call.sql))).toBe(true);
expect(calls.some(call => /INSERT INTO payment_late_reviews/i.test(call.sql))).toBe(true);
expect(calls.some(call => /SET status = CASE WHEN status = 'pending' THEN 'expired'/i.test(call.sql))).toBe(true);
expect(calls.some(call => /UPDATE users|payment_entitlement_grants|credit_transactions/i.test(call.sql))).toBe(false);
});
});