# Period with the car

How a car's Ownership Periods are edited (M-Periodo): the purchase date moves with the start, a period ends without a sale, reopens under RN11, or a second period starts after a gap; never two at once.

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

Mais → "Período com o carro" (M-Periodo) edits a car's Ownership Periods: when it arrived (De) and when it left (Até), roughly or exactly ("2014", "meados de 2018", "mar 2023"). Each edge is stored as its bucket's widest bound (a start its first day, an end its last day, never after today), so every "inside the period" check stays right without knowing the precision (U2-02).

- **De is the purchase date.** Changing it on a period with a recorded purchase writes a ★ Acquisition correction header with the new date and a ledger Correction of the price and costs with the same amounts, all with the system reason `period_adjusted` ("Data ajustada no período"), never a prompt. The original stays in the history.
- **Até on a current car** asks how it left. "Vendi" is Fechar venda ([Sale](/docs/features/disposal)) with the date as roughly as it is known (`disposedOnPrecision`). "Só saiu" closes the period with a reason (Doei, Perda total, Devolvi o leasing, Roubado ou furtado, Outro motivo, Prefiro não dizer: `LEFT_REASONS`) and moves the car to the past (Sold state, as a sale leaves it: no reminders, no new costs, out of RN11's count), with no money and no result. The timeline reads "Saiu"; only the car's chapter shows the reason.
- **A sold period** keeps its Até locked to the ★ Disposal: it changes only through "Corrigir venda" (`disposal.correct`), and it never reopens (RN01).
- **"Ainda é meu"** reopens the latest period of a car that only left; the car is current again, under RN11 (Free holds one active car), the same check as adding a car.
- **"Comprei de novo"** adds another period after a gap, open by default (current again, under RN11). The gap's costs and km belong to no period, and the new period gets its own purchase through M-Compra.

Records outside the period are a warning, never an error: a dry run lists the costs (Expenses and Services as they stand) and odometer readings that were inside the period and fall outside it after the edit, and the owner may save anyway (M-Periodo-Aviso). They stay in the car's history.

## Rules [#rules]

| Rule                                                                                                     | Where                                                                                                                                       | Failure       |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| A start or an end bucket after today, or an end before the start                                         | `spanProblem` in `ownership.rules.ts`                                                                                                       | `BAD_REQUEST` |
| Two periods of a car share a day (compared on the widest bounds, so "2014" again overlaps "2009 – 2014") | `spanProblem`, under the car's row lock; the `ownership_period_no_overlap` EXCLUDE constraint (`btree_gist`) refuses it in the database too | `CONFLICT`    |
| A sold period's Até, or reopening it                                                                     | the period has a `disposal`                                                                                                                 | `CONFLICT`    |
| The car becomes current on Free with its one active car                                                  | `holdsRoomForActive` under the garage lock                                                                                                  | `PLAN_LIMIT`  |

## Procedures [#procedures]

| Procedure                                                                             | Who                         | Answer                                                                           | Failures                                                                                                             |
| ------------------------------------------------------------------------------------- | --------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `ownership.get` `{ vehicleId }`                                                       | Members of the Organization | the car's header and its periods, oldest first, each with `sold` and `purchased` | `NOT_FOUND`                                                                                                          |
| `ownership.updatePeriod` `{ vehicleId, periodId, start?, end?, endReason?, dryRun? }` | the car's owner             | `{ ownership, outside }`                                                         | `INPUT_VALIDATION`, `NOT_FOUND`, `BAD_REQUEST` (dates, or an end on a period still open), `CONFLICT` (overlap, sold) |
| `ownership.closePeriod` `{ vehicleId, periodId, end, endReason, dryRun? }`            | the car's owner             | `{ ownership, outside }`                                                         | as above; `BAD_REQUEST` when the period already ended otherwise                                                      |
| `ownership.reopenPeriod` `{ vehicleId, periodId }`                                    | the car's owner             | `ownership`                                                                      | `NOT_FOUND`, `BAD_REQUEST`, `CONFLICT` (sold, or a later period exists), `PLAN_LIMIT`                                |
| `ownership.addPeriod` `{ id?, vehicleId, start, end?, endReason? }`                   | the car's owner             | `ownership`                                                                      | `NOT_FOUND`, `BAD_REQUEST` (dates, a wished-for or archived car), `CONFLICT` (overlap, id), `PLAN_LIMIT`             |

`outside` is `{ count, records }`: at most `OUTSIDE_LISTED` (20) records, oldest first, each a `cost` (id, origin, date, category, title, vendor, amount) or a `reading` (id, date, km, source).

## Offline and idempotency (RNF04) [#offline-and-idempotency-rnf04]

`closePeriod` goes through the offline queue after Desfazer's 5 seconds: closing a period already closed in the same bucket for the same reason answers it as it stands. `reopenPeriod` on a period already open answers it. `addPeriod` takes a client id; replaying it answers the car.

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

`ownership_period` is mutable (not ★): app_tenant may update its dates, precisions and end reason, never delete it. What hangs off it is ★: its start is the Acquisition's date and a sold end is the Disposal's, corrected as new rows. Every edit is audited on the car (`vehicle.updated`), a dry run included. Period dates are not public outputs: passports and showcases keep their own field allow-list, and nothing here touches plate, VIN or address (RN09).

## Key files [#key-files]

- [`packages/api/src/features/ownership/ownership.contract.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/ownership/ownership.contract.ts)
- [`packages/api/src/features/ownership/ownership.service.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/ownership/ownership.service.ts)
- [`packages/api/src/features/ownership/ownership.rules.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/ownership/ownership.rules.ts)

