# Services and reminders

How a service is recorded into the ledger with its items, reading and private receipt, how a correction keeps its history, and how reminders are due by date or distance with a forecast from the car's readings.

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

A Service is a maintenance record (RF12): type, date, odometer, shop, items, note and a private receipt. Recording one writes, in one transaction:

- a ★ `ledger_entry` of kind `operating`, category `maintenance`: the total in the car's currency (RN02), the shop as vendor, the odometer and the author;
- the `service` row: title, date, odometer, shop, items (`{ name, amountMinor? }`), note, trust and receipt;
- with an odometer value, a ★ `odometer_reading` with source `service` (RN07, as for an [Expense](/docs/features/expense));
- with a receipt, a `vehicle_document` of kind `receipt`, and the trust becomes `with_proof`;
- with `reminderId` or `next`, the reminder it fulfils or schedules (below).

The items only explain the total: priced lines add up to at most the total, and exactly to it when every line is priced. Zero is a valid total (warranty work). Only a car with an open Ownership Period takes a service.

`service.create` answers `saved`, or `odometer_reason_required` with `previousKm` when the reading is lower than the one before its date (nothing written; send again with `odometerReason`). The client generates the `id`; a replay answers the first save (RNF04).

## Trust [#trust]

`trust` is the record's confidence indicator: `declared`, `with_proof` (a file is attached; it does not prove the document is genuine) or `partner_verified`. Tenant traffic can never write `partner_verified` (row-level security on `service` and `ledger_entry`); only the partner path (v2) will. A verified service cannot be corrected by the User (`CONFLICT`).

## Corrections and history (ADR-01, RNF12) [#corrections-and-history-adr-01-rnf12]

`service.correct` is the pencil on M-Servico. It writes a new `correction` ledger entry with the current values overridden by the given ones, naming the entry it corrects, with the `reason`; the original stays. The `service` row then points at the correction and takes the new title, items, shop and note. Items and total are corrected together. A changed odometer appends a reading, flagged with the reason when it is lower. Adding `receiptFileId` attaches a receipt later.

`service.get` returns `history`: the original and each correction, oldest first, with `recordedAt`, `reason`, total, date, odometer and shop. "Edited once" is `history.length - 1`.

## Reminders (RF13, US16) [#reminders-rf13-us16]

A Reminder is due by date (`dueOn`), by distance (`dueKm`), or both, whichever comes first. One with `everyKm`/`everyMonths` repeats: it is a plan item (M-Manutencao's Plan segment). `basis` explains the date: `date` ("Date you entered"), `mileage_forecast` ("Based on your average of …"), `interval` ("Manufacturer interval": accepted unchanged from the catalog's schedule, kept while the interval is not edited).

`reminder.list` answers the car's open reminders, most urgent first, with:

- `lastReading` and `kmPerMonth`: the car's last reading and its average distance per month over the year up to it (`averageKmPerMonth`; null before a month of readings);
- per reminder, `remainingKm` from the last reading (negative when overdue), `forecastOn` (when the average reaches `dueKm`) and `nextOn`, the earlier of `dueOn` and `forecastOn`, which orders the list.

The forecast is never stored; it is computed on every read by `forecastReminder` in `@autofolio/domain/reminder`, which the client may also use offline.

## Alerts (US17, RF14) [#alerts-us17-rf14]

Reminders work without push. `reminder.alerts` is the in-app alert list across the household's current cars: every open service reminder, and a document's due date (US18) once it is due soon or past (an expiring document). `reminderUrgency` in `@autofolio/domain/reminder` gives each one its `urgency`:

- `overdue` on facts only: the last reading is past `dueKm`, or `dueOn` is gone by;
- `soon` when `nextOn` (the date, or the forecast from the last reading and the average distance per month) is within `DUE_SOON_DAYS` (30), a forecast already passed included;
- `later` otherwise, a distance on a car without an average among them.

The list comes overdue first, then due soon, then later (`byUrgency`), each with its car's `kmPerMonth`. Sold and wishlist cars have no alerts.

The `reminders` job on the [scheduled tick](/docs/features/scheduled-jobs) sends the same alerts. During each data region's morning hour (07 UTC for the EU, 11 for Brazil, 14 for the US), every tick reads the alerts of households where a Member asked for push or email, and writes each one that is due soon or overdue to `reminder_notification` once per reminder, window and occurrence (its due date and distance then): a rescheduled reminder notifies again, a replayed tick never does. It then sends each Member who asked a push per registered device and one email digest with the new alerts, in their language (`reminder.messages.ts`), with the car's make and model and never a plate, VIN or amount. Sends are never retried: the in-app list always has them. `reminder_notification` is system-only, like `push_delivery`.

A reminder created with only an interval is first due that far from today and the last reading. A distance-only interval on a car without a reading is `BAD_REQUEST`.

## Plan suggestions (US19, RF29) [#plan-suggestions-us19-rf29]

`reminder.suggestions` offers a schedule for the car's Plan segment. When the car's Catalog Entry has a `service_plan`, it is the manufacturer's (`source: "manufacturer"`). NHTSA vPIC has no maintenance data, so today no entry has one, and the answer is AutoFolio's curated generic plan for the car's powertrain (`source: "generic"`, `GENERIC_SERVICE_PLANS` in `db/schema/garage.enums.ts`): combustion, hybrid or electric, from the catalog's fuel (`powertrainOf`). When the catalog does not know the fuel (`knownPowertrain: null`, a manual car), the client's `powertrain` picks the plan, combustion by default. The app always labels a generic plan as generic, never as the manufacturer's. Each item's `task` is a locale key (`af.oil_change`).

- Accept: `reminder.create` with `planTask`. Accepted unchanged from the manufacturer's schedule, its basis is `interval`; edited, or from the generic plan, it is forecast like any other.
- A task is not suggested again once any reminder of the car came from it (open, done or removed), or while it is dismissed: `reminder.dismissSuggestion` adds it to `vehicle.dismissed_plan_tasks`, `reminder.restoreSuggestions` empties it.
- The app also hides a suggestion the plan already has under the same name.

### Scheduling from a service [#scheduling-from-a-service]

- `service.create` with `reminderId` fulfils that reminder: a repeating one moves to its next occurrence from this service's date and odometer and points at this service; a one-off one is `done`.
- `service.create` with `next: { everyKm?, everyMonths? }` schedules the next one (a new reminder titled like the service, or the fulfilled one with the new interval).
- `service.scheduleNext` is M-Servico's "Next change scheduled" card: it creates, or moves, the reminder scheduled from that service. `service.get` returns it as `next`.

## Receipts: five-minute links (RF15) [#receipts-five-minute-links-rf15]

The receipt is a file in the Organization's private storage. `vehicleDocument.url` returns a presigned link that expires after 300 seconds (`VEHICLE_DOCUMENT_URL_SECONDS`), with `expiresAt` and `mimeType`; ask again when it expires. Another Organization's document is `NOT_FOUND`.

## Procedures [#procedures]

| Procedure                                                                                                                                                    | Failures                                                                                                                                                   |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `service.create` `{ id, vehicleId, title, performedOn, shop?, odometerKm?, odometerReason?, items, amountMinor, note?, receiptFileId?, reminderId?, next? }` | `INPUT_VALIDATION`, `NOT_FOUND` (car, receipt file, reminder), `BAD_REQUEST` (no open ownership period; interval without a reading), `CONFLICT` (id taken) |
| `service.correct` `{ id, correctionId, reason, …fields, receiptFileId? }`                                                                                    | `INPUT_VALIDATION`, `NOT_FOUND`, `CONFLICT` (id taken, partner-verified)                                                                                   |
| `service.get` `{ id }`                                                                                                                                       | `NOT_FOUND`                                                                                                                                                |
| `service.list` `{ vehicleId, limit ≤ 100 }`                                                                                                                  | `INPUT_VALIDATION`                                                                                                                                         |
| `service.scheduleNext` `{ id, everyKm?, everyMonths? }`                                                                                                      | `INPUT_VALIDATION`, `NOT_FOUND`, `BAD_REQUEST`                                                                                                             |
| `reminder.list` `{ vehicleId, limit ≤ 100 }`                                                                                                                 | `NOT_FOUND` (car)                                                                                                                                          |
| `reminder.alerts` `{ limit ≤ 100 }`                                                                                                                          | `INPUT_VALIDATION`                                                                                                                                         |
| `reminder.create` `{ id, vehicleId, title, dueOn?, dueKm?, everyKm?, everyMonths?, planTask? }`                                                              | `INPUT_VALIDATION`, `NOT_FOUND` (car), `BAD_REQUEST`, `CONFLICT`                                                                                           |
| `reminder.update` `{ id, title?, dueOn?, dueKm?, everyKm?, everyMonths? }` (`null` clears)                                                                   | `INPUT_VALIDATION`, `NOT_FOUND`, `BAD_REQUEST` (no due left)                                                                                               |
| `reminder.dismiss` `{ id }`                                                                                                                                  | `NOT_FOUND`                                                                                                                                                |
| `reminder.suggestions` `{ vehicleId, powertrain? }`                                                                                                          | `INPUT_VALIDATION`, `NOT_FOUND` (car)                                                                                                                      |
| `reminder.dismissSuggestion` `{ vehicleId, task }`, `reminder.restoreSuggestions` `{ vehicleId }`                                                            | `INPUT_VALIDATION`, `NOT_FOUND` (car)                                                                                                                      |
| `vehicleDocument.url` `{ id }`                                                                                                                               | `NOT_FOUND`                                                                                                                                                |

All are for Members of the Organization; a non-Member gets `FORBIDDEN`. Distances are always km; the client converts to the account's unit.

## In the app [#in-the-app]

The Service tab (`features/service/service.screen.tsx`, M-Manutencao) opens on the current car with the most urgent alert, and the subtitle picks another car or "All cars", which lists every current car's alerts with the car's name. Upcoming shows the car's `reminder.alerts` (an expiring document opens M-Documentos) with the due line ("in 746 mi or 2 months": `remainingKm` in the account's unit, calendar months to `dueOn`, days under a month) and the basis line; Plan is the reminders with an interval, then the suggested schedule (`service.suggestions.tsx`): a row per item with dismiss and add (also VoiceOver actions), a tap opens the reminder form filled in to edit before adding, a powertrain row when the fuel is unknown, and "Restore" for dismissed ones; History is `service.list`. "Notifications off" shows while alerts cannot reach the device as pushes (the account's choice, the system permission or an unregistered device); a tap asks for push, or opens Settings once the system no longer asks. `usePushRegistration` (under the tabs) registers the device through `notifications.register` once both allow it. A reminder (`service.reminder.screen.tsx`) is edited, removed, or logged as done, which opens the new-service form with its `reminderId`. The form (`service.form.screen.tsx`) generates the id once, so the RN07 resend with `odometerReason` and a lost response never save twice; amounts are parsed to minor units in the account's locale. M-Servico (`service.detail.screen.tsx`) shows the items, the receipt through `vehicleDocument.url` (asked again before `expiresAt`), the trust level, the next reminder (`service.scheduleNext`) and the versions; the pencil and "attach receipt" send `service.correct`.

### Home-screen widget (US33) [#home-screen-widget-us33]

The widget (`service.widget.ts`, synced by `useServiceWidgetSync` under the tabs) shows the household's most urgent service from `reminder.alerts` (a document's due date or an expected recurring expense is no service): "Next service", the item, the car's make and model and the same due line as the Upcoming card. The medium size on iOS, and the Android widget, add RF32's monthly cost: `expense.summary` for every car this calendar month, in the account's main currency. A tap opens that car's M-Veiculo on its Service segment (`autofolio://vehicle/<id>?segment=service`); with no service planned it opens the app. Everything is formatted in the account's language and units before it is pushed. The background is the opaque `card` token, so Reduce Transparency changes nothing, and iOS type follows Dynamic Type. Signing out, or the `widgets.next-service` flag turning off, leaves only the brand; a plate, VIN or note never reaches the widget. See the [widgets package](/docs/packages/widgets).

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

`service`, `reminder` and `vehicle_document` have row-level security by Active Organization; `ledger_entry` and `odometer_reading` are append-only for every role. `reminder.service_id` points at `(service, vehicle)`, so a reminder never names another car's service. No `audit_log` event is written: the ledger row is the money's audit trail, and amounts never go to logs (RNF07).

## Key files [#key-files]

- [`apps/mobile/src/features/service/service.hooks.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/apps/mobile/src/features/service/service.hooks.ts)
- [`packages/api/src/features/service/service.service.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/service/service.service.ts)
- [`packages/api/src/features/reminder/reminder.service.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/reminder/reminder.service.ts)
- [`packages/api/src/features/reminder/reminder.jobs.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/reminder/reminder.jobs.ts)
- [`packages/api/src/features/vehicle-document/vehicle-document.service.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/vehicle-document/vehicle-document.service.ts)
- [`packages/domain/src/reminder.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/domain/src/reminder.ts)

