50165d2369
An ePay extract is valid 30 days after issuance; at 45 days it's worthless, so
delete the DB row + its MinIO object to declutter the list and free storage.
Only type='epay' rows are touched — the free cf-intern extracts are kept.
- cleanupExpiredEpayExtracts({olderThanDays=45, dryRun}): COALESCE(documentDate,
createdAt) < cutoff; deletes MinIO objects (batched, best-effort) then the
rows. Idempotent.
- Self-contained scheduler (epay-cleanup.ts, same pattern as
auto-refresh-scheduler): boot run (+90s) then every 24h, started from
instrumentation.ts. Works with zero external config; idempotent so a
redeploy/interrupt is harmless.
- GET/POST /api/ancpi/cleanup for manual preview (dry-run) / on-demand run —
staff session OR cron Bearer (EPAY_CLEANUP_CRON_SECRET /
NOTIFICATION_CRON_SECRET); excluded from the auth middleware (fail-closed
in-route). ?days overrides the window.
- deleteCfExtractObjects() helper in epay-storage.
Verified on prod: 0 epay rows currently qualify (all recent); the 8 old intern
rows are correctly left untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
// GET /api/ancpi/cleanup?dryRun=1 — preview what would be deleted
|
|
// POST /api/ancpi/cleanup — run the cleanup now
|
|
//
|
|
// On-demand control over the 45-day ePay extract auto-cleanup (the scheduler
|
|
// in epay-cleanup.ts runs it automatically on boot + every 24h). Useful to
|
|
// preview (dryRun) before trusting the automatic run, or to trigger it now.
|
|
//
|
|
// Auth: a staff session (requireCfAccess), OR a cron Bearer token
|
|
// (EPAY_CLEANUP_CRON_SECRET / NOTIFICATION_CRON_SECRET) so an external
|
|
// scheduler can call it. ?days overrides the retention window.
|
|
|
|
import { NextResponse } from "next/server";
|
|
import {
|
|
cleanupExpiredEpayExtracts,
|
|
EPAY_RETENTION_DAYS,
|
|
} from "@/modules/parcel-sync/services/epay-cleanup";
|
|
import { requireCfAccess } from "@/core/auth/cf-access";
|
|
|
|
export const runtime = "nodejs";
|
|
export const dynamic = "force-dynamic";
|
|
|
|
function cronAuthorized(req: Request): boolean {
|
|
const secret =
|
|
process.env.EPAY_CLEANUP_CRON_SECRET ?? process.env.NOTIFICATION_CRON_SECRET;
|
|
if (!secret) return false;
|
|
const auth = req.headers.get("authorization") ?? "";
|
|
return auth === `Bearer ${secret}`;
|
|
}
|
|
|
|
async function authorize(req: Request): Promise<boolean> {
|
|
if (cronAuthorized(req)) return true;
|
|
const access = await requireCfAccess();
|
|
return access.ok;
|
|
}
|
|
|
|
function parseDays(req: Request): number {
|
|
const raw = new URL(req.url).searchParams.get("days");
|
|
const n = raw ? parseInt(raw, 10) : NaN;
|
|
return Number.isFinite(n) && n > 0 ? n : EPAY_RETENTION_DAYS;
|
|
}
|
|
|
|
export async function GET(req: Request) {
|
|
if (!(await authorize(req))) {
|
|
return NextResponse.json({ error: "Neautorizat." }, { status: 401 });
|
|
}
|
|
// GET is always a dry-run (no side effects) — safe to preview from a browser.
|
|
const result = await cleanupExpiredEpayExtracts({
|
|
olderThanDays: parseDays(req),
|
|
dryRun: true,
|
|
});
|
|
return NextResponse.json(result);
|
|
}
|
|
|
|
export async function POST(req: Request) {
|
|
if (!(await authorize(req))) {
|
|
return NextResponse.json({ error: "Neautorizat." }, { status: 401 });
|
|
}
|
|
const dryRun = new URL(req.url).searchParams.get("dryRun") === "1";
|
|
const result = await cleanupExpiredEpayExtracts({
|
|
olderThanDays: parseDays(req),
|
|
dryRun,
|
|
});
|
|
return NextResponse.json(result);
|
|
}
|