- When admin enables maintenance, it's set to 'pending' state - Maintenance activates automatically after the current draw completes - Admin can use immediate=true to force immediate activation - Frontend shows 'Maintenance Scheduled' banner when pending - Telegram bot warns users but still allows purchases when pending - Both mode and pending status tracked in system_settings table
311 lines
9.1 KiB
TypeScript
311 lines
9.1 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import { db } from '../database';
|
|
import { executeDraw } from '../services/draw';
|
|
import { retryPayoutService } from '../services/payout';
|
|
import { JackpotCycle, Payout } from '../types';
|
|
|
|
/**
|
|
* GET /admin/cycles
|
|
* List all cycles with optional filters
|
|
*/
|
|
export async function listCycles(req: Request, res: Response) {
|
|
try {
|
|
const { status, cycle_type, limit = 50, offset = 0 } = req.query;
|
|
|
|
let query = `SELECT * FROM jackpot_cycles WHERE 1=1`;
|
|
const params: any[] = [];
|
|
let paramCount = 0;
|
|
|
|
if (status) {
|
|
paramCount++;
|
|
query += ` AND status = $${paramCount}`;
|
|
params.push(status);
|
|
}
|
|
|
|
if (cycle_type) {
|
|
paramCount++;
|
|
query += ` AND cycle_type = $${paramCount}`;
|
|
params.push(cycle_type);
|
|
}
|
|
|
|
query += ` ORDER BY scheduled_at DESC LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}`;
|
|
params.push(limit, offset);
|
|
|
|
const result = await db.query<JackpotCycle>(query, params);
|
|
|
|
const cycles = result.rows.map(c => ({
|
|
id: c.id,
|
|
lottery_id: c.lottery_id,
|
|
cycle_type: c.cycle_type,
|
|
sequence_number: c.sequence_number,
|
|
scheduled_at: c.scheduled_at.toISOString(),
|
|
status: c.status,
|
|
pot_total_sats: parseInt(c.pot_total_sats.toString()),
|
|
pot_after_fee_sats: c.pot_after_fee_sats ? parseInt(c.pot_after_fee_sats.toString()) : null,
|
|
winning_ticket_id: c.winning_ticket_id,
|
|
winning_lightning_address: c.winning_lightning_address,
|
|
}));
|
|
|
|
return res.json({
|
|
version: '1.0',
|
|
data: {
|
|
cycles,
|
|
total: cycles.length,
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
console.error('List cycles error:', error);
|
|
return res.status(500).json({
|
|
version: '1.0',
|
|
error: 'INTERNAL_ERROR',
|
|
message: 'Failed to list cycles',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /admin/cycles/:id/run-draw
|
|
* Manually trigger draw execution
|
|
*/
|
|
export async function runDrawManually(req: Request, res: Response) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const result = await executeDraw(id);
|
|
|
|
if (!result.success) {
|
|
return res.status(400).json({
|
|
version: '1.0',
|
|
error: result.error || 'DRAW_FAILED',
|
|
message: result.message || 'Failed to execute draw',
|
|
});
|
|
}
|
|
|
|
return res.json({
|
|
version: '1.0',
|
|
data: {
|
|
cycle_id: id,
|
|
winning_ticket_id: result.winningTicketId,
|
|
pot_after_fee_sats: result.potAfterFeeSats,
|
|
payout_status: result.payoutStatus,
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Manual draw error:', error);
|
|
return res.status(500).json({
|
|
version: '1.0',
|
|
error: 'INTERNAL_ERROR',
|
|
message: 'Failed to run draw',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /admin/payouts
|
|
* List payouts with optional filters
|
|
*/
|
|
export async function listPayouts(req: Request, res: Response) {
|
|
try {
|
|
const { status, limit = 50, offset = 0 } = req.query;
|
|
|
|
let query = `SELECT * FROM payouts WHERE 1=1`;
|
|
const params: any[] = [];
|
|
let paramCount = 0;
|
|
|
|
if (status) {
|
|
paramCount++;
|
|
query += ` AND status = $${paramCount}`;
|
|
params.push(status);
|
|
}
|
|
|
|
query += ` ORDER BY created_at DESC LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}`;
|
|
params.push(limit, offset);
|
|
|
|
const result = await db.query<Payout>(query, params);
|
|
|
|
const payouts = result.rows.map(p => ({
|
|
id: p.id,
|
|
lottery_id: p.lottery_id,
|
|
cycle_id: p.cycle_id,
|
|
ticket_id: p.ticket_id,
|
|
lightning_address: p.lightning_address,
|
|
amount_sats: parseInt(p.amount_sats.toString()),
|
|
status: p.status,
|
|
error_message: p.error_message,
|
|
retry_count: p.retry_count,
|
|
created_at: p.created_at.toISOString(),
|
|
}));
|
|
|
|
return res.json({
|
|
version: '1.0',
|
|
data: {
|
|
payouts,
|
|
total: payouts.length,
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
console.error('List payouts error:', error);
|
|
return res.status(500).json({
|
|
version: '1.0',
|
|
error: 'INTERNAL_ERROR',
|
|
message: 'Failed to list payouts',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /admin/payouts/:id/retry
|
|
* Retry a failed payout
|
|
*/
|
|
export async function retryPayout(req: Request, res: Response) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const result = await retryPayoutService(id);
|
|
|
|
if (!result.success) {
|
|
return res.status(400).json({
|
|
version: '1.0',
|
|
error: result.error || 'RETRY_FAILED',
|
|
message: result.message || 'Failed to retry payout',
|
|
});
|
|
}
|
|
|
|
return res.json({
|
|
version: '1.0',
|
|
data: {
|
|
payout_id: id,
|
|
status: result.status,
|
|
retry_count: result.retryCount,
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Retry payout error:', error);
|
|
return res.status(500).json({
|
|
version: '1.0',
|
|
error: 'INTERNAL_ERROR',
|
|
message: 'Failed to retry payout',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /admin/maintenance
|
|
* Get current maintenance mode status
|
|
*/
|
|
export async function getMaintenanceStatus(req: Request, res: Response) {
|
|
try {
|
|
const result = await db.query(
|
|
`SELECT value FROM system_settings WHERE key = 'maintenance_mode'`
|
|
);
|
|
const pendingResult = await db.query(
|
|
`SELECT value FROM system_settings WHERE key = 'maintenance_pending'`
|
|
);
|
|
const messageResult = await db.query(
|
|
`SELECT value FROM system_settings WHERE key = 'maintenance_message'`
|
|
);
|
|
|
|
const enabled = result.rows[0]?.value === 'true';
|
|
const pending = pendingResult.rows[0]?.value === 'true';
|
|
const message = messageResult.rows[0]?.value || 'System is under maintenance. Please try again later.';
|
|
|
|
return res.json({
|
|
version: '1.0',
|
|
data: {
|
|
maintenance_mode: enabled,
|
|
maintenance_pending: pending,
|
|
message: message,
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Get maintenance status error:', error);
|
|
return res.status(500).json({
|
|
version: '1.0',
|
|
error: 'INTERNAL_ERROR',
|
|
message: 'Failed to get maintenance status',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /admin/maintenance
|
|
* Enable or disable maintenance mode
|
|
* When enabling, maintenance is set to "pending" and activates after current draw completes
|
|
*/
|
|
export async function setMaintenanceMode(req: Request, res: Response) {
|
|
try {
|
|
const { enabled, message, immediate } = req.body;
|
|
|
|
if (typeof enabled !== 'boolean') {
|
|
return res.status(400).json({
|
|
version: '1.0',
|
|
error: 'INVALID_INPUT',
|
|
message: 'enabled must be a boolean',
|
|
});
|
|
}
|
|
|
|
if (enabled) {
|
|
if (immediate === true) {
|
|
// Immediate activation (admin override)
|
|
await db.query(
|
|
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_mode', 'true', datetime('now'))
|
|
ON CONFLICT(key) DO UPDATE SET value = 'true', updated_at = datetime('now')`
|
|
);
|
|
await db.query(
|
|
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_pending', 'false', datetime('now'))
|
|
ON CONFLICT(key) DO UPDATE SET value = 'false', updated_at = datetime('now')`
|
|
);
|
|
console.log('Maintenance mode ENABLED IMMEDIATELY');
|
|
} else {
|
|
// Set pending - will activate after current draw completes
|
|
await db.query(
|
|
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_pending', 'true', datetime('now'))
|
|
ON CONFLICT(key) DO UPDATE SET value = 'true', updated_at = datetime('now')`
|
|
);
|
|
console.log('Maintenance mode PENDING (will activate after current draw)');
|
|
}
|
|
} else {
|
|
// Disable both active and pending maintenance
|
|
await db.query(
|
|
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_mode', 'false', datetime('now'))
|
|
ON CONFLICT(key) DO UPDATE SET value = 'false', updated_at = datetime('now')`
|
|
);
|
|
await db.query(
|
|
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_pending', 'false', datetime('now'))
|
|
ON CONFLICT(key) DO UPDATE SET value = 'false', updated_at = datetime('now')`
|
|
);
|
|
console.log('Maintenance mode DISABLED');
|
|
}
|
|
|
|
// Update message if provided
|
|
if (message && typeof message === 'string') {
|
|
await db.query(
|
|
`INSERT INTO system_settings (key, value, updated_at) VALUES ('maintenance_message', $1, datetime('now'))
|
|
ON CONFLICT(key) DO UPDATE SET value = $1, updated_at = datetime('now')`,
|
|
[message]
|
|
);
|
|
}
|
|
|
|
// Get current state
|
|
const modeResult = await db.query(`SELECT value FROM system_settings WHERE key = 'maintenance_mode'`);
|
|
const pendingResult = await db.query(`SELECT value FROM system_settings WHERE key = 'maintenance_pending'`);
|
|
|
|
return res.json({
|
|
version: '1.0',
|
|
data: {
|
|
maintenance_mode: modeResult.rows[0]?.value === 'true',
|
|
maintenance_pending: pendingResult.rows[0]?.value === 'true',
|
|
message: message || 'System is under maintenance. Please try again later.',
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Set maintenance mode error:', error);
|
|
return res.status(500).json({
|
|
version: '1.0',
|
|
error: 'INTERNAL_ERROR',
|
|
message: 'Failed to set maintenance mode',
|
|
});
|
|
}
|
|
}
|
|
|