feat(invoices): quarterly, half-yearly and yearly recurring invoice schedules (#1438)
* fix(mcp): offer the link tool in the uncategorized-transactions VAT blocker The gnubok_vat_close_check blocker hint only named categorize/auto-match, both of which create new bookkeeping. For a transaction whose affarshandelse is already booked on an existing verifikat, following the hint would double-book, so agents dead-ended the case into "contact support" (2026-08-06 support mail from Orto Engineering). The hint now also names gnubok_link_transaction_to_journal_entry, is extracted as an exported constant pinned by a test, and the tool joins the categorize_month recommended loadout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): quarterly, half-yearly and yearly recurring schedules User request: recurring invoice schedules only supported monthly cadence. Adds interval_months (SMALLINT 1-12, default 1) to recurring_invoice_schedules; the UI offers manadsvis/kvartalsvis/ halvarsvis/arsvis presets while API and MCP accept any 1-12. The cron advances next_run_date by whole intervals from the due date, and the new rollNextRunDateForward() helper rolls missed or edited interval schedules on their own month grid so a quarterly Jan/Apr/Jul/Oct schedule missed in an outage rolls Jan 15 to Apr 15, never Feb 15. Monthly (interval 1) keeps its existing today-anchored recompute semantics unchanged. Changing the interval alone never touches next_run_date: the new cadence applies from the next run, so an edit can never pull a send earlier. Existing rows default to 1 and behave byte-identically. The MCP slice of this feature (interval_months on the three recurring-schedule tools in server.ts) was committed in d2600907f alongside the VAT-blocker hint fix by a parallel session sharing this worktree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(invoices): address PR #1438 review findings CodeRabbit round 1, all three findings: - MCP descriptions now state the full accepted interval range (any integer 1-12) instead of enumerating only the 1/3/6/12 presets, and qualify that changing ONLY interval_months leaves next_run_date untouched. - assertValidCadence rejects fractional day_of_month. - rollNextRunDateForward rejects calendar-invalid anchors that pass the shape regex (2026-13-05, 2026-02-31), with regression tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d41ef2a909
commit
a0ca692fed
@@ -125,6 +125,7 @@ import {
|
||||
import {
|
||||
computeInitialRunDate,
|
||||
computeNextRunDate,
|
||||
rollNextRunDateForward,
|
||||
getStockholmDateHour,
|
||||
} from '@/lib/invoices/recurring-schedule-service'
|
||||
import { UpdateInvoiceParamsSchema } from '@/lib/pending-operations/schemas/update-invoice'
|
||||
@@ -559,6 +560,7 @@ async function commitCreateRecurringSchedule(
|
||||
customer_id: validated.customer_id,
|
||||
name: validated.name,
|
||||
day_of_month: validated.day_of_month,
|
||||
interval_months: validated.interval_months,
|
||||
send_hour: validated.send_hour,
|
||||
payment_terms_days: validated.payment_terms_days,
|
||||
currency: validated.currency,
|
||||
@@ -610,6 +612,7 @@ async function commitCreateRecurringSchedule(
|
||||
name: validated.name,
|
||||
customer_id: validated.customer_id,
|
||||
day_of_month: validated.day_of_month,
|
||||
interval_months: validated.interval_months,
|
||||
send_hour: validated.send_hour,
|
||||
currency: validated.currency,
|
||||
auto_send: validated.auto_send,
|
||||
@@ -644,7 +647,7 @@ async function commitUpdateRecurringSchedule(
|
||||
|
||||
const { data: existing, error: existingError } = await supabase
|
||||
.from('recurring_invoice_schedules')
|
||||
.select('id, status, auto_send, customer_id, day_of_month, next_run_date')
|
||||
.select('id, status, auto_send, customer_id, day_of_month, interval_months, next_run_date')
|
||||
.eq('id', scheduleId)
|
||||
.eq('company_id', companyId)
|
||||
.maybeSingle()
|
||||
@@ -691,16 +694,29 @@ async function commitUpdateRecurringSchedule(
|
||||
const dayChanged =
|
||||
changes.day_of_month !== undefined && changes.day_of_month !== existing.day_of_month
|
||||
const effectiveDay = changes.day_of_month ?? existing.day_of_month
|
||||
const effectiveInterval = changes.interval_months ?? existing.interval_months ?? 1
|
||||
const { date: todayStockholm } = getStockholmDateHour(new Date())
|
||||
const stockholmToday = new Date(`${todayStockholm}T00:00:00Z`)
|
||||
|
||||
const staleOnReactivate = reactivating && existing.next_run_date <= todayStockholm
|
||||
if (staleOnReactivate || dayChanged) {
|
||||
const rolled = computeInitialRunDate(stockholmToday, effectiveDay)
|
||||
updateRow.next_run_date =
|
||||
rolled === todayStockholm
|
||||
? computeNextRunDate(stockholmToday, effectiveDay)
|
||||
: rolled
|
||||
if (effectiveInterval === 1) {
|
||||
// Monthly keeps its long-standing today-anchored semantics.
|
||||
const rolled = computeInitialRunDate(stockholmToday, effectiveDay)
|
||||
updateRow.next_run_date =
|
||||
rolled === todayStockholm
|
||||
? computeNextRunDate(stockholmToday, effectiveDay)
|
||||
: rolled
|
||||
} else {
|
||||
// Interval schedules roll on their own month grid so an edit or
|
||||
// reactivation cannot shift a quarterly schedule off its phase.
|
||||
updateRow.next_run_date = rollNextRunDateForward(
|
||||
existing.next_run_date,
|
||||
stockholmToday,
|
||||
effectiveDay,
|
||||
effectiveInterval,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// A conscious reactivation invalidates any lingering warning.
|
||||
|
||||
Reference in New Issue
Block a user