Files
accounted/app/api/bookkeeping/accruals/post-due/cron/route.ts
T
Mattsson e11f70b347 Bug/gh issues fiz (#1103)
* refactor: optimize page loading and data fetching

* fix: resolve recurring production runtime errors

* feat: add MCP company and customer updates

* fix: handle year-end tax adjustments

* feat: harden annual report compliance

* fix: expand invoice logo and font support

* fix: sanitize API route error responses

* fix: sanitize user-facing error messages

* feat: persist onboarding and tax assessment notices

* fix: reduce cloud backup audit churn

* feat: refine invoice editor layout

* fix: show saved tax adjustments in INK2

* fix: complete annual report API mappings

* docs: record operational safeguards and decisions

* fix: harden annual report review findings

* fix: adjust column span for description based on VAT registration

* New css class name
2026-07-21 23:00:15 +02:00

99 lines
3.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 { fetchAllRows } from '@/lib/supabase/fetch-all'
import { postDueInstallments } from '@/lib/bookkeeping/accruals/service'
import { firstOfMonth } from '@/lib/bookkeeping/accruals/compute'
import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message'
ensureInitialized()
/**
* GET /api/bookkeeping/accruals/post-due/cron, daily 05:15 UTC.
*
* Posts the monthly periodiseringsverifikat (source_type 'accrual') for every
* pending installment whose calendar month has begun. Companies run in
* isolated try/catch; one company's failure never blocks the rest. Per-
* installment failures are recorded on the row (last_error) by the service
* and retried on the next run: the periodiseringar page surfaces them.
*
* Idempotency: posting flips the installment pending→posted with a CAS
* claim, so a cron retry (or a concurrent manual "Bokför förfallna") can
* never double-book a month.
*/
export const GET = withCronContext('cron.accrual_postings', async (_request, ctx) => {
const supabase = createServiceClient()
const todayIso = new Date().toISOString().slice(0, 10)
// fetchAllRows pages past PostgREST's 1000-row cap: a single unpaginated
// select would silently drop companies once total due installments exceed
// the cap, permanently starving the ones sorted last.
let rows: Array<{ company_id: string }>
try {
rows = await fetchAllRows<{ company_id: string }>(({ from, to }) =>
supabase
.from('accrual_schedule_installments')
.select('company_id')
.eq('status', 'pending')
.lte('period_month', firstOfMonth(todayIso))
.order('id', { ascending: true })
.range(from, to),
)
} catch (error) {
ctx.log.error('failed to load due accrual installments', error as Error)
return NextResponse.json(
{ success: false, error: error instanceof Error ? getUserErrorMessage(error) : 'unknown' },
{ status: 500 },
)
}
const companyIds = Array.from(new Set(rows.map((row) => row.company_id)))
ctx.log.info('accrual posting cron starting', {
companyCount: companyIds.length,
todayIso,
})
const results: Array<{
companyId: string
posted: number
failed: number
skipped: number
}> = []
const summary = await ctx.forEach('company', companyIds, async (companyId, itemCtx) => {
const result = await postDueInstallments(supabase, companyId)
results.push({
companyId,
posted: result.posted,
failed: result.failed,
skipped: result.skipped,
})
if (result.failed > 0) {
itemCtx.log.warn('some accrual installments failed to post', {
companyId,
failed: result.failed,
})
}
})
ctx.log.info('accrual posting cron summary', {
total: summary.total,
succeeded: summary.succeeded,
failed: summary.failed,
posted: results.reduce((sum, r) => sum + r.posted, 0),
})
return NextResponse.json({
success: true,
total: summary.total,
succeeded: summary.succeeded,
failed: summary.failed,
failures: summary.failures,
results,
})
})
export const POST = GET