When the master changes an account type with "New accounts only", it now serves the type's old rules as kept rule sets (firm.keptAccountRules), each for accounts dated inside [openedAt, closedAt). This build applies them: - getAccountConfig, the one rules lookup trading reads, puts a kept set's rule fields over the live type when the account's rules date falls in its window. Id, label, prefix, size and the projection fields stay the live type's, so every report still names the live type. - account_assignments gains rules_date and rules_date_manual. A new row is dated when the machine first sees the account, on the master's clock when this one runs behind and never before the catalog's last change. Auto dates come down to the broker's evidence: the end of the first fund day in Chicago, or a DXtrade registration instant. Evidence is applied before an account's first trade. Rows from before the migration get its time; unpinning keeps the row and its date. Backups carry both columns. - An account on a kept set stays on its type while its prefix and cached size still match, so a tie-break flip can't move it off its old rules. - Kept entries parse through the same normalizer as live types (now normalizeAccountType in lib/kept-rules.ts); a malformed one rejects the sync like a malformed type. The cached catalog records the parser that stored it, and a cache from an older parser is fetched in full rather than trusted with ?since= (this also brings in maxConcurrentAccounts). - Reports add keptRuleIds per firm and live counts per kept set per type. - The dashboard row and the account page show the rules date next to the account name, with an "old rules" chip saying what differs. Clicking the date edits it inline (PUT /api/accounts/:id/rules-date), previewing which rules the date lands on. Also: the master's per-firm cap on accounts traded at once (maxConcurrentAccounts), applied on top of the machine's own max concurrent when a batch is drawn and in copy trading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47 lines
2.1 KiB
TypeScript
47 lines
2.1 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { setRulesDate } from '@/lib/db';
|
|
import { sortableIso } from '@/lib/kept-rules';
|
|
|
|
type Params = { params: Promise<{ id: string }> };
|
|
|
|
/** A rules date may run this far ahead of the server's clock — room for a
|
|
* browser whose clock or time zone is ahead, not for dating accounts later. */
|
|
const MAX_FUTURE_MS = 24 * 60 * 60 * 1000;
|
|
|
|
function parseAccountId(raw: string): number | null {
|
|
const id = parseInt(raw, 10);
|
|
return Number.isNaN(id) ? null : id;
|
|
}
|
|
|
|
/** Set an account's rules date by hand. The date decides which of its type's
|
|
* rule sets it trades on — one dated before a firm's rule change keeps the
|
|
* old rules — and a date set here is never moved by broker evidence again.
|
|
* The trade path reads it per tick, so nothing needs re-resolving. */
|
|
export async function PUT(req: NextRequest, { params }: Params) {
|
|
const { id: idStr } = await params;
|
|
const accountId = parseAccountId(idStr);
|
|
if (accountId === null) return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
|
|
|
|
const body = await req.json().catch(() => null) as { rulesDate?: unknown } | null;
|
|
const raw = body?.rulesDate;
|
|
const ms = typeof raw === 'string' ? Date.parse(raw) : NaN;
|
|
if (!Number.isFinite(ms)) {
|
|
return NextResponse.json({ error: 'rulesDate must be an ISO date' }, { status: 400 });
|
|
}
|
|
if (ms > Date.now() + MAX_FUTURE_MS) {
|
|
return NextResponse.json({ error: 'A rules date cannot be more than a day ahead' }, { status: 400 });
|
|
}
|
|
|
|
// Stored as a plain four-digit-year toISOString(), the one format every
|
|
// rules-date writer uses — lowerAutoRulesDate compares them as strings,
|
|
// so nothing before 2000 (or that doesn't print as one) gets in.
|
|
const rulesDate = sortableIso(ms);
|
|
if (!rulesDate) {
|
|
return NextResponse.json({ error: 'A rules date cannot be before 2000' }, { status: 400 });
|
|
}
|
|
if (!setRulesDate(accountId, rulesDate, true)) {
|
|
return NextResponse.json({ error: 'Unknown account' }, { status: 404 });
|
|
}
|
|
return NextResponse.json({ ok: true, rulesDate });
|
|
}
|