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_entryof kindoperating, categorymaintenance: the total in the car's currency (RN02), the shop as vendor, the odometer and the author; - the
servicerow: title, date, odometer, shop, items ({ name, amountMinor? }), note, trust and receipt; - with an odometer value, a ★
odometer_readingwith sourceservice(RN07, as for an Expense); - with a receipt, a
vehicle_documentof kindreceipt, and the trust becomeswith_proof; - with
reminderIdornext, 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 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)
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)
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:
lastReadingandkmPerMonth: 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,
remainingKmfrom the last reading (negative when overdue),forecastOn(when the average reachesdueKm) andnextOn, the earlier ofdueOnandforecastOn, 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)
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:
overdueon facts only: the last reading is pastdueKm, ordueOnis gone by;soonwhennextOn(the date, or the forecast from the last reading and the average distance per month) is withinDUE_SOON_DAYS(30), a forecast already passed included;laterotherwise, 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 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)
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.createwithplanTask. Accepted unchanged from the manufacturer's schedule, its basis isinterval; 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.dismissSuggestionadds it tovehicle.dismissed_plan_tasks,reminder.restoreSuggestionsempties it. - The app also hides a suggestion the plan already has under the same name.
Scheduling from a service
service.createwithreminderIdfulfils 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 isdone.service.createwithnext: { everyKm?, everyMonths? }schedules the next one (a new reminder titled like the service, or the fulfilled one with the new interval).service.scheduleNextis M-Servico's "Next change scheduled" card: it creates, or moves, the reminder scheduled from that service.service.getreturns it asnext.
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
| 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
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)
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.
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
apps/mobile/src/features/service/service.hooks.tspackages/api/src/features/service/service.service.tspackages/api/src/features/reminder/reminder.service.tspackages/api/src/features/reminder/reminder.jobs.tspackages/api/src/features/vehicle-document/vehicle-document.service.tspackages/domain/src/reminder.ts