mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 14:21:25 +02:00
fix: gate direct (entry-fee) payouts behind DIRECT_PAYOUTS_ENABLED
completeGame only gated PAID_CREDITS payouts (on creditsPayoutEnabled); a PAID_SINGLE win paid out unconditionally, so DIRECT_PAYOUTS_ENABLED=false did NOT actually stop direct payouts — a no-payout instance (mainnet legitimacy) would still send crypto on an entry-fee win. Caught during deploy verification. - Add this.directPayoutEnabled (from DIRECT_PAYOUTS_ENABLED env / direct rule enabled in the config snapshot), mirroring creditsPayoutEnabled. - completeGame now gates the PAID_SINGLE payout on directPayoutEnabled. - The derived directPayoutsEnabled flags (getGameModeInfo / effective mode) and /api/game-modes PAID_SINGLE.payoutMultiplier now respect it (show 0 when off) instead of inferring from multipliers > 0. Adds a regression test: a PAID_SINGLE win with directPayoutEnabled=false queues no payout. Full suite: 22 suites / 159 tests.
This commit is contained in:
@@ -77,6 +77,10 @@ class GameModeManager {
|
||||
this.creditsPackagePrice = parseAtomicEnvValue(process.env.CREDITS_PACKAGE_PRICE, this.creditsPackagePrice);
|
||||
this.creditsPerGameCost = parseAtomicEnvValue(process.env.CREDITS_PER_GAME, 1) || 1;
|
||||
this.creditsPayoutEnabled = /^true$/i.test(process.env.CREDITS_PAYOUTS_ENABLED || process.env.CREDITS_PAYOUT_ENABLED || 'false');
|
||||
// Direct (per-game / entry-fee) payouts must be independently gateable, like credits.
|
||||
// Default true for backward compatibility; set DIRECT_PAYOUTS_ENABLED=false for a
|
||||
// no-payout instance (sell entry/credits for prestige only, e.g. mainnet legitimacy).
|
||||
this.directPayoutEnabled = !/^false$/i.test(process.env.DIRECT_PAYOUTS_ENABLED || 'true');
|
||||
this.creditsPayoutBaseValue = parseAtomicEnvValue(process.env.CREDITS_PAYOUT_BASE, this.singleGamePrice);
|
||||
process.env.CREDITS_PER_GAME = String(this.creditsPerGameCost);
|
||||
|
||||
@@ -159,6 +163,10 @@ class GameModeManager {
|
||||
const directRule = config.payouts.rules.direct || {};
|
||||
const creditsRule = config.payouts.rules.credits || {};
|
||||
|
||||
if (directRule.enabled !== undefined) {
|
||||
this.directPayoutEnabled = !!directRule.enabled;
|
||||
}
|
||||
|
||||
if (directRule.multipliers) {
|
||||
if (directRule.multipliers.escape !== undefined) {
|
||||
this.directPayoutMultipliers.escape = Number(directRule.multipliers.escape);
|
||||
@@ -469,7 +477,7 @@ class GameModeManager {
|
||||
bothModesEnabled,
|
||||
preferCreditsFirst: this.preferCreditsFirst,
|
||||
creditsPayoutsEnabled: this.creditsPayoutEnabled,
|
||||
directPayoutsEnabled: this.directPayoutMultipliers.escape > 0
|
||||
directPayoutsEnabled: this.directPayoutEnabled && this.directPayoutMultipliers.escape > 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1152,7 +1160,7 @@ class GameModeManager {
|
||||
freePlayEnabled: this.freePlayEnabled,
|
||||
directModeEnabled: this.directModeEnabled,
|
||||
creditsModeEnabled: this.creditsModeEnabled,
|
||||
directPayoutsEnabled: this.directPayoutMultipliers.escape > 0,
|
||||
directPayoutsEnabled: this.directPayoutEnabled && this.directPayoutMultipliers.escape > 0,
|
||||
creditsPayoutsEnabled: this.creditsPayoutEnabled,
|
||||
payoutMultipliers: {
|
||||
direct: this.directPayoutMultipliers,
|
||||
@@ -1244,8 +1252,11 @@ class GameModeManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle payouts based on the actual recorded payment mode, not the current global setting
|
||||
const payoutEligibleStartMode = (recordedPaymentMode === 'PAID_SINGLE') || (recordedPaymentMode === 'PAID_CREDITS' && this.creditsPayoutEnabled);
|
||||
// Handle payouts based on the actual recorded payment mode, not the current global
|
||||
// setting. Each mode's payout is gated by its own enable flag, so a no-payout
|
||||
// instance (DIRECT_PAYOUTS_ENABLED=false / CREDITS_PAYOUTS_ENABLED=false) never sends.
|
||||
const payoutEligibleStartMode = (recordedPaymentMode === 'PAID_SINGLE' && this.directPayoutEnabled)
|
||||
|| (recordedPaymentMode === 'PAID_CREDITS' && this.creditsPayoutEnabled);
|
||||
if (payoutEligibleStartMode && won) {
|
||||
// Use the payout terms snapshotted at game start (so a mid-game config change
|
||||
// can't alter an in-flight payout). Fall back to live calculation only for
|
||||
|
||||
+3
-1
@@ -595,7 +595,9 @@ app.get('/api/game-modes', (req, res) => {
|
||||
name: 'Paid Single Game',
|
||||
cost: toDisplay(directMode.price),
|
||||
enabled: !!directMode.enabled,
|
||||
payoutMultiplier: config.payouts.rules.direct.multipliers
|
||||
payoutMultiplier: (directMode.enabled && config.payouts.rules.direct.enabled)
|
||||
? config.payouts.rules.direct.multipliers
|
||||
: 0
|
||||
},
|
||||
PAID_CREDITS: {
|
||||
name: 'Credits Package',
|
||||
|
||||
@@ -89,4 +89,21 @@ describe('Payout uses the snapshot recorded at game start', () => {
|
||||
|
||||
expect(result.payout.amount).toBe(200000000000); // live 2x
|
||||
});
|
||||
|
||||
test('no payout for a PAID_SINGLE win when direct payouts are disabled (no-payout instance)', async () => {
|
||||
const { gmm, db } = buildGmm();
|
||||
gmm.directPayoutEnabled = false; // e.g. mainnet legitimacy: sell entry, never pay out
|
||||
|
||||
db.query
|
||||
.mockResolvedValueOnce({ rows: [{ id: 1, game_mode: 'PAID_SINGLE', payout_address: 'wow1addr' }] }) // game record
|
||||
.mockResolvedValue({ rows: [] }); // completion UPDATE + anything else
|
||||
|
||||
const result = await gmm.completeGame('socket1', 'game123', true, true, { moves: 10 });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.payout == null || result.payout === undefined).toBe(true); // no payout queued
|
||||
// No payout INSERT happened.
|
||||
const insertedPayout = db.query.mock.calls.find(c => /INSERT INTO payouts/i.test(c[0]));
|
||||
expect(insertedPayout).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user