# 0019. Contracts and offers are generated in the application

- **Status:** Accepted
- **Date:** 2025-07-10

## Context

Two documents leave the system on paper or as a PDF: the quote sent to a prospect, and the rental contract.
Both carry data the system already holds — the units, the terms, the prices, the customer, the storage
properties — and both have to be editable by the office before being sent.

## Decision

**Generate them in-process** with `phpoffice/phpword` and `dompdf`, driven by
[`modules/admin/helpers/DocumentHelper.php`](../../modules/admin/helpers/DocumentHelper.php) and the
`GenerateOffer` / `GenerateRentDocument` admin actions.

The generated file is stored as a `File` and referenced from the record — `ProposalRequest.fileId`. Moving a
proposal request to `Sent`, `Approved` or `Declined` **requires** that file, enforced by a conditional
validation rule.

Two model fields exist purely for the document side: `Storage.nameInContract` (the contract names a unit
differently than the website does) and `StorageProperty.displayInContract` (only some properties belong in
a contract).

## Alternatives

- **A document service or a templating SaaS.** Another external dependency and another place customer data
  goes, for two document types.
- **HTML printed from the browser.** No archival artefact, and the office needs a file to attach to an
  e-mail.
- **LaTeX or a headless office suite.** More faithful typography, far more to install on the server.
- **Store only the data and render on demand.** Rejected: a sent offer must be immutable. Re-rendering it
  after a price change would silently alter a document already in the customer's inbox.

## Consequences

- **The sent document is an artefact, not a view.** What the customer received is what is stored.
- **No external dependency** on the document path.
- **`fileId` gates the proposal workflow.** A failed generation blocks the status transition with a
  validation error naming the file field, not the generator — which reads as a confusing error the first time
  it happens.
- `DocumentHelper` is 383 lines of layout logic that has to be maintained alongside two libraries; `phpword`
  and `dompdf` upgrades are a real cost.
- The document subset of the data (`nameInContract`, `displayInContract`) is a small amount of
  document-specific concern leaking into the domain models. Accepted: the alternative was a mapping layer for
  two fields.
