mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-11-14 11:03:53 +01:00
fix(payments): restore pending invoice on session resume
This commit is contained in:
@@ -32,6 +32,8 @@ const {
|
||||
} = require('../game/soloRestartSnapshot');
|
||||
const { normalizeError } = require('../utils/errors');
|
||||
const money = require('../money/atomic');
|
||||
const ProductGrants = require('../payments/productGrants');
|
||||
const { isWowPaymentsPaused } = require('../config/operatedProductProfiles');
|
||||
const {
|
||||
buildCommerceDisclosure,
|
||||
validatePaidAcknowledgement
|
||||
@@ -775,10 +777,6 @@ class SocketHandlers {
|
||||
'Welcome back! Your game has been restored.');
|
||||
}
|
||||
|
||||
// Restore payment monitoring if it was active
|
||||
if (suspendedState.paymentMonitoringActive && this.paymentHandlers) {
|
||||
this._restorePaymentMonitoring(socket, dbUserId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -798,8 +796,6 @@ class SocketHandlers {
|
||||
nextBlock: (this.debugManager.getCurrentBlockHeight ? this.debugManager.getCurrentBlockHeight() : 0) + 1
|
||||
});
|
||||
|
||||
// Check for pending payment and restore monitoring
|
||||
this._restorePaymentMonitoring(socket, dbUserId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -808,6 +804,11 @@ class SocketHandlers {
|
||||
if (this.gameModeManager) {
|
||||
socket.emit('game_mode_info', this.gameModeManager.getGameModeInfo());
|
||||
}
|
||||
// A pending invoice belongs to the authenticated session, whether or not its owner
|
||||
// also has a suspended game or queue entry. Deliver capabilities before the invoice.
|
||||
if (sessionInfo?.resumed && dbUserId) {
|
||||
this._restorePaymentMonitoring(socket, dbUserId);
|
||||
}
|
||||
this._emitIdentity(socket).catch(err => {
|
||||
if (this.debugManager?.CONSOLE_LOGGING) console.warn('Failed to send identity snapshot:', err.message);
|
||||
});
|
||||
@@ -1747,14 +1748,20 @@ class SocketHandlers {
|
||||
* @param {number} dbUserId - The DB user ID
|
||||
*/
|
||||
async _restorePaymentMonitoring(socket, dbUserId) {
|
||||
if (!dbUserId || !this.paymentHandlers || !this.gameModeManager) {
|
||||
const manager = this.gameModeManager;
|
||||
if (!dbUserId || !this.paymentHandlers || !manager?.db
|
||||
|| manager.paymentsEnabled !== true || isWowPaymentsPaused(process.env)
|
||||
|| !this._isCurrentPaymentRestoreOwner(socket, dbUserId)) {
|
||||
return;
|
||||
}
|
||||
const monitorBeforeRestore = this.paymentHandlers.socketPaymentMap?.get(socket.id) || null;
|
||||
|
||||
try {
|
||||
// Query for pending (unpaid) payment requests for this user
|
||||
const pendingResult = await this.gameModeManager.db.query(`
|
||||
SELECT id, subaddress, expected_amount, payment_type, description, expires_at, address_index,
|
||||
const pendingResult = await manager.db.query(`
|
||||
SELECT id, user_id, subaddress, expected_amount, payment_type, description,
|
||||
expires_at, address_index, product_id, product_grants,
|
||||
provider_id, provider_invoice_id,
|
||||
fairness_proof_version, fairness_offer_id, fairness_offer_issued_at,
|
||||
fairness_commitment, fairness_server_seed, fairness_client_seed,
|
||||
fairness_bound_at, fairness_consumed_at
|
||||
@@ -1766,6 +1773,8 @@ class SocketHandlers {
|
||||
LIMIT 1
|
||||
`, [dbUserId]);
|
||||
|
||||
if (!this._isCurrentPaymentRestoreOwner(socket, dbUserId)) return;
|
||||
|
||||
if (pendingResult.rows.length === 0) {
|
||||
// No pending payment to restore
|
||||
if (this.debugManager.CONSOLE_LOGGING) {
|
||||
@@ -1775,6 +1784,21 @@ class SocketHandlers {
|
||||
}
|
||||
|
||||
const payment = pendingResult.rows[0];
|
||||
const paymentType = payment.payment_type;
|
||||
if (String(payment.user_id) !== String(dbUserId)
|
||||
|| !['single_game', 'credits_package', 'cosmetic_pack'].includes(paymentType)
|
||||
|| typeof manager.isPaymentIntakeEnabled !== 'function'
|
||||
|| !manager.isPaymentIntakeEnabled(paymentType)) return;
|
||||
|
||||
const cryptoType = manager.cryptoType;
|
||||
const provider = manager.paymentProviders?.getProvider?.(cryptoType) || null;
|
||||
const providerId = provider?.id || 'native-monero';
|
||||
if ((payment.provider_id && payment.provider_id !== providerId)
|
||||
|| (providerId !== 'native-monero' && !payment.provider_invoice_id)) return;
|
||||
|
||||
// This socket may already be watching the same invoice after another restore path.
|
||||
if (String(this.paymentHandlers.socketPaymentMap?.get(socket.id)?.paymentId)
|
||||
=== String(payment.id)) return;
|
||||
const fairnessProof = payment.payment_type === 'single_game'
|
||||
&& typeof this.gameModeManager._paymentFairnessProofFromRow === 'function'
|
||||
? this.gameModeManager._paymentFairnessProofFromRow(payment)
|
||||
@@ -1788,9 +1812,8 @@ class SocketHandlers {
|
||||
'This paid entry has no durable fairness binding and requires support review.');
|
||||
return;
|
||||
}
|
||||
const cryptoType = this.gameModeManager.cryptoType;
|
||||
const formattedAmount = this.gameModeManager.formatAtomicHuman
|
||||
? this.gameModeManager.formatAtomicHuman(payment.expected_amount, 3)
|
||||
const formattedAmount = manager.formatAtomicHuman
|
||||
? manager.formatAtomicHuman(payment.expected_amount, 3)
|
||||
: payment.expected_amount;
|
||||
|
||||
// Generate QR code
|
||||
@@ -1802,24 +1825,68 @@ class SocketHandlers {
|
||||
payment.expected_amount,
|
||||
cryptoType,
|
||||
payment.description || 'Restored payment',
|
||||
this.gameModeManager.currencyDecimals
|
||||
manager.currencyDecimals
|
||||
);
|
||||
} catch (qrErr) {
|
||||
console.warn('QR generation failed during restore:', qrErr.message);
|
||||
}
|
||||
|
||||
if (!this._isCurrentPaymentRestoreOwner(socket, dbUserId)
|
||||
|| manager.paymentsEnabled !== true || isWowPaymentsPaused(process.env)
|
||||
|| !manager.isPaymentIntakeEnabled(paymentType)) return;
|
||||
// Another request or concurrent restore may have installed a watcher while the DB
|
||||
// and QR work awaited. Never replace it with the older snapshot read above.
|
||||
const currentMonitor = this.paymentHandlers.socketPaymentMap?.get(socket.id) || null;
|
||||
if (currentMonitor !== monitorBeforeRestore
|
||||
|| (currentMonitor && String(currentMonitor.paymentId) === String(payment.id))) return;
|
||||
|
||||
// Stop an older watcher before binding this invoice. A stop can remove the old
|
||||
// address watcher, so binding first would risk stopping the newly restored one.
|
||||
this.paymentHandlers.stopMonitoringForSocket(socket.id);
|
||||
|
||||
const currentUser = this.connectionHandler.getUserBySocket(socket.id) || { serverId: socket.id };
|
||||
const paymentRequest = {
|
||||
id: payment.id,
|
||||
address: payment.subaddress,
|
||||
invoiceId: payment.provider_invoice_id || payment.subaddress,
|
||||
amount: payment.expected_amount,
|
||||
amountFormatted: formattedAmount,
|
||||
package: null,
|
||||
fairnessProof
|
||||
};
|
||||
|
||||
if (this.walletService && payment.subaddress && providerId === 'native-monero') {
|
||||
this.walletService.addressToUser.set(payment.subaddress, {
|
||||
userId: String(dbUserId),
|
||||
socketId: socket.id,
|
||||
amount: money.toSafe(money.toBig(payment.expected_amount)),
|
||||
addressIndex: payment.address_index ?? 0,
|
||||
accountIndex: 0,
|
||||
detected: false,
|
||||
confirmed: false,
|
||||
status: 'pending'
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof this.paymentHandlers._monitorAddress !== 'function') return;
|
||||
this.paymentHandlers._monitorAddress(socket, paymentRequest, payment.expected_amount,
|
||||
cryptoType, currentUser, paymentType, fairnessProof);
|
||||
|
||||
// Re-emit payment_created to the client
|
||||
socket.emit('payment_created', {
|
||||
paymentId: payment.id,
|
||||
ownerUserId: dbUserId,
|
||||
asset: String(cryptoType).toLowerCase(),
|
||||
network: this.gameModeManager.network,
|
||||
network: manager.network,
|
||||
memo: payment.description,
|
||||
address: payment.subaddress,
|
||||
amount: payment.expected_amount,
|
||||
amountFormatted: formattedAmount,
|
||||
humanAmount: money.format(payment.expected_amount, this.gameModeManager.currencyDecimals),
|
||||
humanAmount: money.format(payment.expected_amount, manager.currencyDecimals),
|
||||
paymentType: payment.payment_type,
|
||||
productId: payment.product_id,
|
||||
grants: payment.product_grants
|
||||
? ProductGrants.publicGrantSummary(payment.product_grants) : null,
|
||||
cryptoType: cryptoType,
|
||||
description: payment.description,
|
||||
expiresAt: payment.expires_at,
|
||||
@@ -1834,49 +1901,6 @@ class SocketHandlers {
|
||||
} : null
|
||||
});
|
||||
|
||||
// Get current user object for queue operations
|
||||
const currentUser = this.connectionHandler.getUserBySocket(socket.id) || { serverId: socket.id };
|
||||
|
||||
// Create a mock paymentRequest object for _monitorAddress
|
||||
const paymentRequest = {
|
||||
id: payment.id,
|
||||
address: payment.subaddress,
|
||||
amount: payment.expected_amount,
|
||||
amountFormatted: formattedAmount,
|
||||
package: null,
|
||||
fairnessProof
|
||||
};
|
||||
|
||||
// Re-register address in wallet service so checkPaymentStatus() can query the wallet RPC.
|
||||
// This is essential after server restart or if the entry was lost during disconnect.
|
||||
if (this.walletService && payment.subaddress) {
|
||||
this.walletService.addressToUser.set(payment.subaddress, {
|
||||
userId: String(dbUserId),
|
||||
socketId: socket.id,
|
||||
amount: money.toSafe(money.toBig(payment.expected_amount)),
|
||||
addressIndex: payment.address_index ?? 0,
|
||||
accountIndex: 0,
|
||||
detected: false,
|
||||
confirmed: false,
|
||||
status: 'pending'
|
||||
});
|
||||
}
|
||||
|
||||
// Clear any monitoring already attached to this socket before restarting it.
|
||||
this.paymentHandlers.stopMonitoringForSocket(socket.id);
|
||||
|
||||
if (typeof this.paymentHandlers._monitorAddress === 'function') {
|
||||
this.paymentHandlers._monitorAddress(
|
||||
socket,
|
||||
paymentRequest,
|
||||
payment.expected_amount,
|
||||
cryptoType,
|
||||
currentUser,
|
||||
payment.payment_type,
|
||||
fairnessProof
|
||||
);
|
||||
}
|
||||
|
||||
// Send status update
|
||||
this.broadcastManager.sendStatusUpdate(
|
||||
socket.id,
|
||||
@@ -1892,6 +1916,11 @@ class SocketHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
_isCurrentPaymentRestoreOwner(socket, dbUserId) {
|
||||
return this._isShuttingDown !== true && socket?.connected === true
|
||||
&& String(this.sessionManager?.sessions?.get(socket.id)?.id) === String(dbUserId);
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,7 @@ test.each(['request', 'show', 'restore'])('the %s invoice path supplies an exact
|
||||
const manager = Object.create(GameModeManager.prototype);
|
||||
Object.assign(manager, {
|
||||
cryptoType: 'WOW', network: 'mainnet', currencyDecimals: 11,
|
||||
paymentsEnabled: true,
|
||||
isPaymentIntakeEnabled: () => true,
|
||||
formatAtomicHuman: () => '1.2346',
|
||||
gameMode: 'PAID_CREDITS', creditsPackagePrice: row.expected_amount,
|
||||
@@ -40,12 +41,15 @@ test.each(['request', 'show', 'restore'])('the %s invoice path supplies an exact
|
||||
handlers._allowInvoiceFlowForReserve = async () => true;
|
||||
handlers._ensureRequiredPayoutAddress = async () => ({ ok: true });
|
||||
handlers._monitorAddress = jest.fn();
|
||||
const socket = { id: 'fixture-socket', emit: (event, data) => emitted.push([event, data]) };
|
||||
const socket = { id: 'fixture-socket', connected: true,
|
||||
emit: (event, data) => emitted.push([event, data]) };
|
||||
try {
|
||||
if (path === 'request') await handlers.handlePaymentRequest(socket, { type: 'credits_package' });
|
||||
else if (path === 'show') await handlers.createAndShowPaymentRequest(socket, { paymentType: 'credits_package' });
|
||||
else await SocketHandlers.prototype._restorePaymentMonitoring.call({
|
||||
gameModeManager: manager, paymentHandlers: handlers, debugManager: { CONSOLE_LOGGING: false },
|
||||
sessionManager: { sessions: new Map([[socket.id, { id: 7 }]]) },
|
||||
_isCurrentPaymentRestoreOwner: SocketHandlers.prototype._isCurrentPaymentRestoreOwner,
|
||||
connectionHandler: { getUserBySocket: () => ({ serverId: socket.id }) },
|
||||
broadcastManager: { sendStatusUpdate: jest.fn() }
|
||||
}, socket, 7);
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
'use strict';
|
||||
|
||||
jest.mock('../src/payments/qrService', () => ({
|
||||
generatePaymentQR: jest.fn(async () => 'data:image/png;base64,fixture')
|
||||
}));
|
||||
|
||||
const SocketHandlers = require('../src/network/socketHandlers');
|
||||
const GameModeManager = require('../src/game/gameModeManager');
|
||||
const { generatePaymentQR } = require('../src/payments/qrService');
|
||||
|
||||
const pending = {
|
||||
id: 31, user_id: 7, subaddress: 'WW3UyJFX6GPKdM21APyVYjGhfX3ebyHvPPgtFC3wHxkYTCGg8k2UM9DLTCzcgoaLHSKnvJKRtCPgJ1sxvgYiFCoK19vLk83Jn',
|
||||
expected_amount: '123456789012', payment_type: 'credits_package',
|
||||
description: '10 credit package', product_id: 'ten-credits',
|
||||
product_grants: { credits: 10 }, provider_id: 'native-monero',
|
||||
expires_at: new Date(Date.now() + 3600000).toISOString(), address_index: 5
|
||||
};
|
||||
|
||||
function fixture({ resumed = true, row = pending } = {}) {
|
||||
const events = [];
|
||||
const socket = {
|
||||
id: 'socket-7', connected: true, on: jest.fn(),
|
||||
emit: jest.fn((event, payload) => events.push([event, payload]))
|
||||
};
|
||||
const sessions = new Map([[socket.id, { id: 7 }]]);
|
||||
const monitored = new Map();
|
||||
const manager = Object.create(GameModeManager.prototype);
|
||||
Object.assign(manager, {
|
||||
paymentsEnabled: true, creditsModeEnabled: true, directModeEnabled: false,
|
||||
cryptoType: 'WOW', network: 'mainnet', currencyDecimals: 11,
|
||||
formatAtomicHuman: () => '1.235',
|
||||
db: { query: jest.fn(async () => ({ rows: row ? [row] : [] })) },
|
||||
getGameModeInfo: jest.fn(() => ({
|
||||
paymentsEnabled: manager.isPaymentIntakeEnabled('credits_package')
|
||||
}))
|
||||
});
|
||||
const handlers = Object.create(SocketHandlers.prototype);
|
||||
Object.assign(handlers, {
|
||||
_isShuttingDown: false,
|
||||
connectionHandler: {
|
||||
handleConnection: jest.fn(async () => ({
|
||||
sessionInfo: { resumed, user: { id: 7 } }
|
||||
})),
|
||||
getUserBySocket: () => ({ serverId: socket.id })
|
||||
},
|
||||
sessionManager: { sessions },
|
||||
suspendedGameManager: { hasSuspendedGame: () => false },
|
||||
queueManager: { updateSocketId: () => false },
|
||||
gameModeManager: manager,
|
||||
paymentHandlers: {
|
||||
socketPaymentMap: monitored,
|
||||
stopMonitoringForSocket: jest.fn(id => monitored.delete(id)),
|
||||
_monitorAddress: jest.fn((sock, request) => monitored.set(sock.id, {
|
||||
paymentId: request.id, address: request.address
|
||||
}))
|
||||
},
|
||||
walletService: { addressToUser: new Map() },
|
||||
broadcastManager: { sendStatusUpdate: jest.fn() },
|
||||
debugManager: { CONSOLE_LOGGING: false },
|
||||
_emitFairnessOffer: jest.fn(),
|
||||
_emitIdentity: jest.fn(async () => null)
|
||||
});
|
||||
return { handlers, socket, sessions, monitored, manager, events };
|
||||
}
|
||||
|
||||
describe('pending invoice restoration on authenticated session resume', () => {
|
||||
const previous = {
|
||||
OPERATED_PRODUCT_PROFILE: process.env.OPERATED_PRODUCT_PROFILE,
|
||||
PAYMENTS_ENABLED: process.env.PAYMENTS_ENABLED
|
||||
};
|
||||
beforeEach(() => {
|
||||
generatePaymentQR.mockReset();
|
||||
generatePaymentQR.mockResolvedValue('data:image/png;base64,fixture');
|
||||
delete process.env.OPERATED_PRODUCT_PROFILE;
|
||||
process.env.PAYMENTS_ENABLED = 'true';
|
||||
});
|
||||
afterAll(() => {
|
||||
for (const [key, value] of Object.entries(previous)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
test('resumes a standalone invoice after capabilities without a game or queue', async () => {
|
||||
const { handlers, socket, manager, events } = fixture();
|
||||
await handlers.handleConnection(socket);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
const modes = events.findIndex(([event]) => event === 'game_mode_info');
|
||||
const invoice = events.findIndex(([event]) => event === 'payment_created');
|
||||
expect(modes).toBeGreaterThanOrEqual(0);
|
||||
expect(invoice).toBeGreaterThan(modes);
|
||||
expect(events[invoice][1]).toMatchObject({
|
||||
paymentId: pending.id, ownerUserId: pending.user_id,
|
||||
amount: pending.expected_amount, productId: pending.product_id,
|
||||
memo: pending.description, restored: true
|
||||
});
|
||||
expect(manager.db.query).toHaveBeenCalledTimes(1);
|
||||
expect(manager.db.query.mock.calls[0][1]).toEqual([7]);
|
||||
expect(handlers.paymentHandlers._monitorAddress).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('does not restore to a newly created or different owner session', async () => {
|
||||
const fresh = fixture({ resumed: false });
|
||||
await fresh.handlers.handleConnection(fresh.socket);
|
||||
expect(fresh.manager.db.query).not.toHaveBeenCalled();
|
||||
|
||||
const wrongOwner = fixture({ row: { ...pending, user_id: 8 } });
|
||||
await wrongOwner.handlers._restorePaymentMonitoring(wrongOwner.socket, 7);
|
||||
expect(wrongOwner.events.some(([event]) => event === 'payment_created')).toBe(false);
|
||||
expect(wrongOwner.handlers.paymentHandlers._monitorAddress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('drops an in-flight restore when the session is adopted elsewhere', async () => {
|
||||
const { handlers, socket, sessions, manager, events } = fixture();
|
||||
let finishQuery;
|
||||
manager.db.query = jest.fn(() => new Promise(resolve => { finishQuery = resolve; }));
|
||||
const restore = handlers._restorePaymentMonitoring(socket, 7);
|
||||
sessions.delete(socket.id);
|
||||
socket.connected = false;
|
||||
finishQuery({ rows: [pending] });
|
||||
await restore;
|
||||
expect(events.some(([event]) => event === 'payment_created')).toBe(false);
|
||||
expect(handlers.paymentHandlers._monitorAddress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not deliver or watch after adoption during QR creation', async () => {
|
||||
const { handlers, socket, sessions, events } = fixture();
|
||||
let finishQr;
|
||||
generatePaymentQR.mockImplementationOnce(() => new Promise(resolve => { finishQr = resolve; }));
|
||||
const restore = handlers._restorePaymentMonitoring(socket, 7);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
expect(generatePaymentQR).toHaveBeenCalledTimes(1);
|
||||
sessions.delete(socket.id);
|
||||
finishQr('data:image/png;base64,fixture');
|
||||
await restore;
|
||||
expect(events.some(([event]) => event === 'payment_created')).toBe(false);
|
||||
expect(handlers.paymentHandlers._monitorAddress).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not replace a newer watcher installed while QR creation awaited', async () => {
|
||||
const { handlers, socket, monitored, events } = fixture();
|
||||
let finishQr;
|
||||
generatePaymentQR.mockImplementationOnce(() => new Promise(resolve => { finishQr = resolve; }));
|
||||
const restore = handlers._restorePaymentMonitoring(socket, 7);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
expect(generatePaymentQR).toHaveBeenCalledTimes(1);
|
||||
const newer = { paymentId: 32, address: 'newer-address' };
|
||||
monitored.set(socket.id, newer);
|
||||
finishQr('data:image/png;base64,fixture');
|
||||
await restore;
|
||||
expect(monitored.get(socket.id)).toBe(newer);
|
||||
expect(handlers.paymentHandlers.stopMonitoringForSocket).not.toHaveBeenCalled();
|
||||
expect(handlers.walletService.addressToUser.size).toBe(0);
|
||||
expect(events.some(([event]) => event === 'payment_created')).toBe(false);
|
||||
});
|
||||
|
||||
test('keeps one watcher and one invoice event for the same pending payment', async () => {
|
||||
const { handlers, socket, events } = fixture();
|
||||
await handlers._restorePaymentMonitoring(socket, 7);
|
||||
await handlers._restorePaymentMonitoring(socket, 7);
|
||||
expect(handlers.paymentHandlers._monitorAddress).toHaveBeenCalledTimes(1);
|
||||
expect(events.filter(([event]) => event === 'payment_created')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('stops a prior socket watcher before binding and monitoring the pending invoice', async () => {
|
||||
const { handlers, socket, monitored } = fixture();
|
||||
monitored.set(socket.id, { paymentId: 2, address: 'old-address' });
|
||||
const bind = jest.spyOn(handlers.walletService.addressToUser, 'set');
|
||||
await handlers._restorePaymentMonitoring(socket, 7);
|
||||
const stopped = handlers.paymentHandlers.stopMonitoringForSocket.mock.invocationCallOrder[0];
|
||||
const rebound = bind.mock.invocationCallOrder[0];
|
||||
const restarted = handlers.paymentHandlers._monitorAddress.mock.invocationCallOrder[0];
|
||||
expect(stopped).toBeLessThan(rebound);
|
||||
expect(rebound).toBeLessThan(restarted);
|
||||
expect(monitored.get(socket.id).paymentId).toBe(pending.id);
|
||||
});
|
||||
|
||||
test('free and official paused profiles do not query or advertise an invoice', async () => {
|
||||
const free = fixture();
|
||||
free.manager.paymentsEnabled = false;
|
||||
await free.handlers.handleConnection(free.socket);
|
||||
expect(free.manager.db.query).not.toHaveBeenCalled();
|
||||
expect(free.events.find(([event]) => event === 'game_mode_info')[1].paymentsEnabled).toBe(false);
|
||||
expect(free.events.some(([event]) => event === 'payment_created')).toBe(false);
|
||||
|
||||
process.env.OPERATED_PRODUCT_PROFILE = 'such-play-wow-prestige';
|
||||
process.env.PAYMENTS_ENABLED = 'false';
|
||||
const paused = fixture();
|
||||
await paused.handlers.handleConnection(paused.socket);
|
||||
expect(paused.manager.db.query).not.toHaveBeenCalled();
|
||||
expect(paused.events.find(([event]) => event === 'game_mode_info')[1].paymentsEnabled).toBe(false);
|
||||
expect(paused.events.some(([event]) => event === 'payment_created')).toBe(false);
|
||||
});
|
||||
|
||||
test('uses persisted routed invoice identity and refuses a missing gateway reference', async () => {
|
||||
const gatewayRow = { ...pending, provider_id: 'btcpay', provider_invoice_id: 'invoice-31' };
|
||||
const routed = fixture({ row: gatewayRow });
|
||||
routed.manager.paymentProviders = { getProvider: () => ({ id: 'btcpay' }) };
|
||||
await routed.handlers._restorePaymentMonitoring(routed.socket, 7);
|
||||
const request = routed.handlers.paymentHandlers._monitorAddress.mock.calls[0][1];
|
||||
expect(request.invoiceId).toBe(gatewayRow.provider_invoice_id);
|
||||
|
||||
const missing = fixture({ row: { ...gatewayRow, provider_invoice_id: null } });
|
||||
missing.manager.paymentProviders = routed.manager.paymentProviders;
|
||||
await missing.handlers._restorePaymentMonitoring(missing.socket, 7);
|
||||
expect(missing.handlers.paymentHandlers._monitorAddress).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user