# Service estimate and quotation — Phase 3C.1

## Boundary and frozen baseline

This phase starts from clean `master`, commit `377475f`, with 1,699 frozen tests.
Initial Django checks and migration-drift checks passed. `apps.commercial` owns
commercial proposals and customer decisions. It references technical, customer,
catalog and inventory identities without owning or rewriting those domains.

> Absence of a quotation is not evidence that a repair is free or covered.
> Phase 3C.1 introduces an enforceable approval gate only after customer-pay
> responsibility has been explicitly established in the commercial domain.

No ServiceCase status is added. Diagnosis, opening repair, and unperformed
planning remain available under the existing rules. Existing quotation-free
workflows are unchanged and remain commercially unclassified. There is no invoice,
payment, receivable, tax filing, price-list engine, customer portal, messaging
delivery, or Phase 3C.2/3C.3 functionality.

## Models and durable evidence

All commercial records except the internal numbering counter have UUID IDs and
created/updated timestamps. Normal model save/delete and queryset delete are
disabled; supported services own mutations. References use PROTECT.

| Model | Important fields |
| --- | --- |
| QuotationSequence | ServiceCenter primary key, next_value; internal transactional counter |
| QuotationFamily | One-to-one ServiceCase, ServiceCenter, human-readable number, permanent approval_obligation flag |
| ServiceQuotation | family, positive revision, is_current, status, currency, customer/internal notes, valid_until, subtotal, discount_total, tax_total, grand_total, customer_pay_total, covered_total, context_snapshot, scope_snapshot, created_by, submitted_at/by |
| QuotationLine | quotation, position, PART/LABOR/SERVICE, CUSTOMER/WARRANTY/COMPANY responsibility, description, coverage_reason, optional SparePart and part-code/name snapshot, optional case repair action/finding, quantity, unit_price, subtotal, discount, taxable_amount, tax_rate/label/tax, total, is_active |
| QuotationDecision | One-to-one quotation, APPROVED/REJECTED, server-created decision timestamp, recorded_by, channel, recipient_name, reference, note |
| CommercialWorkAuthorization | exact approved quotation, PERFORM/CONSUME/COMPLETE, repair execution/action, optional SparePart and quantity, actor, server-created timestamp |

Retired draft lines remain history; saving draft composition creates a replacement
active set. Submitted content and lines are immutable. PostgreSQL triggers also
protect identities, immutable evidence, cross-case technical links and the
permanent approval obligation. Deferred constraints reconcile line sums with
quotation totals and require decision/status agreement at commit.

Submission captures customer display name/number, case/job, device/model/variant,
service-center label, immutable intake warranty context, and active open repair
scope including action IDs, taxonomy identities/labels and plan notes. Lines retain
their descriptions, prices, discounts, tax labels/rates and part labels. Historical
commercial values do not follow subsequent master edits. Snapshot data contains
customer information and is accessible only through scoped commercial queries.

## Numbering and revisions

Numbers are ServiceCenter-scoped `EST-00000001` through an 18-digit sequence.
The counter uses transactional get-or-create and row locking. Failed transactions
roll back allocation; committed numbers are never reused. There is no deletion
or cancellation cleanup that resets numbering.

One family exists per case. Revision numbers increase from 1, and a partial
unique constraint permits only one current revision. Creation of a new revision
copies active lines into a new DRAFT, clears submission/decision evidence and
validity, and retains the old content. Currency cannot change across revisions.

| Operation | Transition |
| --- | --- |
| Create/edit | DRAFT; explicit server recalculation |
| Submit | DRAFT → SUBMITTED; freeze commercial content and scope |
| Record decision | SUBMITTED → APPROVED or REJECTED |
| Revise submitted | old SUBMITTED → SUPERSEDED, not current; new DRAFT |
| Revise approved/rejected | old status and decision retained, not current; new DRAFT |

Historical approved/rejected revisions retain their actual decision rather than
relabeling it. Their `is_current=False` records replacement. Every material
replacement requires its own decision; there is no implicit approval carry-forward
for a decrease or covered-only replacement after an obligation was established.

## Money, responsibility and warranty

There was no existing monetary convention to extend. Money uses exact Decimal
values backed by `numeric(14,2)`, with a maximum of 999,999,999,999.99. Inputs
reject floats, booleans, non-finite values, negatives, excessive precision and
overflow. Quantities are positive whole numbers, bounded at one million. This
phase explicitly supports a two-decimal currency representation; no currency
conversion or zero/three-decimal currency rules are supplied. The required
three-letter currency label must be appropriate to this representation.

All calculation is centralized in `commercial.money.calculate`:

1. Subtotal = quantity × unit price.
2. Fixed line discount must be between zero and subtotal.
3. Taxable amount = subtotal − discount.
4. Tax = taxable amount × explicit tax percentage / 100, rounded per line to
   two decimals using ROUND_HALF_UP.
5. Line total = taxable amount + tax; aggregate totals sum those line values.
6. Customer total sums CUSTOMER lines; covered total sums WARRANTY and COMPANY.
   Customer + covered always equals grand total.

Percentage discounts and hidden quotation-level adjustments are not supported.
Tax defaults to zero. A nonzero rate needs an explicit tax label; the rate and
amount are snapshots, not lookups on rendering. This is a calculation mechanism,
not a jurisdictional compliance engine. Service input does not accept browser
calculated totals.

Each line explicitly chooses responsibility. Covered lines retain their full
commercial value and require a rationale. The intake warranty snapshot is copied
as context without modification; it never automatically establishes entitlement
or makes all repairs free. A separate claim-adjudication process is deferred.
SparePart selling prices are supplied explicitly on the quotation. No retail
price or inventory cost architecture was added to frozen parts.

## Repair gate and approved scope

The first saved CUSTOMER line with a positive net total permanently sets the
family's approval-obligation flag, including while still DRAFT. Editing it to
zero, retiring it, rejecting it, or creating a replacement revision does not clear
that flag. Current approved commercial state resolves the gate; a later replacement
reopens the requirement until that replacement is approved.

Once established, the gate conservatively applies to the case's repair performance,
successful completion and part consumption. Opening an execution, adding/editing
unperformed plan rows, reservation, issue, returns, abandonment and diagnosis keep
their existing technical/inventory rules. A purely covered/zero-net quotation
that has never established a customer-pay obligation does not activate this gate.

`require_commercial_authorization` requires the current APPROVED revision. Performed
actions must match the IDs, taxonomy identities and notes in its submitted repair
scope. New or materially changed plan actions require revision and approval.
Successful completion validates all active actions against that same scope.
An estimate submitted before a repair plan exists remains valid commercial history,
but once a gated repair plan is added its scope must be presented in a revision
before performance. No action is silently inferred from a part or labor charge.

Consumption also requires a PART line and a sufficient approved quantity. Its
budget subtracts earlier commercially authorized consumption across the entire
family, not just the latest revision. Copying a revision cannot reset consumed
quantity and authorize the same budget twice. Additional consumption needs an
explicit increased cumulative quantity approved in a new revision. Each operation
retains the exact revision it used; later revisions do not rewrite prior evidence.

The first priced edit of an already-created, previously unclassified draft is
rejected if repair performance occurred after that family was created. This closes
the race in which a stale draft retrospectively classifies concurrently performed
unapproved work. Retrospective pricing/reclassification of that work is deferred;
the service does not fabricate prior consent. Technical work before commercial
enrollment remains unclassified history, not an inferred free or covered charge.

Quotation validity limits submission/acceptance. Expiry does not retroactively
revoke an already-recorded approval. Work still needs current technical eligibility,
active compatible parts and all existing lifecycle safeguards.

## Inventory independence

Creating, editing, submitting, deciding and revising quotations never create
ledger movements, units, reservations, custody, returns or recovery records.
Quotation PART lines are proposed commercial scope, not physical inventory.
The only inventory integration is a commercial decision check inside the existing
consumption service, after its case/action validation and before physical posting.
Its authorization evidence rolls back with a failed inventory transaction.

## Public APIs

All commands in `apps.commercial.services` require an actor:

- `create_service_quotation`
- `update_service_quotation`
- `set_quotation_lines`
- `submit_service_quotation`
- `record_quotation_decision`
- `revise_service_quotation`

Mutations of an existing revision require `expected_revision`, the ISO value of
its `updated_at`. Line edits also advance the aggregate revision. Submitted
decisions use server actor/time and an explicit IN_PERSON, PHONE, SMS, WHATSAPP,
EMAIL or OTHER evidence channel, recipient, and reference. These channels send
no messages.

Scoped query APIs in `apps.commercial.queries`:

- `quotation_cases`, `service_quotations`
- `current_service_quotation`, `approved_service_quotation`
- `quotation_history`, `quotation_detail`, `quotation_lines`, `quotation_customer_due`
- `service_cases_awaiting_quotation`, `service_cases_awaiting_customer_approval`

Collections remain SQL-filtered, deterministic QuerySets. Awaiting-quotation is
an informational queue of eligible cases without commercial records, not an
assertion that every case must obtain a quotation.

`commercial.gating.customer_pay_approval_required` and
`require_commercial_authorization` are trusted integration helpers, not disclosure
endpoints or substitutes for caller authorization. The latter requires the caller
to hold the case UPDATE lock and an atomic transaction.

## Authorization and Admin

The existing same-path RBAC engine authorizes `commercial.manage_servicequotation`,
`commercial.record_quotationdecision`, and `commercial.view_servicequotation` at
the case's ServiceCenter. Organization scope is not duplicated. Native staff,
Groups and direct Django permissions do not create business scope. Active
superusers retain their established bypass, while lifecycle, active hierarchy,
ownership, snapshot and money integrity remain mandatory.

Admin creation, draft metadata/line management, submission, decision recording
and revision creation delegate to services. Mutations are POST/CSRF protected;
actor-bound signed aggregate revisions reject stale or tampered forms. Native
Admin permissions and business permissions both apply. Calculated totals and
statuses are not editable. Quotation child lines have no standalone edit screen;
decision and work evidence screens are readonly. Commercial history cannot be
deleted. Submitted line tables display snapshot labels, values and responsibilities.
Normal template escaping protects notes; existing no-credentials guidance is reused.

## Transactions and locking

The deterministic order follows frozen dependency-first conventions:

1. Actor User SHARE; case Company SHARE; actor organization paths and Roles SHARE.
2. Catalog Brand/Category/Model/Variant SHARE, then Device SHARE.
3. Referenced PartCategory and SparePart SHARE in sorted UUID order, when relevant.
4. ServiceCase UPDATE.
5. Family/revision UPDATE, followed by commercial line/evidence writes.

Creation takes the commercial center counter after the case; no command holds
that counter and then acquires a different case. Technical reference reads occur
under the case lock shared by all supported technical writers, without taking
taxonomy or part locks in reverse order. Submission snapshots line dependencies
before locking and rechecks the composition after the case/revision locks.

Repair performance/completion and inventory consumption already hold the case
UPDATE lock. Their narrow commercial gate only reads commercial state and writes
authorization evidence; it does not acquire earlier dependency or inventory locks.
Thus first customer-pay declaration/revision/decision cannot race independently
with performance, successful completion or consumption. If performance using old
approval commits first, its exact authorization is retained and a subsequent
replacement gates future work. If replacement commits first, waiting work rejects.

Real PostgreSQL tests observe blocking with separate connections and verify final
rows. Coverage includes competing numbering/first creation, draft/line submission,
approval/rejection/revisions, role revocation, case cancellation, part deactivation
and rename, rollback/retry, first obligation/performance, revision/performance,
revision/completion and revision/consumption in both material orderings. These
tests do not establish formal deadlock freedom for arbitrary external lock orders.

## Performance and limitations

Representative tests assert one query for list, history, approval queue and joined
line display, and two for detail plus its prefetched active lines. Pricing sums
operate on bounded quotation composition, not inventory ledger history. Inventory
remains independent. These are query-count regression budgets, not load benchmarks.

This phase has one family per case, at most 100 active lines per revision,
whole-number line quantities, fixed discounts,
two-decimal money, explicit tax inputs, conservative case-wide gating once enrolled,
and no automatic approval carry-forward. It has no quotation withdrawal/deletion,
retrospective consent, currency conversion, full audit of every draft keystroke,
or new customer communication delivery. Raw privileged SQL remains a trusted
administrative boundary, subject to the added history and reconciliation triggers;
ordinary users use services and controlled Admin.

## Verification and exact changed files

The single final post-change full run used
`python manage.py test --noinput --verbosity 2`, created a fresh PostgreSQL test
database, applied every migration from zero, and passed **1,790 tests** in
**2,784.236 seconds**, with **zero failures/errors and zero skips**. It returned
`OK`, destroyed the test database and exited with code 0. The complete log is
`C:\Users\Subin\AppData\Local\Temp\c-care-phase3c1-full-suite.log`.

| Verification | Result |
| --- | ---: |
| Frozen baseline tests, unchanged | 1,699 |
| New commercial tests | 91 |
| Final total / passed | 1,790 / 1,790 |
| Failures / errors / skips | 0 / 0 / 0 |
| New real PostgreSQL concurrency tests, included above | 28 passed |

The new tests comprise 41 monetary/service/gating/query tests, 10 Admin tests,
12 security/integration tests and 28 concurrency tests. Successful log markers
independently confirm all 1,790 results and all 91 commercial results. Focused
verification passed 89 tests before the final two Admin cases; final targeted
verification passed 11 tests. No production/test changes followed the full run.

Both new migrations, `commercial.0001_initial` and
`commercial.0002_history_integrity`, were inspected and are applied to the
development database. Final `check` reports zero issues;
`makemigrations --check` reports no changes; `showmigrations` shows every
migration applied; and `git diff --check` passes. A separate whitespace scan
also covers new untracked files.

The security review verifies company/center isolation, same-path permission/scope
coupling, protected same-case references, server-calculated amounts, server actor
attribution, stale approval rejection, immutable presented history, no normal
deletion, CSRF and escaped text. A bounded scan of all 21 changed/new files found
no private-key, access-token or credential-bearing database-URL signatures.
`.env` remains ignored. This scan is evidence from the changed files, not a claim
that arbitrary future free text can never contain credentials.

No frozen test, historical migration, unrelated file or `.gitignore` was modified.
HEAD remains `377475f` on `master`. All four tracked modifications and 17 new files
remain unstaged; nothing was staged, committed, pushed, amended, reset or rebased.
Only this report was finalized after the full run. Phase 3C.2 was not started.

Tracked modifications:

```text
apps/inventory/usage_services.py
apps/service/repair_services.py
config/settings/base.py
docs/ARCHITECTURE.md
```

New files:

```text
apps/commercial/__init__.py
apps/commercial/admin.py
apps/commercial/apps.py
apps/commercial/gating.py
apps/commercial/migrations/0001_initial.py
apps/commercial/migrations/0002_history_integrity.py
apps/commercial/migrations/__init__.py
apps/commercial/models.py
apps/commercial/money.py
apps/commercial/queries.py
apps/commercial/services.py
apps/commercial/templates/admin/commercial/workflow.html
apps/commercial/test_admin.py
apps/commercial/test_concurrency.py
apps/commercial/test_security.py
apps/commercial/tests.py
docs/SERVICE_QUOTATION.md
```

Tracked diff statistics (Git excludes untracked files):

```text
 apps/inventory/usage_services.py | 3 +++
 apps/service/repair_services.py  | 5 +++++
 config/settings/base.py          | 1 +
 docs/ARCHITECTURE.md             | 5 +++++
 4 files changed, 14 insertions(+)
```

Git status:

```text
 M apps/inventory/usage_services.py
 M apps/service/repair_services.py
 M config/settings/base.py
 M docs/ARCHITECTURE.md
?? apps/commercial/
?? docs/SERVICE_QUOTATION.md
```


## Phase 6D operational presentation

The operational quotation queue and retained-revision view show actual states,
current-revision flags, submitted timestamps and explicit customer decisions. A
non-current rejected revision retains its original decision/state according to
the frozen engine. No later invoice/repair infers approval. Existing signed
action forms and services remain authoritative; see OPERATIONAL_UI.md.
