* 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>
56 lines
2.0 KiB
TypeScript
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
|