# History transfer

How a seller hands a car's technical history to the next owner's AutoFolio account: the materialized offer, both consents, the system path the buyer reads it through, and what never travels.

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

History transfer (US43, RF46) lets a seller offer the technical history of a car in their garage or sold to the AutoFolio account of one email. The buyer sees the offer in their garage and accepts or declines it. Accepting adds a new car to the buyer's Active Organization with a copy of what the seller ticked, each record marked "From previous owner". Both answers are ★ `consent_record` rows (purpose `history_transfer`), and every step is audited (`history_transfer.offered`, `.revoked`, `.accepted`, `.declined`; never the email).

| Travels                                                          | Never travels                                                     |
| ---------------------------------------------------------------- | ----------------------------------------------------------------- |
| make, model, trim, year, colour, catalog entry                   | plate and VIN (RN09), nickname, notes                             |
| services: title, date, reading, shop, item names, trust          | service costs and line amounts, ledger entries, expenses          |
| odometer readings with their flags and reasons (RN07)            | acquisitions, disposals, loans, valuations (the economic history) |
| photos, and the documents the seller ticks one by one (none yet) | reminders, recurring expenses, passports                          |

A service keeps "with receipt" only when its receipt document goes too; a partner's verification is the partner's, so it travels as "with receipt" at most.

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

`history_transfer` lives in the seller's Organization: `recipient_email` (lower-cased), `snapshot` (the materialized technical history, with the seller's file keys), `counts`, `expires_at` (30 days), and one outcome at most (`accepted_at` with `accepted_by` and `accepted_vehicle_id`, `declined_at` or `revoked_at`). A pending offer holds its snapshot and an ended one never does; once it expires, the cron tick's `historyTransfers` job (`history-transfer.jobs.ts`, 100 offers a tick) drops the snapshot too, and the offer stays listed as expired with its counts; a car has one pending offer, and a new one replaces it. Tenant traffic inserts an offer and revokes it, nothing else ([ADR-0009](/docs/adr/0009-row-level-security)).

The buyer is in another household, so `inbox`, `accept` and `decline` run on the system handle (SystemReason `history-transfer.receive`) and every query is keyed by the session's email, only when it is verified. Accepting copies the files into `orgs/<buyer>/transfers/` first (a file the seller has since deleted is left out), then in one transaction locks the offer, checks the Free limit (RN11), and writes the car, an Ownership Period opening today, the readings, files, photos, documents and services (`from_previous_owner`, no `ledger_entry_id`), the outcome, the buyer's consent and the audit event. A failure removes the copies.

`service`, `odometer_reading` and `vehicle_document` carry `from_previous_owner`. A copied service has no money: `amountMinor`, `currency` and `ledgerEntryId` are null in the service contract, and `service.correct` refuses it (`CONFLICT`). History events carry the flag too.

## Procedures [#procedures]

| Procedure                                                             | Who                         | Failures                                                                                 |
| --------------------------------------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
| `historyTransfer.prepare` `{ vehicleId }`                             | the car's owner (documents) | `NOT_FOUND`, `CONFLICT` (wished-for or archived car)                                     |
| `historyTransfer.offer` `{ vehicleId, email, sections, documentIds }` | the car's owner (documents) | `INPUT_VALIDATION`, `BAD_REQUEST` (own email), `NOT_FOUND` (car or document), `CONFLICT` |
| `historyTransfer.list` `{ vehicleId?, limit }`                        | every Member                | none: the offers of the cars they see, with `status`                                     |
| `historyTransfer.revoke` `{ id }`                                     | the car's owner             | `NOT_FOUND`; an ended offer stays as it is                                               |
| `historyTransfer.inbox` `{}`                                          | every Member                | none: pending offers to their verified email from other households                       |
| `historyTransfer.accept` `{ id }`                                     | owners and admins           | `FORBIDDEN`, `NOT_FOUND` (gone), `CONFLICT` (the seller's own household), `PLAN_LIMIT`   |
| `historyTransfer.decline` `{ id }`                                    | every Member                | `NOT_FOUND` (gone), `CONFLICT` (the seller's own household)                              |

## App [#app]

M-Passaporte's last row, "Transfer history to the new owner" (`transferir-historico`), opens the transfer screen (`screen-transfer`, route `/transfer`): the cars to offer and the offers made, each with its status and, while pending, Withdraw. A car opens the offer form (`screen-transfer-offer`, `/transfer/<vehicleId>`): Services, Odometer readings and Photos with their counts, the documents to tick, what never goes, the next owner's email (`transferir-email`), the consent footnote and "Offer history" (`transferir-oferecer`).

The buyer finds "Offered to you" on M-Garagem; an offer opens its review (`screen-transfer-received`, `/transfer-offer/<id>`): who offers it, what comes with it, what never does, then "Accept and add to my garage" (`transferir-aceitar`), which opens the new car, or Decline (`transferir-recusar`). The copied services read "From previous owner" in Service, M-Veiculo and History, without a cost or a correction.

`autofolio://dev/M-Passaporte` opens the seeded offer of the sold Audi; `autofolio://dev/M-Garagem` shows the Cayman whose history a fictitious seller offers the demo account.

shortcut: copied documents are not queued for Document Text, so search and the assistant skip them; the buyer is not notified by push or email, the seller tells them.

## Key files [#key-files]

- [`packages/db/src/schema/history-transfer.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/db/src/schema/history-transfer.ts)
- [`packages/api/src/features/history-transfer/history-transfer.service.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/history-transfer/history-transfer.service.ts)
- [`packages/api/src/features/history-transfer/history-transfer.rules.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/history-transfer/history-transfer.rules.ts)
- [`apps/mobile/src/features/history-transfer/history-transfer.screen.tsx`](https://github.com/CarlosZiegler/autofolio/blob/main/apps/mobile/src/features/history-transfer/history-transfer.screen.tsx)

