Skip to content
Docs

Command palette

Jump to a page or search the docs.

Changelog

Acquisition

How a car's purchase (M-Compra) is saved as an append-only header and ledger entries, why filling in a blank is not a correction, how the amount financed opens a Loan, and why a replay never saves twice.

Markdown

What it does

An Acquisition is how a car's Ownership Period began (RF06). acquisition.save writes, in one transaction:

  • a ★ acquisition header: the purchase date, down payment and amount financed in the car's currency, and the Loan it opened;
  • ★ ledger_entry rows of kind acquisition_price and acquisition_cost (the purchase base, RN06), dated the purchase date;
  • above zero financed, a loan whose principal is the amount financed and an opening ★ loan_snapshot. The principal is never a ledger entry or an expense (RN05);
  • with an invoice, a vehicle_document of kind purchase_invoice for the uploaded file; new entries get trust with_proof;
  • the Ownership Period's start_date, which follows the purchase date. A car in the garage without a period gets one; a wished-for car without one is BAD_REQUEST, and so is a sold car's date after its sale.

Every field is optional (US06). An omitted field keeps its value, so a partial save is fine and the rest can be filled in later. The first save makes recorded true, which removes the garage's "Next step" card.

Corrections (ADR-01, RNF12)

Nothing is updated. Changing a recorded value (price, costs, date, down payment, amount financed) is a Correction:

  • a changed price or cost, or a moved date, writes a correction entry naming the latest entry of that kind, with the reason;
  • the new header names the one it replaces (corrects_id), so a period's headers form a chain and the latest is the purchase as it stands;
  • a changed amount financed opens a new Loan with the old one's terms and settles the old one with a zero-balance snapshot (a Loan's principal is fixed).

Without a reason, a save that would change a recorded value writes nothing and answers { status: "reason_required" }. Filling in a blank needs no reason. A save that changes nothing writes nothing.

Trade-in (US08)

tradeIn: { vehicleId, valueMinor } names one of the household's current cars. The same transaction sells it on the purchase date for valueMinor through Close sale's recordSale (Disposal header, sale_proceeds entry, a remaining Loan paid off, period closed, Sold), so the old car's deal result uses the trade-in value. The header stores trade_in_vehicle_id and trade_in_entry_id, the old car's sale_proceeds entry, which is the money; nothing is copied into the new car's ledger and its purchase base stays the full price. Corrections carry the link forward.

acquisition.get answers tradeIn (car and value) once linked, and tradeInOptions: the other cars in the garage with an open period in the same currency, while the purchase has no trade-in and is not a sold car's. A trade-in is BAD_REQUEST when it is this car, not in the garage, in another currency, owned only after the purchase date, or when the purchase already has one; another Organization's car is NOT_FOUND. Nothing is written then. A sale is never undone (RN01), so a linked trade-in stays.

Offline and idempotency (RNF04)

The client generates the save's id (a UUID), which becomes the new header's id. Sending it again answers the purchase as it stands and writes nothing more; saves of one car run one at a time. The same id for another car is CONFLICT.

Procedures

ProcedureWhoFailures
acquisition.get { vehicleId }Members of the OrganizationNOT_FOUND (car)
acquisition.save { id, vehicleId, acquiredOn?, priceMinor?, costsMinor?, downPaymentMinor?, financedMinor?, invoiceFileId?, tradeIn?, reason? }Members of the OrganizationINPUT_VALIDATION, NOT_FOUND (car, invoice file or trade-in), BAD_REQUEST (no period to record, date after the sale, trade-in refused), CONFLICT (id)

acquisition.get answers before any save too: the period's start as the date, and price and costs already in the ledger. null is a value not entered yet. timeOwned(from, until) in @autofolio/domain/ownership gives M-Compra's "3 years, 4 months", until today or the sale.

Data and isolation

All tables have row-level security by Active Organization. acquisition, ledger_entry and loan_snapshot are append-only for every role. No audit_log event is written: the ★ rows are the purchase's audit trail, and amounts never go to logs (RNF07).

App

M-Compra (screen-M-Compra, route /purchase/[vehicleId]) opens from the garage's "Next step" card and the vehicle profile's More menu. The date card opens a date sheet and shows the time owned; price, acquisition costs, down payment and amount financed are fields in the account locale (blank stays to fill in later); the trade-in row (troca) opens a sheet (sheet-troca) to pick a car and its value (valor-troca, usar-troca). Save (salvar) confirms a trade-in first, and asks for a reason when the server answers reason_required. A linked trade-in opens the old car's profile. More holds "Attach invoice" (a new document of kind purchase_invoice) or Documents. "Close sale" opens M-Fechar-Venda.

autofolio://dev/M-Compra opens the seeded Corolla Cross with the drawn down payment and amount financed typed in.

Key files

Edit on GitHubDocumentation is in English.

We use essential cookies to make our app work. With your consent, we also use optional cookies to improve your experience. Read our Cookie Policy and Privacy Policy.