# Post-repair quality control and rework — Phase 3A.5

QC is an independent technical verification gate between repair and future customer
handover. It is not another repair, a warranty decision, or case closure. This phase
extends clean baseline `e4dbd77` with 1,012 existing tests. Existing repair and
diagnostic evidence remains immutable, including completed unknown RootCause.

## Lifecycle and assignment

| Operation | Case transition | Evidence |
| --- | --- | --- |
| Existing successful repair completion | REPAIRING → REPAIRED | Existing API unchanged |
| Submit for QC | REPAIRED → QC_PENDING | Explicit case revision; eligible independent inspector |
| Begin QC | QC_PENDING → QC_IN_PROGRESS | New attempt, exact successful repair reference and unanswered checks |
| PASS | QC_IN_PROGRESS → QC_PASSED | Immutable COMPLETED/PASSED attempt |
| FAIL | QC_IN_PROGRESS → DIAGNOSED | Immutable COMPLETED/FAILED attempt; explicit rework |
| Abandon with reason | QC_IN_PROGRESS → QC_PENDING | Immutable ABANDONED attempt; new attempt required next time |

The current engineer assignment remains current throughout QC, failure, rework and
success. Reassignment/unassignment remain unavailable outside the frozen assignment
workflow. QC failure does **not** change the old repair outcome from REPAIRED to
NOT_REPAIRED. The engineer completed that repair as repaired, and independent QC
subsequently found it unacceptable. Rework uses a **new** repair execution through
the existing Phase 3A.4 APIs and the existing completed diagnosis. Each new repair
must again explicitly enter QC_PENDING. Abandonment retries may reference the same
successful repair but always create a new QC attempt.

QC_PASSED means technically approved for a future handover stage. It is not delivery,
final closure, invoicing or payment. This phase provides no handover bypass API.

### Cancellation policy

Phase 3A.4 explicitly denied cancellation after successful repair. This phase
preserves that policy: cancellation is denied in **REPAIRED, QC_PENDING,
QC_IN_PROGRESS and QC_PASSED**. The optional relaxation mentioned in the Phase 3A.5
brief is not adopted. No attempt or assignment is abandoned/ended by a rejected
cancellation. Abandonment remains the explicit QC interruption operation.

Once QC FAIL returns the case to DIAGNOSED, existing rework cancellation is available:
it closes the current engineer assignment, retains the finalized failed QC, and sets
CANCELLED. A stale QC operation then rejects. In a FAIL/cancel race, cancel-first
rejects while QC is in progress; fail-first permits cancellation from DIAGNOSED.

## Eligibility and separation of duties

Canonical permission: **`service.perform_quality_control`**, attached to ServiceCase.
It is created by normal migration/post-migrate permission handling. No roles or
postings are seeded. QC uses the same SQL posting-scope predicate as engineer duty,
parameterized by permission while preserving its existing default behavior.

An active User needs an active organizational posting and an active role assignment
on that **same path**, whose active Role grants the QC permission. The assignment
must belong to that User. Source dimensions and target Company/Region/Center must
be active and internally consistent. Permission and scope cannot be mixed between
paths, users or companies. Native Groups/direct permissions/staff/superuser flags
do not grant QC duty. Unrelated engineer permission does not grant QC duty.

| QC posting | Coverage |
| --- | --- |
| Company only | Centers in the same Company |
| Region only | Centers in that Region and Company |
| Exact Center with matching Region | That Center |
| Exact Center plus Department | That Center, all dimensions active and consistent |
| Department only | Does not qualify |
| Region plus Department without Center | Does not qualify |
| Other Company or sibling Center | Does not qualify |

The inspector must differ from the **engineer on the specific successful repair's
assignment period**, even if the inspector is a superuser with a real QC posting.
Only the recorded inspector can edit/check/PASS/FAIL an open attempt. All technical
writes freshly revalidate eligibility, independence, active Device/catalog identity,
the current assignment and latest successfully completed repair.

Abandonment is administrative recovery, following previous phases: an active,
explicitly attributed actor may preserve and abandon an interrupted attempt even
when its inspector loses eligibility. The trusted caller must authorize recovery.
The QC Admin endpoint additionally requires a valid current QC posting scope.

## Exact data model

All three models have UUID `id`, `created_at`, `updated_at`, protected foreign keys,
ordinary-save/delete guards and QuerySet.delete protection.

| Model | Additional fields |
| --- | --- |
| ServiceQualityControl | service_case, repair_execution, inspector, status, checklist_version, started_at, completed_at, outcome, summary, failure_note, abandoned_at, abandoned_by, abandon_reason |
| ServiceQualityControlCheck | quality_control, check_code, check_name, sequence, allows_not_applicable, nullable result, note |
| ServiceQualityControlComplaintCheck | quality_control, complaint (original ServiceCaseComplaint), nullable result, note |

Attempt status is IN_PROGRESS, COMPLETED or ABANDONED. Outcome is PASSED or FAILED
only for completed attempts; it is NULL otherwise. Completion belongs to the fixed
inspector. Abandonment records its actor, reason and timestamp separately. Summary
allows 4,000 characters; other notes/reasons allow 2,000. Text is trimmed. Never
record passwords, PINs, OTPs or credentials.

PostgreSQL enforces one IN_PROGRESS attempt per case, controlled status/outcome and
timestamp coherence, positive checklist version, one code per attempt, one original
complaint per attempt, controlled results and explained permitted NOT_APPLICABLE.
It also prevents dangling references. Cross-table consistency, current eligibility,
separation of duties, completeness and immutable history are service/model rules;
they cannot be expressed safely as ordinary local CHECK constraints.

## Controlled checklist

`quality_control_checklist.py` defines version 1 with 21 checks: boot, display,
touch, buttons, charging, battery/basic power, cellular/network, Wi-Fi, Bluetooth,
microphone, earpiece, loudspeaker, front camera, rear camera, major sensors,
vibration, SIM, identifiers, cosmetic/physical condition, repaired complaint
verification and final functional verification.

Begin snapshots the code, name, sequence and whether N/A is permitted into every
attempt. Every result starts NULL, explicitly meaning **unanswered**. Recording a
result accepts only PASS, FAIL or NOT_APPLICABLE. There is no configurable global
checklist master and no fabricated passing results.

All entries require an answer before PASS. Boot, charging, battery/basic power,
cosmetic condition, repaired complaint verification and final functional verification
must PASS. Feature-dependent checks permit NOT_APPLICABLE only with a nonblank
explanation. The inspector is accountable for applicability; this phase does not
infer hardware features from an unimplemented capability registry. N/A must never
be used to excuse a broken applicable feature. The versioned definition is checked
again at completion, including expected codes, names, sequence and N/A policy.
Future checklist changes must retain definitions for existing stored versions.

### Original complaint verification

Begin creates a verification row for every intake complaint with `removed_at IS NULL`.
Removed complaints remain available through existing intake history, but are not
current verification obligations. Original complaint records are never changed.
Results are RESOLVED, NOT_RESOLVED or NOT_TESTABLE. Missing/NULL, NOT_RESOLVED and
NOT_TESTABLE all block PASS. Completion compares the exact current relevant complaint
set under the case lock and requires every row RESOLVED. With no active intake
complaints, the separate repaired-complaint and final-function checks still must PASS.

### PASS, FAIL and immutability

PASS requires the responsible eligible independent inspector, open attempt/current
repair, a complete valid checklist with no FAIL/unanswered result, permitted explained
N/A entries, every relevant complaint RESOLVED, and matching revisions under locks.

FAIL requires a nonblank failure note **and** at least one technical FAIL or complaint
NOT_RESOLVED/NOT_TESTABLE. It may finish early without answering unrelated checks;
the recorded failure provides the rework basis. A meaningless failure cannot be
recorded. Abandonment requires an interruption reason and is not a failure outcome.

Finalized attempts and children cannot be changed through supported APIs/Admin.
No reopen, reassignment of inspectors, destructive deletion or revision/supersession
API exists. Open results may be corrected in place, with stale-write protection.

## Public services

Import from `apps.service.quality_control_services`:

```python
submit_for_quality_control(*, service_case, inspector, expected_updated_at=_UNSET)
begin_quality_control(*, service_case, inspector, expected_updated_at=_UNSET)
update_quality_control(*, quality_control, inspector, summary, expected_updated_at=_UNSET)
set_quality_control_check(*, quality_control, inspector, check_code, result,
    note=_UNSET, expected_updated_at=_UNSET)
set_quality_control_complaint_check(*, quality_control, inspector, complaint, result,
    note=_UNSET, expected_updated_at=_UNSET)
complete_quality_control_pass(*, quality_control, inspector,
    summary=_UNSET, expected_updated_at=_UNSET)
complete_quality_control_fail(*, quality_control, inspector,
    failure_note, expected_updated_at=_UNSET)
abandon_quality_control(*, quality_control, actor, reason, expected_updated_at=_UNSET)
```

Omit optional values instead of importing `_UNSET`. Omitted note/summary preserves
the value; explicit empty string clears it. Omitted expected revision uses the
supplied object's `updated_at`, **not an unconditional write**. Refresh or use the
returned current object before further operations. Submission/start compare the
case revision; all attempt mutations compare the aggregate QC revision. Every child
edit bumps it. Methods return the affected case, attempt or child. To continue after
a child edit, refresh its parent attempt. Stale objects/forms cannot overwrite results.

## Public queries and disclosure boundaries

Import from `apps.service.quality_control_queries`:

```python
eligible_quality_control_inspectors(service_case)
current_quality_control(service_case)
quality_control_history(service_case)
quality_control_checks(quality_control)
quality_control_complaint_checks(quality_control)
qc_pending_cases_for_center(service_center, *, inspector=None)
qc_cases_for_inspector(inspector)
failed_quality_controls_for_case(service_case)
latest_completed_quality_control(service_case)
quality_control_visible_cases(inspector)
```

`qc_cases_for_inspector` is business-scoped SQL: it checks fresh active QC posting
scope, permission and separation of duties, and includes REPAIRED (awaiting explicit
submission), QC_PENDING, QC_IN_PROGRESS and QC_PASSED. Filter by status to distinguish
waiting, in-progress and passed queues. It may show another eligible inspector's
in-progress case within scope; ownership checks still prohibit editing it.

`quality_control_visible_cases` scopes audit-history disclosure independently of
case status, so failed/rework and cancelled histories remain inspectable within a
current QC posting. History does not imply technical eligibility or ownership.
Without `inspector`, the center pending query is **trusted internal**, not caller
authorization. Raw history/current/child queries are also trusted internal helpers;
future handlers must scope/authorize their results. An inspector may legitimately
work across companies only with a valid granting path in each.

Collections are lazy. Invalid/unsaved inputs yield empty results. Attempts order
started_at/UUID, completed-latest uses completed_at/UUID descending, checklist rows
sequence/UUID, complaint checks created_at/UUID, queues received_at/job_number/UUID.
Exists avoids duplicate queue entries from multiple granting paths. Joined relations
support normal actor, repair engineer, complaint and case displays. Tests measure
**one SQL query each** for evaluated history, technical checks, complaint checks and
scoped queue, including their normal related display fields. Admin prefetches all
attempt child sets and complaint taxonomy; query count does not grow per checklist row.

General engineer queues retain the current assignment in QC states under their
existing engineer-eligibility semantics. QC duty does not grant engineer duty.

## Transactions and concurrency

Technical writes use one atomic transaction and the established lock order:

1. Inspector User SHARE; case Company SHARE.
2. Inspector organizational paths SHARE in UUID order; referenced Roles SHARE in UUID order.
3. Catalog Brand/Category/Model/optional Variant SHARE; Device SHARE.
4. ServiceCase UPDATE; current engineer assignment UPDATE.
5. Completed diagnostic assessment UPDATE; latest repair execution UPDATE.
6. QC attempt UPDATE; technical checks UPDATE in UUID order, then complaint checks UPDATE in UUID order when finalizing.

Single-child edits lock that child after the attempt. The case/attempt locks serialize
all supported child writers. Current repair, assignment, eligibility, independence,
case state and revision are checked after dependencies are locked. No repair-action
or diagnostic taxonomy is revalidated: those are finalized historical evidence.
Original complaint membership is stable under the case lock and frozen intake rules.

Abandonment needs actor SHARE → case → assignment → diagnosis → repair → QC UPDATE,
without acquiring upstream eligibility/catalog dependencies behind the case lock.
Cancellation rejects QC states under the case lock. All child creation/finalization
and lifecycle changes roll back together on failure, including late case-save failure.

Tests use separate PostgreSQL connections and observed `pg_blocking_pids` waits.
They cover duplicate starts, PASS/FAIL, cancellation in both serialization orders,
repair/start races, revocation/deactivation, stale child/completion operations,
abandonment, repeated rework, self-QC and out-of-scope inspectors, and begin/PASS/FAIL
rollback. They do not prove formal deadlock freedom for arbitrary outer transactions
or raw writers that violate the lock protocol.

## Admin

The ServiceCase QC link opens a dedicated workflow. Every POST uses a domain service,
CSRF, native change permission and a valid current QC posting scope. The signed token
contains case UUID/revision and attempt UUID/revision/repair/inspector. Services compare
the captured aggregate revision again under locks, including when a form choice was
resolved after an intervening edit. The signed-in user is the technical inspector;
there is no impersonation selector. No superuser duty or scope bypass exists here.

The page displays the inspected repair/engineer, original complaints and complete QC
attempt history. Generic QC model Admin is readonly, scoped by QC posting and protected
against add/delete. Protected lifecycle fields cannot be submitted through raw model
editing. Earlier non-QC Admin semantics remain unchanged; those trusted native Admin
pages are not an end-user company authorization framework.

## Reporting semantics and limitations

Each attempt stores its inspector, exact repair, outcome, reason, timestamps, checklist
version and complaint results. This preserves first-pass success, QC failure/rework
counts, repair engineer versus inspector, recurring complaint failures, abandoned
attempts and eventual success without a reporting dashboard. Abandoned attempts are
not completed failures and can be excluded explicitly from future metrics.

The controlled list is a starting smartphone verification policy, not a certification
claim or a hardware testing integration. It records inspector assertions, not device
telemetry. Open edits are not a full revision journal. Label snapshots belong only to
the versioned QC checklist; original complaint taxonomy remains a protected live
reference. Custom workflow histories are not paginated. This remains default-database
PostgreSQL code. Raw SQL, bulk/update writes and private persistence can bypass
application contracts; database owners can alter evidence. It is not a tamper-proof
ledger. Domain services enforce integrity/technical ownership; trusted recovery callers
must authorize their actor. No new frozen authorization-engine adapter is introduced.

Phase 3A.6 now adds explicit release, physical delivery and separate closure after
QC_PASSED. It revalidates the latest passed QC against the latest successful repair;
QC PASS still performs none of these transitions automatically. Delivery ends the
engineer assignment without modifying QC history. See [SERVICE_HANDOVER.md](SERVICE_HANDOVER.md).
Payment, invoicing, inventory, notifications, SLA and dashboards remain unimplemented.

## Verification

The final complete `python manage.py test --noinput` run, executed after the last
code change, passed **1,111 tests, 0 failed, 0 skipped**, in **1,138.764 seconds**.
This consists of **1,012 unchanged baseline tests + 99 new tests**: 53 domain/query,
13 Admin and 33 real PostgreSQL concurrency tests. The full run used a fresh
PostgreSQL test database and applied the complete migration graph.

`check`, `makemigrations --check`, `migrate`, `showmigrations service` and
`git diff --check` pass. New `service.0005_quality_control` was inspected before
application; service migrations 0001 through 0005 are applied. Historical migrations,
existing tests and frozen Phase 1/2 source remain unchanged. Existing repair and
diagnostic services are unchanged; integration is additive in ServiceCase metadata,
private posting-query parameters, QC states, Admin links and cancellation guards.

The bounded changed-file credential-pattern/artifact scan found no candidates;
`.env` remains ignored. No unresolved integrity/security conflict was found in
this review. Known privileged-write, caller-authorization and operational limits
are documented above; test success is not a formal deadlock/security proof.

Git has **10 modified tracked files and 11 new files**, all unstaged/uncommitted.
HEAD remains `e4dbd77`. No stage, commit or push occurred; Phase 3A.6 was not started.

## File inventory

New files:

- `apps/service/quality_control_models.py`
- `apps/service/quality_control_services.py`
- `apps/service/quality_control_queries.py`
- `apps/service/quality_control_checklist.py`
- `apps/service/quality_control_admin.py`
- `apps/service/templates/admin/service/quality_control.html`
- `apps/service/migrations/0005_quality_control.py`
- `apps/service/test_quality_control.py`
- `apps/service/test_quality_control_admin.py`
- `apps/service/test_quality_control_concurrency.py`
- `docs/QUALITY_CONTROL.md`

Integration changes:

- `apps/service/models.py`
- `apps/service/services.py`
- `apps/service/engineer_queries.py`
- `apps/service/engineer_admin.py`
- `apps/service/admin.py`
- `docs/ARCHITECTURE.md`
- `docs/SERVICE_INTAKE.md`
- `docs/ENGINEER_ASSIGNMENT.md`
- `docs/ENGINEER_DIAGNOSIS.md`
- `docs/ENGINEER_REPAIR.md`
