Recurring journal entries¶
Backend domain: app/graphql/recurring_journals/
Migration: add_recurring_journals
RBAC Path: RECURRING_JOURNALS
Overview¶
A RecurringJournal is a template (name + balanced lines + memo template) plus a cadence (frequency, interval, day-of-month/week, optional end-date / occurrence count). The backend tracks nextRunDate, lastRunAt, lastLedgerEntryId, and isActive.
materializeRecurringJournal(id) posts one occurrence on demand. runRecurringJournalsDueToday materializes every active journal whose nextRunDate <= today.
Date arithmetic: monthly cadences honor dayOfMonth with month-length clamping (e.g. day 31 in February → last day of February).
Setup prerequisites¶
None beyond having GL accounts to debit/credit.
GraphQL surface¶
Queries¶
recurringJournals: [RecurringJournal!]!
recurringJournal(recurringJournalId: UUID!): RecurringJournal!
RecurringJournal fields: id, name, memoTemplate, frequency (DAILY | WEEKLY | MONTHLY | QUARTERLY | YEARLY), interval, dayOfMonth, dayOfWeek, startDate, endDate, occurrencesRemaining, nextRunDate, lastRunAt, lastLedgerEntryId, isActive, plus nested lines: [RecurringJournalLine!]!.
RecurringJournalLine: id, recurringJournalId, accountId, debitAmount, creditAmount, memo.
Mutations¶
createRecurringJournal(recurringJournal: RecurringJournalInput!): RecurringJournal!
pauseRecurringJournal(recurringJournalId: UUID!): RecurringJournal!
resumeRecurringJournal(recurringJournalId: UUID!): RecurringJournal!
skipRecurringJournal(recurringJournalId: UUID!): RecurringJournal!
materializeRecurringJournal(recurringJournalId: UUID!): RecurringJournal!
runRecurringJournalsDueToday: Int!
RecurringJournalInput: name, frequency, interval, startDate, lines: [RecurringJournalLineInput!]!, plus optional memoTemplate, endDate, occurrencesRemaining, dayOfMonth, dayOfWeek.
RecurringJournalLineInput: accountId, debitAmount, creditAmount, memo.
Behaviour to handle in the UI¶
- The create form is essentially a manual-journal form plus a cadence section.
- Validate lines balance on the client too: sum of debits must equal sum of credits (backend will reject otherwise).
- Show
nextRunDateprominently in the list so users know what's coming. pauseRecurringJournal/resumeRecurringJournalshould be quick toggles in a row context menu.skipRecurringJournaladvancesnextRunDateby one cadence without posting — useful for "we already paid this manually this month".materializeRecurringJournalis the manual "Run now" button.
Error states¶
| Backend behaviour | When | Frontend message |
|---|---|---|
ValueError ("not balanced") |
Create with unbalanced lines | "Lines must balance: debits = credits." |
PeriodClosedError (from underlying ledger service) |
Materializing into a closed period | "Cannot post into a closed period. Reopen the period or change the run date." |
Suggested UX flow¶
- Recurring journals list with status badge (Active / Paused),
nextRunDate, last run, line count. - Create / edit form with cadence picker, line grid, balance check.
- Detail page showing the next 6 occurrences (frontend-computed from
nextRunDate+ cadence) and a history list (queryledgerEntriesfiltered byrecurring_journal_idonce that linkage is exposed — see follow-up).
Open follow-ups (not yet implemented)¶
runRecurringJournalsDueTodayis exposed as a mutation for manual triggering. There is no scheduled worker task yet that fires it daily — add arender_sdk.Workflowstask inworker/tasks.pybefore relying on auto-posting. For now, surface a "Run due journals" button in the admin area.LedgerEntrydoes not yet carry arecurring_journal_idsource link, so listing past occurrences requires matchinglast_ledger_entry_idplus walkingledger_entriesby memo. Either expose aentriesForRecurringJournal(id)query or store the source linkage.