Keep event revenue at what was paid when the ticket price changes.
The event header revenue and the door summary's pre-sale total were computed as settled tickets × the current event price, so editing the price rewrote revenue for tickets already sold. They now sum the paid payment amounts; the header no longer falls back to count × price. Admin analytics per-event revenue gets the same fix. Bookings that haven't been paid yet owe the current price, so a price or currency change now reprices open `pending` payments in the same transaction as the event update. Paid/refunded history, pending_approval, on_hold and Lightning invoices keep their amount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e203fb6c74
commit
b3584e6c4d
@@ -79,6 +79,8 @@ function seedTicket(row: {
|
||||
phone?: string | null;
|
||||
bookingId?: string | null;
|
||||
qr?: string;
|
||||
/** Seed the pre-sale payment a paid ticket was bought with, at this amount. */
|
||||
paidAmount?: number;
|
||||
}) {
|
||||
sqlite
|
||||
.prepare(
|
||||
@@ -100,6 +102,17 @@ function seedTicket(row: {
|
||||
row.qr ?? `QR-${row.id}`,
|
||||
new Date().toISOString()
|
||||
);
|
||||
if (row.paidAmount !== undefined) seedPayment(row.id, row.paidAmount, 'paid');
|
||||
}
|
||||
|
||||
function seedPayment(ticketId: string, amount: number, status: string, provider = 'bancard') {
|
||||
const now = new Date().toISOString();
|
||||
sqlite
|
||||
.prepare(
|
||||
`INSERT INTO payments (id, ticket_id, provider, amount, currency, status, paid_at, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'PYG', ?, ?, ?, ?)`
|
||||
)
|
||||
.run(`pay-${ticketId}`, ticketId, provider, amount, status, status === 'paid' ? now : null, now, now);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
@@ -134,7 +147,7 @@ beforeAll(() => {
|
||||
)
|
||||
.run(EVENT_ID, now, PRICE, now, now);
|
||||
|
||||
seedTicket({ id: 'tkt-paid', first: 'José', last: 'Núñez', status: 'confirmed', paymentStatus: 'paid', phone: '+595 981 234 567' });
|
||||
seedTicket({ id: 'tkt-paid', first: 'José', last: 'Núñez', status: 'confirmed', paymentStatus: 'paid', phone: '+595 981 234 567', paidAmount: PRICE });
|
||||
seedTicket({ id: 'tkt-unpaid', first: 'Ana', last: 'Group', status: 'confirmed', paymentStatus: 'unpaid', bookingId: 'bk-1' });
|
||||
seedTicket({ id: 'tkt-unpaid-2', first: 'Beto', last: 'Group', status: 'confirmed', paymentStatus: 'unpaid', bookingId: 'bk-1' });
|
||||
seedTicket({ id: 'tkt-cancelled', first: 'Carla', last: 'Gone', status: 'cancelled', paymentStatus: 'unpaid' });
|
||||
@@ -197,7 +210,7 @@ describe('door-checkin: existing ticket', () => {
|
||||
|
||||
const after = sqlite.prepare('SELECT checkin_at FROM tickets WHERE id = ?').get('tkt-paid').checkin_at;
|
||||
expect(after).toBe(before);
|
||||
expect(sqlite.prepare('SELECT COUNT(*) n FROM payments WHERE ticket_id = ?').get('tkt-paid').n).toBe(0);
|
||||
expect(sqlite.prepare("SELECT COUNT(*) n FROM payments WHERE ticket_id = ? AND source = 'door'").get('tkt-paid').n).toBe(0);
|
||||
});
|
||||
|
||||
it('settles an unpaid group-booking ticket in cash and checks in, in one call', async () => {
|
||||
@@ -323,7 +336,7 @@ describe('door-checkin: walk-ins', () => {
|
||||
|
||||
describe('undo', () => {
|
||||
it('reverts a plain check-in to its previous state', async () => {
|
||||
seedTicket({ id: 'tkt-undo', first: 'Undo', last: 'Me', status: 'confirmed', paymentStatus: 'paid' });
|
||||
seedTicket({ id: 'tkt-undo', first: 'Undo', last: 'Me', status: 'confirmed', paymentStatus: 'paid', paidAmount: PRICE });
|
||||
await post(`/api/events/${EVENT_ID}/door-checkin`, {
|
||||
ticketId: 'tkt-undo',
|
||||
idempotencyKey: 'key-undo-checkin',
|
||||
@@ -440,6 +453,17 @@ describe('door-summary', () => {
|
||||
expect(body.door.lines.length).toBe(body.door.count);
|
||||
expect(body.door.lines[0]).toHaveProperty('name');
|
||||
});
|
||||
|
||||
it('keeps pre-sale revenue at what was paid when the ticket price changes', async () => {
|
||||
sqlite.prepare('UPDATE events SET price = ? WHERE id = ?').run(99000, EVENT_ID);
|
||||
try {
|
||||
const { body } = await as(ADMIN, () => get(`/api/events/${EVENT_ID}/door-summary`));
|
||||
expect(body.presale).toEqual({ count: 2, total: PRICE * 2 });
|
||||
expect(body.door.total).toBe(PRICE * 6);
|
||||
} finally {
|
||||
sqlite.prepare('UPDATE events SET price = ? WHERE id = ?').run(PRICE, EVENT_ID);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Walk-in price & POS ====================
|
||||
|
||||
Reference in New Issue
Block a user