# Recurring expense

How a scheduled cost (insurance, a loan payment, parking, a tax) becomes an expected entry the owner confirms into the ledger or skips, shown in M-Gastos and in the alerts.

## What it does [#what-it-does]

A Recurring Expense (RF30, US14) schedules a cost on a car: insurance, a loan payment, parking, a tax or other, with an amount in the car's currency and a cadence. The cadence is `everyMonths` from an anchor date (`anchorOn`): 1 is monthly on its day ("every 10th"), 12 yearly on its date, anything up to 120 every N months. Every occurrence is counted from the anchor, so the 31st clamps per month and never drifts (`nextOccurrence` in `@autofolio/domain/reminder`).

Nothing reaches the ledger by itself. The cron tick turns each due occurrence into an **expected entry**: a `reminder` row with `recurring_expense_id`, due that day and titled with the payee or the kind (in the author's language). So the alert list, push and email of [the reminder job](/docs/features/service) show it as they show any reminder. The owner then confirms it with the actual amount and date, which writes the ★ ledger entry, or skips it.

Confirming books:

- insurance, parking, tax, other: an Expense (category insurance, other, tax, other) whose ★ ledger entry shares the expected entry's id, so M-Gastos and the TCO count it;
- a loan payment: `loan_interest` (a cost) and `loan_principal` (never an expense, RN05) ★ ledger entries, and the Loan's new ★ balance. The interest is a month at the Loan's latest balance and rate (an annual rate divided by 12), at most the payment; the owner can change it to match the statement. Without a rate the owner enters it.

## Procedures [#procedures]

| Procedure                                                                                         | Who                         | Failures                                                                                                                 |
| ------------------------------------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `recurringExpense.list` `{ vehicleId? }`                                                          | Members of the Organization | —                                                                                                                        |
| `recurringExpense.create` `{ id, vehicleId, kind, label?, amountMinor, everyMonths, firstDueOn }` | Members of the Organization | `INPUT_VALIDATION`, `NOT_FOUND` (car), `BAD_REQUEST` (not in the garage, a loan payment without a Loan), `CONFLICT` (id) |
| `recurringExpense.update` `{ id, kind?, label?, amountMinor?, everyMonths?, firstDueOn? }`        | Members of the Organization | `INPUT_VALIDATION`, `NOT_FOUND`, `BAD_REQUEST` (a loan payment without a Loan)                                           |
| `recurringExpense.delete` `{ id }`                                                                | Members of the Organization | `NOT_FOUND`                                                                                                              |
| `recurringExpense.expected` `{ id }`                                                              | Members of the Organization | `NOT_FOUND` (not open)                                                                                                   |
| `recurringExpense.confirm` `{ id, amountMinor, effectiveDate, interestMinor? }`                   | Members of the Organization | `INPUT_VALIDATION`, `NOT_FOUND`, `BAD_REQUEST` (car no longer owned), `CONFLICT` (skipped)                               |
| `recurringExpense.skip` `{ id }`                                                                  | Members of the Organization | `NOT_FOUND`, `CONFLICT` (confirmed)                                                                                      |

`list` answers the schedules by next due and the open expected entries, oldest first, each with a loan payment's split. A first date in the past starts at its next occurrence from today; a new date or interval restarts the schedule the same way. Deleting a schedule deletes its expected entries; confirmed costs stay in the ledger.

## Tick and idempotency [#tick-and-idempotency]

`pendingRecurringExpenses` (`recurring-expense.jobs.ts`) runs every minute: it reads at most 100 schedules of current cars due by today, inserts their expected entries (one per schedule and date, a unique index) and moves each schedule from the date it read to the next occurrence. A schedule far behind catches up an occurrence a tick. A sold car's schedule is no longer read.

The client generates a schedule's id; replaying it answers what was saved. Confirming or skipping again changes nothing.

## Data and isolation [#data-and-isolation]

`recurring_expense` has row-level security by Active Organization; tenant traffic can update only its kind, label, amount, interval and dates. Expected entries are `reminder` rows. No `audit_log` event: a schedule is not money of record, and a confirmed occurrence is a ★ ledger entry with its author (RNF12).

## App [#app]

M-Gastos shows "Upcoming recurring" (`recorrentes`) under the categories: the schedule due first ("Insurance · every 10th", its amount), or how many payments wait to be confirmed. It opens Recurring expenses (`/recurring`): To confirm, Scheduled, and Add (`add-recorrente`). The form (`/recurring-form`) picks the type, payee, amount, Monthly / Yearly / Every N months and the first due date; "Loan payment" is offered only for a car with a Loan. An expected entry (`/expected-expense`, also from its alert on the Service tab) confirms the amount and date, with the interest of a loan payment, or skips it. `autofolio://dev/M-Gastos` opens the seeded M3 with its insurance on the 10th; the Porsche's parking waits to be confirmed under All cars.

## Key files [#key-files]

- [`packages/api/src/features/recurring-expense/recurring-expense.service.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/recurring-expense/recurring-expense.service.ts)
- [`packages/api/src/features/recurring-expense/recurring-expense.jobs.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/recurring-expense/recurring-expense.jobs.ts)
- [`apps/mobile/src/features/recurring-expense/recurring-expense.screen.tsx`](https://github.com/CarlosZiegler/autofolio/blob/main/apps/mobile/src/features/recurring-expense/recurring-expense.screen.tsx)

