Close pre-sale registration a configurable time before events start.
Events gain nullable presale_closure_enabled / presale_close_minutes_before overrides; null inherits the new site_settings defaults (enabled, 120 min). A shared resolver computes the effective cutoff, which the public event API exposes as presaleClosesAt and the public booking endpoint enforces. Door and admin ticket creation are not gated. Admin: toggle + duration picker in the event modal (pre-filled from the site default) and a matching default card on Settings › General. Public: the event page shows "Registration Closed" after the cutoff and the checkout page redirects back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
745af4184f
commit
59acc32a46
@@ -0,0 +1,75 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
resolvePresaleClosure,
|
||||
isPresaleClosed,
|
||||
DEFAULT_PRESALE_CLOSE_MINUTES,
|
||||
} from './presale.js';
|
||||
|
||||
const START = '2030-01-01T20:00:00.000Z';
|
||||
const startMs = new Date(START).getTime();
|
||||
const minutes = (n: number) => n * 60_000;
|
||||
|
||||
describe('resolvePresaleClosure', () => {
|
||||
it('falls back to the built-in defaults when neither event nor settings specify anything', () => {
|
||||
const r = resolvePresaleClosure({ startDatetime: START }, null);
|
||||
expect(r.enabled).toBe(true);
|
||||
expect(r.minutesBefore).toBe(DEFAULT_PRESALE_CLOSE_MINUTES);
|
||||
expect(r.closesAt?.getTime()).toBe(startMs - minutes(DEFAULT_PRESALE_CLOSE_MINUTES));
|
||||
});
|
||||
|
||||
it('inherits from site settings when the event has null overrides', () => {
|
||||
const r = resolvePresaleClosure(
|
||||
{ startDatetime: START, presaleClosureEnabled: null, presaleCloseMinutesBefore: null },
|
||||
{ presaleClosureEnabled: true, presaleCloseMinutesBefore: 30 }
|
||||
);
|
||||
expect(r.minutesBefore).toBe(30);
|
||||
expect(r.closesAt?.getTime()).toBe(startMs - minutes(30));
|
||||
});
|
||||
|
||||
it('lets the event override the site settings', () => {
|
||||
const r = resolvePresaleClosure(
|
||||
{ startDatetime: START, presaleClosureEnabled: true, presaleCloseMinutesBefore: 1440 },
|
||||
{ presaleClosureEnabled: false, presaleCloseMinutesBefore: 30 }
|
||||
);
|
||||
expect(r.enabled).toBe(true);
|
||||
expect(r.closesAt?.getTime()).toBe(startMs - minutes(1440));
|
||||
});
|
||||
|
||||
it('returns closesAt null when closure is disabled (site-wide or per event)', () => {
|
||||
expect(resolvePresaleClosure({ startDatetime: START }, { presaleClosureEnabled: false }).closesAt).toBeNull();
|
||||
expect(
|
||||
resolvePresaleClosure({ startDatetime: START, presaleClosureEnabled: false }, { presaleClosureEnabled: true }).closesAt
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('accepts PostgreSQL 0/1 integers and string minutes', () => {
|
||||
const r = resolvePresaleClosure(
|
||||
{ startDatetime: new Date(START), presaleClosureEnabled: 1, presaleCloseMinutesBefore: '45' },
|
||||
{ presaleClosureEnabled: 0, presaleCloseMinutesBefore: 10 }
|
||||
);
|
||||
expect(r.enabled).toBe(true);
|
||||
expect(r.minutesBefore).toBe(45);
|
||||
const off = resolvePresaleClosure({ startDatetime: START, presaleClosureEnabled: 0 }, { presaleClosureEnabled: 1 });
|
||||
expect(off.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('closes at the event start when minutes is 0', () => {
|
||||
const r = resolvePresaleClosure({ startDatetime: START, presaleCloseMinutesBefore: 0 }, null);
|
||||
expect(r.closesAt?.getTime()).toBe(startMs);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPresaleClosed', () => {
|
||||
const event = { startDatetime: START, presaleClosureEnabled: true, presaleCloseMinutesBefore: 60 };
|
||||
|
||||
it('is open before the cutoff and closed from the cutoff onwards', () => {
|
||||
const cutoff = startMs - minutes(60);
|
||||
expect(isPresaleClosed(event, null, cutoff - 1)).toBe(false);
|
||||
expect(isPresaleClosed(event, null, cutoff)).toBe(true);
|
||||
expect(isPresaleClosed(event, null, startMs + minutes(5))).toBe(true);
|
||||
});
|
||||
|
||||
it('never closes when closure is disabled', () => {
|
||||
expect(isPresaleClosed({ ...event, presaleClosureEnabled: false }, null, startMs + minutes(60))).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user