# Engineer repair planning and execution — Phase 3A.4

This extends the verified Phase 3A.3 baseline `a30be7c` (905 tests). It uses the
existing global RepairAction taxonomy and same-path engineer eligibility. It does
not change ownership, diagnostic evidence, taxonomy or authorization semantics.

## Workflow and history

| Operation | Case transition | Repair history |
| --- | --- | --- |
| Begin | DIAGNOSED → REPAIRING | Create OPEN execution linked to current assignment and its completed diagnosis |
| Complete REPAIRED | REPAIRING → REPAIRED | COMPLETED execution with successful outcome |
| Complete NOT_REPAIRED | REPAIRING → DIAGNOSED | COMPLETED execution with unsuccessful outcome |
| Abandon with reason | REPAIRING → DIAGNOSED | ABANDONED execution, no completion outcome |
| Cancel | REPAIRING → CANCELLED | Abandon execution and end assignment atomically |

The current engineer assignment remains intact on success, unsuccessful completion,
and abandonment. Reassignment and unassignment remain restricted to their existing
RECEIVED/ASSIGNED workflow; neither is available during REPAIRING or after REPAIRED.
Cancellation after REPAIRED is denied. Cancellation during repair requires an
explicit active actor, preserves actions, and uses the same timestamp for execution
abandonment, assignment closure and case cancellation.

NOT_REPAIRED and abandonment permit a **new explicit attempt** from DIAGNOSED by the
responsible engineer. They do not reopen an old execution or diagnosis. Multiple
historical attempts may reference the same completed diagnosis; at most one is OPEN.
REPAIRED does not imply QC, delivery, payment or case closure.

## Models

`ServiceRepairExecution` has UUID id; protected ServiceCase, engineer-assignment,
diagnostic-assessment and actor references; controlled OPEN/COMPLETED/ABANDONED
status; note; started_at/by; nullable completed_at/by and REPAIRED/NOT_REPAIRED
outcome; nullable abandoned_at/by; abandon_reason; and created_at/updated_at.

`ServiceRepairAction` has UUID id; protected execution and global RepairAction
references; note; is_active; nullable performed_at/by; and created_at/updated_at.
Taxonomy names/codes are not copied. Later master display changes can therefore
change historical labels, while protected identities remain stable.

Execution notes allow 4,000 characters; action notes and abandonment reasons allow
2,000. Text is trimmed. Never record passwords, screen PINs, credentials or OTPs.

## Plan versus performed work

Adding an action creates a plan row with no performed timestamp or actor.
`mark_repair_action_performed` explicitly records both using the current engineer
and an aware server-side timestamp. No public API accepts a supplied performance
timestamp. Naive/future timestamps and incoherent actor/timestamp pairs reject.

Only active, unperformed rows can be edited. Performed taxonomy, note, actor and
timestamp cannot be rewritten. Soft removal can exclude either a planned or
performed row from the active plan; it retains its existing performed evidence.
Removed rows cannot be edited or restored. A new planned row may select that
taxonomy again, but must independently be marked performed. Active duplicates
within one execution are prohibited.

Both completion outcomes require at least one **active** action, every active
action performed, and every active action currently valid. Removed actions do not
satisfy completion. Abandonment is the explicit recovery path for incomplete work.

## Public services

Import from `apps.service.repair_services`:

```python
begin_service_case_repair(*, service_case, actor, note="",
    expected_engineer_assignment_id=_UNSET, expected_updated_at=_UNSET)
update_repair_execution(*, repair_execution, actor, note,
    expected_updated_at=_UNSET)
add_repair_action(*, repair_execution, repair_action, actor, note="",
    expected_updated_at=_UNSET)
update_repair_action(*, action, actor, repair_action=_UNSET, note=_UNSET,
    expected_updated_at=_UNSET, expected_execution_updated_at=_UNSET)
remove_repair_action(*, action, actor, expected_updated_at=_UNSET,
    expected_execution_updated_at=_UNSET)
mark_repair_action_performed(*, action, actor, expected_updated_at=_UNSET,
    expected_execution_updated_at=_UNSET)
complete_service_case_repair(*, repair_execution, actor, outcome, note=_UNSET,
    expected_updated_at=_UNSET)
abandon_service_case_repair(*, repair_execution, actor, reason,
    expected_updated_at=_UNSET)
```

Services return the created/updated history row. Omit optional preconditions rather
than importing the private sentinel. Child operations bump the execution revision.
Admin always supplies applicable revisions, including **both** execution and child
revisions on child mutations. This prevents a freshly resolved form choice from
overwriting changes made after the aggregate form was loaded.

## Eligibility, taxonomy and authorization

Technical writes require the actual current assigned engineer, an active User and
valid same-path `service.handle_servicecase` RBAC/organizational eligibility for
the case's ServiceCenter. Company/Region/Center and required source dimensions must
remain active and consistent. Staff, superuser, Groups or direct native permissions
do not replace that operational posting or permit impersonation. Device and its
catalog hierarchy must remain active and have unchanged identity during validation.

Selection, update, performance and completion use the frozen canonical
`repair_action_applies_to_category` helper with fresh database state. Choices use
`applicable_repair_actions_for_category`. Supplied actions must exist, be active and
apply to the Device's ProductCategory. Completion rechecks every active action;
deactivation/applicability removal before completion prevents it.

The referenced diagnosis must remain completed, not abandoned, and belong to the
same case and assignment. Completed diagnostic findings are historical evidence:
their old taxonomy is not revalidated or rewritten by repair. A completed diagnosis
with NULL RootCause remains valid. NULL still means unknown/unconfirmed; no master
values are fabricated.

As in Phase 3A.3, abandonment/cancellation are recovery operations requiring an
active attributed actor, not ongoing engineer eligibility. Domain services enforce
integrity and technical ownership, **not complete caller authorization**. Trusted
Admin uses native model permissions. Future endpoints must authorize mutations and
query disclosure separately. No new authorization adapter or engine is introduced.

## Transactions and lock order

All writes are atomic on the default PostgreSQL database. Technical operations
follow the existing dependency-first order:

1. Actor User SHARE, then case Company SHARE.
2. Actor organizational paths SHARE, UUID order; referenced Roles SHARE, UUID order.
3. Catalog Brand/Category/ProductModel/optional Variant SHARE, then Device SHARE.
4. Required RepairAction masters SHARE, deduplicated UUID order.
5. ServiceCase UPDATE, then current engineer assignment UPDATE.
6. Referenced completed diagnostic assessment UPDATE, then repair execution UPDATE.
7. Changed action UPDATE; completion locks all active actions in UUID order.

Fresh SQL eligibility is checked after dependencies and case locks. Frozen supported
User/org/RBAC, catalog, Device and taxonomy writers conflict with these dependency
locks. Canonical applicability is read under taxonomy/category protection.

Completion snapshots active action identities, taxonomy ids, performed timestamps
and revisions before acquiring taxonomy locks. After locking the case/execution
and action rows it compares the snapshot again; a changed plan rejects and asks
for review rather than acquiring new taxonomy locks behind the case lock.

Every supported plan writer locks the same case and execution, serializing the
aggregate and ensuring no action slips through completion. Child writes compare
their pre-lock snapshot as well as optional caller revisions. Recovery only needs
actor SHARE → case → assignment → diagnosis → execution UPDATE; it never obtains
eligibility/catalog/taxonomy locks afterward. Cancellation closes the assignment
only after preserving repair history. Any later validation failure rolls back all
earlier writes, including abandonment or completion.

Separate-connection PostgreSQL tests observe real lock waits using
`pg_blocking_pids`. They cover competing begin, action additions/duplicates,
update/remove/performance, completion/plan changes, taxonomy and applicability,
User/RBAC/org/catalog/Device changes, cancellation, abandonment, failed retry,
stale Admin forms and late-failure rollback. These bounded tests do not prove
formal deadlock freedom for arbitrary callers or pre-acquired outer locks.

## Admin and queries

The ServiceCase repair link offers begin from DIAGNOSED and plan/note/performance,
completion and abandonment while REPAIRING. It uses the signed-in actor, CSRF,
native permissions and signed case/assignment/execution revision tokens. Domain
services revalidate under locks. History Admin pages are readonly; add/delete are
disabled even for superusers. Case status remains readonly. A REPAIRED case rejects
mutation submissions, including stale forms.

Import from `apps.service.repair_queries`:

```python
current_repair_execution(service_case)  # OPEN row or None
repair_execution_history(service_case)
active_repair_actions(repair_execution)
repair_action_history(repair_execution)
repairing_cases_for_engineer(engineer)
repaired_cases_for_engineer(engineer)
repairing_cases_for_center(service_center)
```

Collection queries are lazy and return empty results for invalid/unsaved inputs.
History includes finalized attempts and inactive actions. Execution ordering is
started_at/UUID; actions use created_at/UUID; queues use received_at/job_number/UUID.
Related display objects and actors are joined. Admin prefetches actions, taxonomy
and performers for all displayed attempts. Query-count tests check bounded reads.

Queues retain the existing current-assignment and active same-path semantics,
including REPAIRING/REPAIRED in general engineer queues. Upstream loss of eligibility
hides cases operationally but does not erase current assignments/history. Queries
are internal helpers, not a replacement for caller authorization. The historical
RECEIVED-only intake queue remains unchanged.

## Schema boundary and limitations

Only new `service.0004_repair_execution` is added. It introduces two models and
extends ServiceCase status/cancellation constraints. PostgreSQL enforces valid
status/outcome/timestamp coherence, one OPEN execution per case, active action
uniqueness, performed actor/time coherence and protected references. Cross-table
case/assignment/diagnosis consistency, applicability, eligibility and immutable
history depend on supported services/model validation, not cross-table CHECKs.

Ordinary save/delete and QuerySet.delete reject. Raw SQL, QuerySet.update, bulk
writes and private persistence remain trusted escape hatches; this is not a
tamper-proof ledger. Draft edits are in place, not a full revision journal. Later
correction/supersession of finalized evidence is out of scope. Taxonomy labels are
live references. Long history pages are not paginated in the custom workflow.

No parts, inventory, pricing, estimates, approvals, QC, delivery, payment, claims,
automatic unassignment, diagnosis mutation or Phase 3A.5 workflow is added.

## Verification

The final `python manage.py test --noinput` run passed **1,012 tests, 0 failed,
0 skipped**, in **748.171 seconds**. This verifies all **905 unchanged baseline
tests + 107 new tests** in one run: 55 repair model/service/query tests, 12 Admin
tests and 40 real PostgreSQL concurrency tests. A fresh PostgreSQL test database
applied the complete migration graph, including `service.0004_repair_execution`.

`check`, `makemigrations --check`, `migrate`, `showmigrations service` and
`git diff --check` pass. All four service migrations are applied. No historical
migration, baseline test, Phase 1/2 source or frozen SERVICE_TAXONOMY documentation
was modified. No architecture conflict or unresolved security issue was found in
this bounded review. The changed-file credential-pattern/artifact scan found no
candidates; `.env` remains ignored.

Git contains 9 modified tracked files and 10 new files, all unstaged/uncommitted.
HEAD remains `a30be7c`. No commit/push occurred and Phase 3A.5 was not started.
Authorization and privileged-write limitations are documented above.

## Changed files

New files:

- `apps/service/repair_models.py`
- `apps/service/repair_services.py`
- `apps/service/repair_queries.py`
- `apps/service/repair_admin.py`
- `apps/service/templates/admin/service/repair.html`
- `apps/service/migrations/0004_repair_execution.py`
- `apps/service/test_repair.py`
- `apps/service/test_repair_admin.py`
- `apps/service/test_repair_concurrency.py`
- `docs/ENGINEER_REPAIR.md`

Existing files updated for integration:

- `apps/service/models.py`
- `apps/service/services.py`
- `apps/service/admin.py`
- `apps/service/engineer_admin.py`
- `apps/service/engineer_queries.py`
- `docs/ARCHITECTURE.md`
- `docs/SERVICE_INTAKE.md`
- `docs/ENGINEER_ASSIGNMENT.md`
- `docs/ENGINEER_DIAGNOSIS.md`

## Phase 3A.5 integration

Successful repair still returns REPAIRED. Independent QC explicitly submits it to
QC_PENDING, then starts QC_IN_PROGRESS. PASS reaches QC_PASSED; FAIL returns to
DIAGNOSED for a new repair through the existing APIs. A QC failure never changes
a successful repair outcome or its actions. The assignment remains current.
The existing post-repair cancellation prohibition extends through QC states.
See [QUALITY_CONTROL.md](QUALITY_CONTROL.md) for scope, independence and rework.

Phase 3A.6 requires the latest successful repair's passed QC for release/delivery
and closure. It preserves repair evidence, rejects open or superseding technical
work and ends the assignment only at physical delivery. No repair/reopen operation
is available after closure. See [SERVICE_HANDOVER.md](SERVICE_HANDOVER.md).
