* feat(voucher): add create voucher and correct entry previews; update commit methods * feat: add support for pending operations in API key scopes and OAuth client management - Introduced new API key scopes for reading and approving pending operations. - Updated the scope groups to include pending operations. - Added new tools for listing and managing pending operations. - Implemented OAuth client registration and revocation endpoints. - Created a UI panel for managing OAuth clients, including registration and revocation. - Added tests for pending operations tools and OAuth allowlist functionality. - Implemented a database migration for OAuth client registrations with appropriate policies and constraints. * feat: Implement OAuth client registration rate limiting and enhance security measures - Added IP-based rate limiting to the OAuth client registration endpoint to prevent enumeration attacks. - Introduced a service-role client for allowlist lookups, ensuring trust boundaries are maintained. - Updated error responses to be uniform across different types of redirect URI validation failures. - Enhanced tests to reflect changes in OAuth scope handling, ensuring fallback to read-only scopes when no scopes are provided. - Improved handling of high-risk pending operations, requiring explicit confirmation for approvals. - Added audit logging for OAuth client revocations and pending operation approvals/rejections to maintain a security audit trail. - Refactored API key scope management to include default read-only scopes for OAuth-issued keys and added segregation-of-duties checks. * feat: add recurring invoice scheduling functionality - Implemented recurring invoice schedules with a new database schema. - Created API routes for managing recurring invoices (GET and POST). - Added cron job to automatically generate invoices based on schedules. - Developed service functions for computing next run dates and executing schedules. - Added tests for the new functionality, including validation and success cases. - Introduced error handling for various scenarios in the invoice creation process. * feat: refine VAT rate validation and enhance recurring invoice handling
138 lines
4.3 KiB
TypeScript
138 lines
4.3 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { ensureInitialized } from '@/lib/init'
|
|
import { withCronContext } from '@/lib/api/with-cron-context'
|
|
import { createServiceClient } from '@/lib/supabase/server'
|
|
import {
|
|
executeRecurringSchedule,
|
|
computeNextRunDate,
|
|
} from '@/lib/invoices/recurring-schedule-service'
|
|
import type {
|
|
RecurringInvoiceSchedule,
|
|
RecurringInvoiceScheduleItem,
|
|
} from '@/types'
|
|
|
|
ensureInitialized()
|
|
|
|
type DueSchedule = RecurringInvoiceSchedule & { items: RecurringInvoiceScheduleItem[] }
|
|
|
|
/**
|
|
* GET /api/invoices/recurring/cron — daily 06:30 UTC.
|
|
*
|
|
* Spawns invoices for every active schedule whose next_run_date is today or
|
|
* earlier. Each schedule runs in isolated try/catch so a failure on one
|
|
* doesn't block the rest. On success: bump next_run_date, last_run_at,
|
|
* last_invoice_id, generated_count. On failure: leave next_run_date alone so
|
|
* tomorrow's run retries; pause the schedule only if the same error recurs
|
|
* across days (out of scope for v1 — let the user investigate).
|
|
*/
|
|
export const GET = withCronContext('cron.recurring_invoices', async (_request, ctx) => {
|
|
const supabase = createServiceClient()
|
|
|
|
const today = new Date()
|
|
const todayIso = today.toISOString().slice(0, 10)
|
|
|
|
const { data: due, error } = await supabase
|
|
.from('recurring_invoice_schedules')
|
|
.select('*, items:recurring_invoice_schedule_items(*)')
|
|
.eq('status', 'active')
|
|
.lte('next_run_date', todayIso)
|
|
|
|
if (error) {
|
|
ctx.log.error('failed to load due recurring schedules', error)
|
|
return NextResponse.json(
|
|
{ success: false, error: error.message },
|
|
{ status: 500 },
|
|
)
|
|
}
|
|
|
|
const schedules = (due ?? []) as DueSchedule[]
|
|
|
|
ctx.log.info('recurring invoice cron starting', {
|
|
dueCount: schedules.length,
|
|
todayIso,
|
|
})
|
|
|
|
type RunResult = {
|
|
scheduleId: string
|
|
invoiceId?: string
|
|
invoiceNumber?: string | null
|
|
autoSent?: boolean
|
|
warning?: string | null
|
|
skipped?: boolean
|
|
skipReason?: string
|
|
error?: string
|
|
}
|
|
const results: RunResult[] = []
|
|
|
|
const summary = await ctx.forEach('schedule', schedules, async (schedule, itemCtx) => {
|
|
// Idempotency: skip if already ran today. Protects against cron retries
|
|
// within the same UTC day; cheaper than a Postgres advisory lock and the
|
|
// window we're protecting (one row, ~seconds) is tiny.
|
|
if (schedule.last_run_at) {
|
|
const lastRunDay = schedule.last_run_at.slice(0, 10)
|
|
if (lastRunDay >= todayIso) {
|
|
itemCtx.log.info('schedule already ran today; skipping')
|
|
results.push({
|
|
scheduleId: schedule.id,
|
|
skipped: true,
|
|
skipReason: 'already_ran_today',
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
const result = await executeRecurringSchedule(supabase, schedule, today)
|
|
|
|
const nextRunDate = computeNextRunDate(today, schedule.day_of_month)
|
|
const { error: updateError } = await supabase
|
|
.from('recurring_invoice_schedules')
|
|
.update({
|
|
next_run_date: nextRunDate,
|
|
last_run_at: today.toISOString(),
|
|
last_invoice_id: result.invoiceId,
|
|
last_run_warning: result.warning,
|
|
generated_count: schedule.generated_count + 1,
|
|
})
|
|
.eq('id', schedule.id)
|
|
.eq('company_id', schedule.company_id)
|
|
|
|
if (updateError) {
|
|
// The invoice exists. If we don't mark the schedule as ran, tomorrow's
|
|
// cron would spawn a duplicate. Surface this loudly.
|
|
itemCtx.log.error(
|
|
'invoice created but failed to update schedule — manual cleanup may be needed',
|
|
updateError,
|
|
{ scheduleId: schedule.id, invoiceId: result.invoiceId },
|
|
)
|
|
throw new Error(
|
|
`schedule update failed after invoice ${result.invoiceId} created: ${updateError.message}`,
|
|
)
|
|
}
|
|
|
|
results.push({
|
|
scheduleId: schedule.id,
|
|
invoiceId: result.invoiceId,
|
|
invoiceNumber: result.invoiceNumber,
|
|
autoSent: result.autoSent,
|
|
warning: result.warning,
|
|
})
|
|
})
|
|
|
|
ctx.log.info('recurring invoice cron summary', {
|
|
total: summary.total,
|
|
succeeded: summary.succeeded,
|
|
failed: summary.failed,
|
|
})
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
total: summary.total,
|
|
succeeded: summary.succeeded,
|
|
failed: summary.failed,
|
|
failures: summary.failures,
|
|
results,
|
|
})
|
|
})
|
|
|
|
export const POST = GET
|