# Insights

How M-Insights computes a car's total cost of ownership and cost per km or mile for a period, compares it across the garage's cars, and how proactive insights, last month's summary and the dashboard photo work.

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

A car's total cost of ownership (TCO) for a period (RF18, US21) is **depreciation + running costs + interest and fees** (`periodTco` in `@autofolio/domain`). Loan principal never counts (RN05): a payment is counted once, as the interest and fees booked to the ledger. "Compare cars" (RF25, US22) puts every car's cost per km or mile for the same period side by side, a car sold in it over the months it was owned: its running costs over its distance, the number M-Veiculo shows, as the M-Insights board draws it (the M3 at $1.12 / mi there, its TCO $1.80 / mi above).

- **Running costs** are M-Gastos' "Total spent" for the same period (`totalSpent`): its six categories, upgrades included, read through each Expense's and Service's current ledger entry (a correction replaces the original; raw ledger rows are never summed).
- **Interest and fees** are the car's `loan_interest` and `loan_fee` entries dated in the period, as they stand after corrections. M-Financiamento shows the same entries over the Loan's whole life, so a year's are part of that total (the seeded M3: $1,870 of $3,740).
- **Depreciation** is the car's value at the period's start minus its value at its end (`depreciation`). The value on a day is the latest purchase price or Valuation on or before it, else the first one: a car bought during the period starts at its price. A gain in value is negative and lowers the TCO; the app labels it "Estimated appreciation", never a profit (RN04). Without any value it is unknown and left out.
- **Distance** is the latest odometer reading minus the last one on or before the period's start, as on M-Veiculo. Without two readings there is no cost per distance.

Periods are calendar months up to today's: `1m` (this month), `6m`, `12m`, or `all` (the whole ownership). `12m` is M-Gastos' 12 months.

## Procedures [#procedures]

| Procedure                                                         | Who                         | Failures                              |
| ----------------------------------------------------------------- | --------------------------- | ------------------------------------- |
| `insights.tco` `{ vehicleId, period, today, distanceUnit }`       | Members of the Organization | `INPUT_VALIDATION`, `NOT_FOUND` (car) |
| `insights.compare` `{ period, today, distanceUnit }`              | Members of the Organization | `INPUT_VALIDATION`                    |
| `insights.overview` `{ vehicleId?, period, today, distanceUnit }` | Members of the Organization | `INPUT_VALIDATION`, `NOT_FOUND` (car) |

`tco` answers the total and per distance, each component, the distance in km, running costs by category and by month (at most 12). `compare` answers the cars with a distance in the period, each with its running costs and those per distance and whether it is sold: current cars in M-Garagem's order, then cars sold during the period (their costs and readings end at the sale). Amounts are integer minor units in the car's currency; a cost or Valuation in another currency without a stored rate is left out.

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

Reads only, through the request's `app_tenant` handle with the Organization filter; row-level security alone hides another Organization's cars. Nothing is written or audited, and amounts never go to logs (RNF07).

## App [#app]

M-Insights (`screen-M-Insights`, route `/insights`) opens from M-Patrimonio's Insights button. The subtitle picks a current car; the segmented control (`periodo-1m`, `periodo-6m`, `periodo-1a`, `periodo-tudo`) the period. The TCO card shows the TCO per distance, the total and the distance with the months' running costs as a line; then the three components with the formula, the category ring (grey ramp, legend with percentages) and "Compare cars" (each car's running costs per distance, a sold one marked "Sold"), whose current cars open their numbers. Its footnote says why that figure is lower than the TCO per distance above: it leaves out depreciation and interest. The screen reads `insights.overview`, Compare cars and the chosen car's TCO in one request: without a car the server picks the first current one with a cost per distance in the period, else the first current one, and that choice stays when the period changes. `autofolio://dev/M-Insights` opens the seeded M3.

## Proactive insights (AI spec §4) [#proactive-insights-ai-spec-4]

With the `ai.insights` flag on, each current car is compared with its own pattern, and only a real change becomes an insight, always with its number and its basis (`insights.signals.ts`, pure):

| Kind                 | Found when                                                                                                                                                     | Shows                                                        | Plan |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ---- |
| `fuel_economy_drop`  | the last full-to-full tank (RF09) used 15% or more per distance than the car's average over at least two tanks before it, the fill at most 45 days old         | the last tank and the average, in the account's economy unit | Plus |
| `spend_spike`        | this month's costs (M-Gastos' bars) pass twice the average of the six months before, three of them with costs                                                  | spent so far and the monthly average                         | Free |
| `insurance_increase` | an insurance payment is 5% or more above the one 12 months (± 20 days) before it, at most 45 days old; three times or more is a change of cadence, not a price | both payments and their dates                                | Plus |
| `service_due`        | a service reminder is due soon or overdue (US17's alert)                                                                                                       | the due date, distance left and pace                         | Free |

Free sees the basic kinds, Plus all of them (ADR-0017's tier; `PLUS_INSIGHTS`). No model computes these numbers (RN03); the `insights` model row of the AI registry is unused for now.

**Last month's summary.** The card above the list: the household's running costs in the last calendar month per currency, against the average of the six months before (once three of them had costs), km driven, the biggest category and each car with costs.

**Storage.** The `insight` table records one row per kind and key (the fill, the month, the payment, the reminder's due), so the same change is never news twice; only `status` changes (`new` → `dismissed`). The numbers are read from the records each time, never stored. RLS as for Moments: a Member sees only granted cars; the summary row (no car) only owners and admins.

**Morning push.** The `insights` job runs in each data region's morning hour (07, 11, 14 UTC, as reminders and Moments): it records what each household's cars hold and pushes the new ones from the last 7 days to the Members who allow push and see that car, once per device, never retried. Plus's kinds reach Plus accounts only; a due service is listed, not pushed (the reminder job already did); the summary reaches owners and admins in the month's first week. Push copy carries percentages, never an amount, plate or VIN. The scan pages through the households and keeps its place for the day in `job_cursor`, as the Moments job does. Reading the feed writes only the insights it has not recorded before.

| Procedure                   | Who                                   | Failures                 |
| --------------------------- | ------------------------------------- | ------------------------ |
| `insights.feed` `{ today }` | Members of the Organization           | `INPUT_VALIDATION`       |
| `insights.dismiss` `{ id }` | owners, admins and editors of the car | `NOT_FOUND`, `FORBIDDEN` |

The app's **Insights** screen (`screen-Insights`, route `/insight-feed`) shows the summary card, "By car" when several cars had costs, then "What changed": each row opens the car's costs or maintenance and has Hide (also a VoiceOver action). Free sees a row to Plus. The garage shows "Insights for you · N to see" while some wait, and Profile always lists it with the flag on. `autofolio://dev/Insights` opens it on the demo garage: the seeded Porsche's last tank (+35%) and its insurance renewal (+12%), the M3's due services and September's summary.

## Dashboard photo (Painel) [#dashboard-photo-painel]

M-Veiculo → More → "Odometer from a photo" (`screen-Painel`, route `/odometer-photo`, with `ai.insights` on and a car in the garage): photograph the instrument cluster (or pick a photo), the text is read on the phone (Vision / ML Kit, never metered, nothing uploaded) and `readCluster` picks the odometer: the number on an ODO/Gesamt line, else the largest whole number that is not the range or a trip (a trip shows tenths); clocks, temperatures and percentages are skipped, miles when the display says so. The range is shown for checking only, other numbers can be picked, and the value stays a draft until Save (RN10). `odometer.record` `{ id, vehicleId, km, readOn, reason? }` appends the reading with source `photo` through RN07: a lower one (or one above a later reading) answers `odometer_reason_required` and the screen asks a reason; saved, it is flagged. It goes through the offline queue (RNF04, the id is made once); a reading the server sends back later opens the screen again from a toast. A sold car answers `BAD_REQUEST`, another household's `NOT_FOUND`. `autofolio://dev/Painel` opens it with a sample cluster read past the M3's last reading.

## Key files [#key-files]

- [`packages/domain/src/rules.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/domain/src/rules.ts)
- [`packages/api/src/features/insights/insights.rules.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/insights/insights.rules.ts)
- [`packages/api/src/features/insights/insights.service.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/insights/insights.service.ts)
- [`apps/mobile/src/features/finances/finances.insights.screen.tsx`](https://github.com/CarlosZiegler/autofolio/blob/main/apps/mobile/src/features/finances/finances.insights.screen.tsx)
- [`packages/api/src/features/insights/insights.signals.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/insights/insights.signals.ts)
- [`packages/api/src/features/insights/insights.jobs.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/insights/insights.jobs.ts)
- [`apps/mobile/src/features/insights/insights.screen.tsx`](https://github.com/CarlosZiegler/autofolio/blob/main/apps/mobile/src/features/insights/insights.screen.tsx)
- [`apps/mobile/src/features/odometer/odometer.cluster.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/apps/mobile/src/features/odometer/odometer.cluster.ts)

