Files
accounted/app/api/invoices/reminders/cron/route.ts
T
MattssonandClaude Fable 5 e92b5a59b2 fix(reminders): settings UI discloses that automatic sending is disabled (#2033)
* fix(reminders): settings UI discloses that automatic sending is disabled

The invoice reminder cron has answered 503 since May 2026 (PR #583), so
no automatic reminders are sent, but the settings UI still let users
configure reminder day levels as if sending worked.

Introduce REMINDERS_SENDING_ENABLED (lib/invoices/reminders-enabled.ts)
as the single shared flag read by both sides: the cron route uses it as
its 503 gate (with the original sending pipeline restored behind it, so
re-enabling later is one flag flip), and the invoice settings form shows
an attn notice while the flag is off. Schedule settings stay editable;
notice strings added to both sv and en locales.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtvffGr6uVk2J2Skuz6L98

* docs(reminders): correct the re-enable contract after skeptic review

The flag docblock, route docblock, and test header claimed flipping
REMINDERS_SENDING_ENABLED alone resumes sending. False on hosted: the
route has had no vercel.json cron entry since PR #559 and the crontab
ratchet pins it in INTENTIONALLY_UNSCHEDULED, and POST requires the cron
secret so no dashboard can trigger it. Rewrite the claims into the real
re-enable checklist and record the pre-flip prerequisites surfaced by
review: invoice_reminders lacks a unique (invoice_id, reminder_level)
constraint and the fee entry is booked before the reminder row, so a run
dying mid-batch double-books the fee; the backlog would get highest-level
reminders first. Comments and a test name only; no runtime change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtvffGr6uVk2J2Skuz6L98

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 21:43:19 +02:00

56 lines
2.0 KiB
TypeScript

import { NextResponse } from 'next/server'
import { processOverdueReminders } from '@/lib/invoices/reminder-processor'
import { REMINDERS_SENDING_ENABLED } from '@/lib/invoices/reminders-enabled'
import { getEmailService } from '@/lib/email/service'
import { withCronContext } from '@/lib/api/with-cron-context'
import { errorResponseFromCode } from '@/lib/errors/get-structured-error'
/**
* GET/POST /api/invoices/reminders/cron: sends overdue invoice reminders.
* Both verbs require the cron secret via withCronContext; POST mirrors GET
* so a secret-bearing operator can trigger a run outside the schedule.
*
* Gated behind REMINDERS_SENDING_ENABLED (off since May 2026, PR #583):
* while the flag is off this route answers 503 and nothing is sent, and
* the invoice settings UI reads the same flag to disclose that state.
* On hosted the route is also unscheduled (no vercel.json cron entry
* since PR #559), so the flag flip alone does not resume sending there;
* see lib/invoices/reminders-enabled.ts for the full re-enable checklist.
*/
export const GET = withCronContext('cron.invoice_reminders', async (_request, ctx) => {
if (!REMINDERS_SENDING_ENABLED) {
ctx.log.info('invoice reminders feature is disabled; skipping run')
return NextResponse.json({ disabled: true }, { status: 503 })
}
if (!getEmailService().isConfigured()) {
ctx.log.error('email service not configured; skipping reminder run')
return errorResponseFromCode('INVOICE_SEND_EMAIL_NOT_CONFIGURED', ctx.log, {
requestId: ctx.requestId,
})
}
const result = await processOverdueReminders()
ctx.log.info('reminder cron summary', {
processed: result.processed,
sent: result.sent,
failed: result.failed,
})
return NextResponse.json({
success: true,
processed: result.processed,
sent: result.sent,
failed: result.failed,
results: result.results.map((r) => ({
invoiceNumber: r.invoiceNumber,
reminderLevel: r.reminderLevel,
success: r.success,
error: r.error,
})),
})
})
export const POST = GET