# Service Intake / Job Card Foundation - Phase 3A.1

`apps.service` records receipt of a physical Device for an explicitly selected
Company, ServiceCenter and presenting Customer. It owns the ServiceCase aggregate,
its intake children and its captured warranty facts. It is not a repair execution,
inventory, financial or delivery workflow. Engineer assignment is now described in
[Phase 3A.2](ENGINEER_ASSIGNMENT.md). No public HTTP/API
or final service-center UI is introduced.

## Boundaries and fields

The five business models use UUID IDs and the project's `created_at`/`updated_at`
timestamps. Relationships use PROTECT. Their ordinary save/delete and QuerySet
delete paths reject; public mutation services perform the supported writes.
Private `_persist` methods are internal, not caller-facing safe-write APIs.

| Model | Fields in addition to UUID/timestamps |
| --- | --- |
| ServiceCase | company, service_center, job_number, customer, device, intake_channel, received_at, status, customer_reference, intake_note, created_by, cancelled_at, cancellation_reason |
| ServiceCaseComplaint | service_case, complaint_symptom, customer_description, removed_at |
| ServiceCaseIntakeCondition | service_case, condition_type, description |
| ServiceCaseAccessory | service_case, accessory_type, description, quantity |
| ServiceCaseWarrantySnapshot | service_case (one-to-one), recorded_coverage, coverage_source, coverage_start_date, coverage_end_date, coverage_reference, captured_at |

`ServiceCaseNumberSequence` is internal: protected ServiceCenter one-to-one primary
key plus positive `next_value`. It is not exposed in Admin. Cancellation timestamp
and reason preserve the cancellation event; complaint `removed_at` implements
non-destructive removal. There are no speculative future-state tables or fields.

ServiceCase permanently stores Company and ServiceCenter references, rather than
deriving Company from a later hierarchy lookup. Company, ServiceCenter, job number,
Customer, Device, creator, received time and arrival channel are immutable through
supported writes. Region is validated at intake but is not an additional historical
ownership field. Frozen organization rules already prohibit Company reassignment;
a later allowed same-company Region movement does not change stored case ownership.

Optional `customer_reference` is trimmed, nonunique and not system identity.
`intake_note` is for intake remarks, never engineer diagnosis. Text is trimmed.
Only these two case metadata fields have an update service in this phase.

## Numbering

Numbers are `JOB-00000001`, `JOB-00000002`, etc., independently per ServiceCenter.
The counter is locked under the ServiceCenter row, including lazy initialization;
there is no MAX+1 query. Display padding is at least eight digits, with supported
capacity up to 18 digits. PostgreSQL protects `(service_center, job_number)` and
the number format. Allocation and snapshot creation share one transaction; failure
restores an existing counter or removes a newly initialized one.

Cancellation never releases a number. Normal deletion is disabled. Even a trusted
bypass deleting a case leaves the sequence advancing; arbitrary privileged deletion
or corruption of the allocator itself is outside this contract. A backwards or
exhausted counter fails closed rather than silently repairing or duplicating numbers.
Different centers can allocate without serializing their counters, including with
the same Customer/creator and separate Devices. Sharing the same Device still
intentionally serializes Device-related work.

## Creation prerequisites and ownership independence

The caller explicitly supplies persisted default-database Company, ServiceCenter,
Customer, Device and created_by User. Under locks, creation requires:

- Active User, Company, ServiceCenter and the center's Region.
- Center and its Region belong to the selected Company.
- Active Customer in that Company.
- Active Device with an active Brand/Category/ProductModel and optional Variant.
- Current Device identification policy configured and current identifier slots
  satisfying REQUIRED/OPTIONAL/NOT_APPLICABLE rules.
- No OWNER history for that Device belonging to another Company.

Existing Device catalog locking and identification validation are reused. Stale
objects supply identity only; persisted state is reloaded. Creation does not create
Customers, register Devices, change identifiers, or change any prerequisite lifecycle.

The presenting Customer need not equal the recorded owner. A family member,
representative or other same-company Customer may present the Device. Ownership
history is not modified. Permanent Device ownership-company affinity is checked
against all OWNER history while Device is locked, including after ownership ends.
There is no existing public affinity getter, so intake performs the same conservative
history query without modifying the frozen ownership architecture.

A Device with no OWNER history has no ownership-company affinity. Intake itself
**does not establish affinity** or silently assign ownership. Consequently an unowned
Device may have cases in different Companies; each case remains explicitly company
owned and scoped. Later ownership cannot retroactively rewrite a case's historical
Company. This is deliberately distinct from ownership and respects the frozen rule
that only OWNER history establishes ownership-company affinity.

## Received time and initial lifecycle

Channels: WALK_IN, COURIER, PICKUP, DEALER, OTHER. These are descriptive only.
Service defaults use timezone.now after prerequisite locks. Explicit times must be
aware and no later than application time; valid backdating is supported. Explicit
offsets are normalized to UTC before persistence/snapshot date selection, so a
reload does not change the interpretation of the received calendar date. Admin
uses its explicit received-time field, initially populated with current time.

Creation always starts RECEIVED. Phase 3A.2 adds ASSIGNED between receipt and
cancellation, with explicit assignment/unassignment transitions. Intake metadata
and children remain editable only while RECEIVED.
Cancellation is atomic and preserves every child/snapshot. The first cancellation
records timestamp and trimmed optional reason; repeated calls without an optimistic
precondition are idempotent and do not overwrite that reason. No reopening exists.
Cancelled cases cannot accept metadata changes, complaint add/removal, conditions
or accessories through services or Admin.

Later Company/Center/Customer/Device deactivation does not cancel or delete cases.
Historical queries remain available, and numbers stay reserved. Explicit cancellation
remains possible while prerequisites are inactive. Intake child/metadata editing is
controlled by case status only, except new complaint selection also revalidates
current active catalog/applicability. Broader policy for operating open cases under
inactive centers is deliberately deferred.

## Public services

Imports are from `apps.service.services`:

```python
create_service_case(*, company, service_center, customer, device, created_by,
                    intake_channel, received_at=None, customer_reference="",
                    intake_note="")
update_service_case_intake(*, service_case, customer_reference=_UNSET,
                          intake_note=_UNSET, expected_updated_at=_UNSET)
cancel_service_case(*, service_case, reason="", expected_updated_at=_UNSET,
                    cancelled_by=None)
add_service_case_complaint(*, service_case, complaint_symptom,
                          customer_description="")
remove_service_case_complaint(*, service_case, complaint)
add_service_case_intake_condition(*, service_case, condition_type, description)
add_service_case_accessory(*, service_case, accessory_type,
                          description="", quantity=1)
```

Each returns the affected model. `_UNSET` is an internal omission sentinel; omit
optional arguments instead of importing it. Optional case revision preconditions
compare updated_at under lock; Admin always supplies revisions for metadata edits
and the currently selected row revision for cancellation. Domain/constraint conflicts
raise ValidationError after rollback. Unsupported raw deletion of supplied objects
can instead lead to model DoesNotExist; no service recreates a missing object.

For an ASSIGNED case, cancellation requires an explicit active cancelled_by User
and atomically closes the current engineer period at the cancellation timestamp.
Admin always supplies request.user. Actorless RECEIVED cancellation remains
backward-compatible; see [engineer assignment](ENGINEER_ASSIGNMENT.md).

Phase 3A.3 adds DIAGNOSING and DIAGNOSED. Cancellation during diagnosis atomically
abandons the open assessment before closing the engineer assignment; completed
evidence is retained. Intake metadata/children remain editable only in RECEIVED.
See [engineer diagnosis](ENGINEER_DIAGNOSIS.md) for the extended lifecycle and recovery.

## Complaint capture

Complaints reference ComplaintSymptom only; no diagnosis, root cause or repair action
is stored. Addition locks current catalog dependencies and the symptom, then uses
the existing `complaint_applies_to_category` API. Global/restricted applicability
logic is not copied. Inactive/unmapped symptoms and inactive catalog hierarchy reject.
The symptom must apply to the Device's current ProductCategory.

The same symptom can occur only once per case, including removed history. Optional
customer wording is trimmed and immutable. Removal sets removed_at under the case
lock and preserves the original row; `.complaints.filter(removed_at__isnull=True)`
selects currently reported complaints, while `.complaints.all()` includes removal
history. Removed rows are not restored/re-added in this phase; a future correction
workflow would need explicit history semantics. Master-data deactivation or later
applicability changes do not delete existing complaint records.

## Conditions and received accessories

Condition types: COSMETIC, PHYSICAL_DAMAGE, LIQUID_INDICATOR, POWER_STATE, OTHER.
A nonblank trimmed description is required. These are observations at receipt,
not technical findings or a fixed matrix of Boolean diagnostic fields.

Accessory types: CHARGER, CABLE, CASE, SIM, MEMORY_CARD, BOX, OTHER. Quantity must be
a positive integer. Description is optional. Multiple rows are permitted, with no
inventory identity, reservation, stock movement or consumption behavior. Condition
and accessory rows are append-only, immutable after creation; no correction/removal
workflow is provided in this phase.

**Never store screen PINs, passwords, account credentials or OTPs in intake notes,
customer wording, references or descriptions.** No such fields exist. Admin help
text explicitly prohibits credentials. Free text cannot automatically prove the
absence of secrets; operators must follow this rule. A future unlocking workflow
needs a separately designed secure mechanism.

## Warranty snapshot

Creation holds Device UPDATE, the same coordination lock used by warranty/evidence
writers, and calls existing Phase 2B query APIs. On `case.received_at.date()` (UTC),
recorded coverage is true only when the current active coverage includes that date
and the Device is active. Boundaries are inclusive. Covered snapshots copy source,
start/end dates and reference; uncovered snapshots have false plus all nullable
coverage facts set to NULL. Every supported creation produces exactly one snapshot.

`captured_at` records capture time, distinct from a potentially backdated received_at.
The snapshot describes coverage visible **at transaction capture time**, evaluated
for the received date; it does not reconstruct which coverage was active at an
arbitrary past timestamp. There is no coverage mutation, automatic warranty creation,
purchase verification, Customer inference or warranty-holder assumption.

**Recorded warranty coverage is not an approved warranty repair.** The snapshot
makes no claim decision about liquid/customer-induced damage, exclusions or later
diagnosis. Later replacement, clearing, evidence invalidation or Device deactivation
does not change the snapshot. Snapshot facts and captured_at are immutable through
supported paths; Admin is audit-only.

## Transactions and lock order

Creation order:

1. Creator User FOR SHARE.
2. Explicit Company FOR SHARE.
3. ServiceCenter FOR UPDATE (coordinates this center's first counter/allocation).
4. Customer FOR SHARE.
5. Existing catalog helper: Brand SHARE, ProductCategory SHARE, ProductModel SHARE,
   optional ProductVariant SHARE, with fresh ancestor validation.
6. Device FOR UPDATE; reread catalog identity, activity, identifiers and ownership.
7. ServiceCenter counter FOR UPDATE, allocate, insert case and capture snapshot.

Company SHARE synchronizes with frozen organization and Customer lifecycle's
Company UPDATE protocol, including Region/Center movement. Customer SHARE also
coordinates ordinary row updates while allowing concurrent intake at independent
centers. Device UPDATE synchronizes ownership, identifiers, evidence and warranty.
No new catalog lock is acquired behind Device. User first is compatible with
purchase-review attribution locking. Counter and snapshot failures roll back the
entire creation, with no committed case lacking its snapshot.

Complaint addition takes catalog locks in the existing order, then ComplaintSymptom
SHARE, then ServiceCase UPDATE. Frozen applicability writers lock the symptom before
changing mappings; category foreign-key key-share locks are compatible with intake's
category SHARE. Category lifecycle and symptom changes cannot invalidate selection
mid-transaction. Removal locks ServiceCase then complaint. Conditions/accessories,
metadata updates lock ServiceCase. Cancellation optionally locks its actor before
ServiceCase, then current engineer history; it never acquires catalog or
organizational update locks afterward.

Finite tested schedules establish valid serialization, not formal deadlock freedom
for arbitrary outer transactions or privileged raw writers. New callers composing
operations must preserve ordering rather than pre-acquiring locks in reverse.

## Queries and search

Imports are from `apps.service.queries`:

```python
service_case_by_job_number(*, service_center, job_number)  # case or None
service_cases_for_customer(customer)                    # lazy QuerySet
service_cases_for_device(device)                        # lazy QuerySet
service_cases_for_service_center(service_center)        # lazy QuerySet
active_service_cases_for_service_center(service_center) # lazy QuerySet
search_service_cases(*, company, query)                 # lazy QuerySet
```

Ordering is received_at DESC, UUID ASC. Job lookup trims/uppercases and always uses
the explicit center. Active means **status == RECEIVED only**, not current activity
of related master records. Other lookups include cancelled/inactive-related history.
Invalid or unsaved scope inputs yield empty results/None.

Search requires a persisted Company, applies its SQL predicate, and searches job
number, Customer number/name, reference or exact normalized Device identity key.
Historical identifiers continue to find the same Device's cases; distinct removes
join duplicates. Related Company/Center/Customer/Device display/creator/snapshot
are joined. Representative lists including displays and snapshots execute one query.
Device-scoped history can span Companies when the Device lacked ownership affinity;
it is a trusted historical API, not an unscoped end-user search endpoint.

## Admin and authorization

All five business models are registered; the internal counter is not. Case Add calls
create_service_case and attributes the request User. Facts/lifecycle are readonly;
only reference/note may be edited while RECEIVED with a signed revision and locked
precondition. Cancellation is an explicit service action retaining all children.
Its Admin action uses an empty optional reason; callers needing a reason use the
public cancellation service. No action promises batch all-or-nothing across cases.

Children are added through services, then audit-only. Complaint removal is an
explicit non-destructive action. Cancelled cases are excluded from child choices
and still rejected under lock if cancellation races form submission. Company and
center-qualified choices disambiguate tenant-local numbers. Warranty snapshots are
readonly with no Add. Deletion is disabled, native model permissions and CSRF remain
in force, and generic log messages do not copy note/identifier contents.

Services enforce **domain integrity, not caller authorization**. created_by is
attribution, not a Company/Center scope grant. Staff, superuser, Groups and direct
permissions are not new business scopes. Frozen authorization adapters are unchanged;
ServiceCase remains an unsupported target there. Native Admin model permissions are
for trusted administration. Future HTTP/UI workflows must implement explicit caller
authorization before invoking these services or revealing query results.

## Database and application boundaries

PostgreSQL protects center/job uniqueness and format, positive sequence, channel and
status enums, coherent cancellation metadata, complaint uniqueness, condition type
and nonblank description, accessory enum/positive quantity, one-to-one snapshot and
its boolean/date/source/null coherence, UUID keys and foreign-key integrity.

Cross-table Company consistency, operational prerequisites, permanent ownership
company affinity, complete snapshot creation, applicable complaints, normalization,
future-time rejection and historical immutability are enforced by services/models,
not by triggers or cross-table database checks. Ordinary model save is disabled;
validated internal persistence checks immutable facts and local consistency.
QuerySet.update, bulk writes, raw SQL and private persistence are trusted escape
hatches and can bypass application rules. Database owners can erase/change history;
this is not a tamper-proof ledger. Customer/Device/User/master references are PROTECT
and cannot silently cascade away service records.

`service.0001_initial` creates exactly the six models and their constraints. It
depends on current Customers, Devices, Organization, service taxonomy and the swappable
User migration. Existing migrations are untouched. A fresh PostgreSQL test database
successfully applies this graph, including the existing btree_gist dependency.

## Verification and limitations

The clean frozen baseline at `c40e100` was rerun before implementation: **657 passed,
0 failed, 0 skipped** (326.246 seconds). No existing tests were changed.

Focused Phase 3A.1 verification: **69 passed, 0 failed, 0 skipped** (60.224 seconds).
It includes 24 actual PostgreSQL races: same-center allocation, independent centers,
Company/Center/Customer/Device/creator/catalog/policy lifecycle, intake-first
preservation, warranty replacement/clear in both orders, purchase-dependent coverage
invalidation, complaint lifecycle/category/applicability, cancellation/child writes,
duplicate complaints and cross-company ownership affinity.

Model/service/Admin tests cover protected identity, presenter independence, explicit
company scope, timestamp handling, frozen warranty copies and boundary dates,
cancellation, removal history, counter corruption/exhaustion and rollback, child
rollback, database constraints, deletion, CSRF, stale forms and query counts.

Full regression: **726 passed, 0 failed, 0 skipped** (492.494 seconds): all 657
unchanged baseline tests plus 69 additive tests. Django system checks pass,
`makemigrations --check` reports no changes, and all migrations are applied.
`git diff --check` passes; frozen application source, existing tests and historical
migrations are unchanged. No credentials or generated database/log artifacts were
added. Changes remain unstaged and uncommitted.
The results above record Phase 3A.1. Phase 3A.2 verification and engineer assignment
behavior are documented separately in [ENGINEER_ASSIGNMENT.md](ENGINEER_ASSIGNMENT.md).
Phase 3A.3 diagnosis is documented in [ENGINEER_DIAGNOSIS.md](ENGINEER_DIAGNOSIS.md).
Phase 3A.4 repair is documented in [ENGINEER_REPAIR.md](ENGINEER_REPAIR.md).
No inventory, estimate, claim decision, QC,
delivery, payment or closure is implemented. Multiple RECEIVED
cases for one Device are deliberately not prohibited; no such policy was requested.
Operational policy for cases under inactive centers, correction/restoration of
immutable child facts, and business authorization integration remain deferred.

Phase 3A.4 adds REPAIRING and REPAIRED. Cancellation during REPAIRING atomically
abandons the repair attempt and closes the current engineer assignment, retaining
actions and diagnosis. Cancellation after REPAIRED is denied. Intake editing
remains RECEIVED-only.

Phase 3A.5 adds an independent [QC gate](QUALITY_CONTROL.md). Original complaints
remain immutable; QC records separate verification rows for active complaints.
Post-repair cancellation remains denied in QC_PENDING, QC_IN_PROGRESS and QC_PASSED.
After QC FAIL returns to DIAGNOSED, existing cancellation semantics apply.

Phase 3A.6 adds READY_FOR_DELIVERY, DELIVERED and terminal CLOSED. Intake facts stay
immutable; delivery creates separate acknowledgements for every received accessory.
Cancellation is denied in all three states. Presenting Customer and stable Device
references remain intact, with no ownership change. See [SERVICE_HANDOVER.md](SERVICE_HANDOVER.md).
