# Asset & Inventory Management — Implementation Plan

**Status:** Phase 0 only. Plan approved for review. No code, migrations, or commits until the checkpoints in section N are accepted.  
**Module:** New nwidart module `Asset` (alias `asset`), backend and frontend.  
**Created:** 27 Sep 2026  
**Scope:** Serialized assets and quantity inventory in one module.  
**Execution WBS:** [ASSET_MANAGEMENT_WBS.md](./ASSET_MANAGEMENT_WBS.md)  
**Schema DDL:** [ASSET_MANAGEMENT_DATABASE_SCHEMA.md](./ASSET_MANAGEMENT_DATABASE_SCHEMA.md)  
**Task cards:** [ASSET_MANAGEMENT_TASK_CARDS.md](./ASSET_MANAGEMENT_TASK_CARDS.md)  
**Client feature overview (BN):** [ASSET_MANAGEMENT_FEATURES_BN.md](./ASSET_MANAGEMENT_FEATURES_BN.md)

This document is the Phase 0 discovery and architecture plan. It does not authorize implementation.

---

## How to read this

| Section | Contents |
|---------|----------|
| A | What already exists in the repository |
| B | What to reuse |
| C | What is missing |
| D | Domain model |
| E | Who owns what |
| F | How modules integrate |
| G | V1 vs later |
| H | Database model (no migrations) |
| I | Lifecycle flows |
| J | Permissions and approval |
| K | Risks that need a decision |
| L | Phased build plan |
| M | Post-V1 roadmap |
| N | Human approval checkpoints |

---

## A. Repository Findings

ERPFlow is a modular Laravel backend (`nwidart/laravel-modules`) plus a React frontend. Enabled modules are Configuration, Employee, Attendance, Payroll, Project, and Commissioning. There is no Procurement, Finance/GL, Maintenance, Vendor, or Inventory module.

Architecture that already exists and must be followed:

- Service → repository, each behind an interface, bound in the module service provider.
- Thin controllers, form requests, API resources, DTOs where the domain needs them, `ApiResponseTrait` responses.
- Tenant scope through `BelongsToCompany` and `company_id`.
- Permissions in `backend/config/actions.php`, synced to platform actions. Naming in practice is `{module}.{section}.{action-slug}` (example: `employee.assets.assign-assets`).
- Approval is a platform engine. Modules call `ApprovalGatewayContract::submit()`. If the company has not enabled approval for that module action, the gateway runs `onApproved` immediately. If enabled, it stores a payload and a registered `ApprovalExecutorInterface` applies or rejects it later. Executors are registered with `Platform::registerApprovalExecutor()`.
- Cross-module reads use a context port plus a real and a stub implementation (Commissioning → Project). Cross-module writes use an idempotent inbox, not a shared transaction (Commissioning payment-cleared inbox).
- History is two layers: platform `activity_logs` via `ActivityLogServiceContract`, and domain history tables (Employee timeline with `reference_type` / `reference_id`).
- Imports already exist as a module-local pattern, not a shared engine: Maatwebsite Excel, `employee_import_batches`, a stored file, row counts, and an error-report file. Re-import can update employees. That overwrite behavior must not be copied for assets.
- Concurrency is row locking (`lockForUpdate`) inside a service transaction (leave balances, payroll disbursement).
- Documents are per-owner file rows (`employee_documents` with versioning, `contract_attachments` with path, name, mime, size). There is no e-sign provider. The only signature is a static image on the employee ID card.

### Conflict — stop before any schema work

The Employee module already owns a live asset register:

| Table | What it actually is |
|-------|---------------------|
| `assets` | `company_id`, `name`, `code`, `status` (`available` / `assigned`). Unique `(company_id, code)`. |
| `employee_assets` | Assignment rows: employee, dates, `status` (`assigned` / `returned` / `damaged` / `lost`), remarks. No foreign keys. |

`EmployeeAssetService` creates the asset on the fly from a typed name and code (`firstOrCreateByCode`), refuses a second active assignment, and writes Employee timeline events `asset_assigned` and `asset_returned`. The profile tab `AssetTab` is the only UI. Permissions are `employee.assets.view-assets`, `assign-assets`, and `return-assets`.

That register is an HR custody list. It has no item, category, serial, location, condition, cost, quantity, tag, warranty, or approval. It occupies the table name `assets`. A second `assets` table cannot be created, and these two tables must not be altered until the coexistence decision in section N is approved.

### Other domain facts

- **Employee** is `employee_personal_infos` (`employee_no`, linked `user_id`). Employment status is `probation`, `confirmed`, `active`, `resigned`, `terminated`. Exit is a status plus timeline events. There is no offboarding workflow that checks assets. Organization placement is a dated `employee_organization_assignments` row (branch, division, department, team, section, designation, grade, cost center, work location).
- **Location** is not one hierarchy. Company → branch (`branch_type` includes `warehouse`) → division. Department is a separate tree (`parent_department_id`). Section, team, work location, and cost center are flat company masters. `work_locations` is name + code + active/inactive, with no floor, room, or parent. `geo_*` tables are postal addresses, not sites. There is no floor/room model.
- **Finance** is a cost center master and payroll payment methods. No chart of accounts, journal, fixed asset, depreciation, or disposal accounting.
- **Project `contracts`** are customer commercial contracts. They are not vendor AMCs.
- **Vendor** exists only as a user type on onboarding. There is no vendor master.
- **Maintenance, purchase request, purchase order, GRN, item catalog, unit of measure, QR/barcode, custom-field engine:** not present.
- Primary keys are bigint `id()`. Newer tables use real foreign keys and `restrict` / `cascade` chosen per relationship. Older Employee asset tables have indexes only.

---

## B. Existing Reusable Concepts

| Concept | Reuse as |
|---------|----------|
| `companies` + `BelongsToCompany` | Tenant boundary on every new table |
| `employee_personal_infos` | The only employee identity. Assignments store `employee_id`. |
| `employee_employments.status` | Exit signal. Read it. Do not close assignments when status becomes `resigned` or `terminated`. |
| `employee_organization_assignments` | Current department, branch, cost center, work location of the holder |
| `employee_timeline_events` | Employee-facing history. Asset module asks Employee to append `asset_assigned` / `asset_returned` through a port. It does not insert that table itself. |
| `work_locations` | The physical site an asset location hangs from |
| `branches` | Org/reporting site, including branches already typed `warehouse` |
| `departments` | Optional custody holder for a pool of assets. Not a location. |
| `cost_centers` | Tag on acquisition and write-off. Not a GL account. |
| `document_types` | Classification for warranty and contract files |
| `users` | Actor on assign, approve, count, acknowledge |
| Approval gateway, workflows, payloads, audits | Transfer, disposal, write-off, and stock adjustment |
| `activity_logs` | Security/audit trail of who did what |
| `actions.php` + permission sync | New `asset` module permissions |
| Maatwebsite Excel + import-batch shape | Pattern to copy inside the Asset module |
| `lockForUpdate` inside a service transaction | Balance and asset-row concurrency |
| Commissioning context port + inbox | Future Procurement, Maintenance, and Finance boundaries |
| File attachment shape from `contract_attachments` | Warranty, AMC, and handover files |
| nwidart module registration | New `Asset` module |

Do not reuse Project `contracts`, `geo_*`, payroll tables, or the user-type `vendor` as a supplier master.

---

## C. Gaps

- No item master, unit, category, or tracking mode.
- No serialized registry beyond the Employee name/code list.
- No quantity ledger or balance.
- No floor/room location under a work location.
- No QR/barcode, tag, or scan route.
- No physical verification or stock count.
- No handover acknowledgement.
- No category attributes.
- No warranty or AMC.
- No vendor master.
- No procurement documents, so GRN → asset cannot be built against a real source.
- No maintenance work orders.
- No GL, capitalization, or depreciation.
- No shared import framework and no preview-then-confirm import.
- No e-sign.
- The live `assets` / `employee_assets` feature collides with the new domain and has no migration path yet.

---

## D. Proposed Domain Model

One new module, `Asset` (alias `asset`), owns both serialized assets and quantity inventory. Splitting them would force a circular dependency on the item master. Folders inside the module keep the two lifecycles separate.

```text
Item  (asset_items.tracking_mode = SERIALIZED | QUANTITY, immutable once stock or instances exist)
  ├── Serialized Asset
  │      ├── Tag                  opaque token, QR/barcode resolves to the asset
  │      ├── Assignment           employee or department + physical location
  │      ├── Handover             acknowledgement evidence
  │      ├── Verification line    expected vs found
  │      ├── Warranty
  │      ├── Service contract     AMC, many assets per contract
  │      └── Service event        thin history until a Maintenance module exists
  │
  └── Quantity Inventory
         ├── Stock Balance        projection, one row per item + location
         ├── Stock Ledger         source of truth, append-only
         ├── Stock Count          verification line of type quantity
         └── Adjustment           approved document that posts a ledger row
```

Location sits beside both, not inside Item:

```text
work_locations (existing site)
  └── asset_locations (floor / room / cage, owned by Asset)
        ├── serialized asset.current_location_id
        └── inventory balance (item + location)
```

Department and employee are custody, not location. A laptop can sit on Floor 5 and be held by an employee whose HR department is Engineering. Quantity stock has a location and no holder.

V1 tracking modes are only `SERIALIZED` and `QUANTITY`. Lot and batch are not columns, not statuses, and not transaction types. An item cannot be both.

Status and condition stay separate.

### Status (business lifecycle)

```text
RECEIVED → AVAILABLE → ASSIGNED
AVAILABLE → UNDER_MAINTENANCE → AVAILABLE or ASSIGNED
AVAILABLE or ASSIGNED → LOST
LOST → AVAILABLE (recovery)
any non-terminal → RETIRED → DISPOSED     (disposal approval)
any non-terminal → WRITTEN_OFF            (write-off approval)
```

`DISPOSED` and `WRITTEN_OFF` are terminal. `IN_STORAGE` is not a status. Storage is `AVAILABLE` at a location marked storage. `IN_TRANSFER` is the transfer document’s state, not a second asset status. The asset stays at the source location until the approved transfer commits.

### Condition (physical)

`NEW`, `GOOD`, `FAIR`, `POOR`, `DAMAGED`, `UNUSABLE`.

Mapping from the legacy return vocabulary, when the later bridge runs:

| Legacy return status | New meaning |
|----------------------|-------------|
| `returned` | Assignment closed, asset `AVAILABLE` |
| `damaged` | Assignment closed, condition `DAMAGED` |
| `lost` | Assignment closed, status `LOST` |

---

## E. Ownership Matrix

| Responsibility | Owner | Evidence |
|----------------|-------|----------|
| Purchase request | Procurement | Module does not exist. Asset must not create one. |
| Purchase order | Procurement | Same. |
| GRN | Procurement | Same. Asset exposes an idempotent receipt inbox for a future caller. |
| Item master | Asset | Nothing else defines a stock item. Future Procurement reads it through a port. |
| Unit of measure | Asset | No unit table exists. |
| Category and attribute definitions | Asset | No custom-field framework exists. Keep this inside Asset. Do not build a platform field engine. |
| Serialized asset | Asset | New `serialized_assets`. Do not overload Employee `assets`. |
| Legacy HR asset list | Employee | Stays until the section N decision. Not deleted. |
| Quantity inventory | Asset | Ledger and balance live in this module. |
| Employee | Employee | `employee_personal_infos` only. |
| Department, branch, work location, cost center | Configuration | Referenced, not copied. |
| Floor / room | Asset | `asset_locations` under an existing work location. Do not add a parent to `work_locations` in V1. |
| Maintenance work order | Future Maintenance | Does not exist. |
| Maintenance history until that module exists | Asset | Append-only `asset_service_events`. Ownership moves when Maintenance is built. |
| Warranty | Asset | No contract module fits. |
| AMC / service contract | Asset | Project `contracts` are customer contracts. Do not reuse them. |
| Vendor master | Unowned | No table. V1 uses `asset_service_providers` inside Asset, with an explicit later move to Procurement. See section K. |
| Depreciation, GL, journals, gain/loss | Future Finance | Asset stores cost facts and an outbox row only. |
| Asset assignment and handover | Asset | Employee timeline is notified, not used as the assignment record. |
| Physical verification | Asset | One session covers serialized scan and quantity count. |
| Stock count and quantity adjustment | Asset | Adjustment posts a ledger row after approval when approval is enabled. |
| Asset disposal and write-off | Asset for the operational state, Finance for the accounting entry | Asset commits status only after approval. Finance posting is a recorded intent, not a journal. |
| Approval | Platform | Existing gateway. New executors only. |

---

## F. Integration Architecture

```text
Procurement (future)                    Asset module                         Employee
  PR / PO / GRN  --inbox, idempotent-->  receipt application                 employee_personal_infos
                                         creates assets or ledger rows        (read via port)
                                         never opens Procurement tables

Configuration                          Asset
  work_locations, branches,     <--FK-- asset_locations, balances, assets
  departments, cost_centers

Asset --port--> Employee timeline (asset_assigned / asset_returned)
Asset --read--> employment status (exit list). No auto-close.

Future Maintenance --AssetContextPort--> asset id, status, warranty, AMC
Asset service events remain until Maintenance owns work orders.

Future Finance <-- asset_finance_events (acquisition, disposal, write-off)
Asset does not post journals.

Platform ApprovalGateway
  asset.transfer | asset.dispose | asset.write-off | asset.adjust-stock
  executor commits the asset/ledger change
```

Rules:

- No cross-module database transaction.
- Asset never queries another module’s repository. It uses a port or a foreign key to a stable master (`work_locations`, `employee_personal_infos`, `cost_centers`).
- Receipt, finance, and maintenance each get an inbox or outbox with a unique source key so a retry cannot double-create assets or double-post.
- Do not add a new event bus. Laravel events inside the Asset module are enough. Cross-module delivery follows the Commissioning inbox.

### Procurement receipt (when Procurement exists)

```text
GRN committed in Procurement
  → row in asset_receipt_inbox (unique company + source + line)
  → Asset receipt service
       SERIALIZED: create N serialized_assets, status RECEIVED, location from the receipt
       QUANTITY: append RECEIPT ledger row, update balance
  → mark inbox applied
```

Until that module exists, the same receipt service is called by opening-balance import and by a manual receipt screen. Both pass a source key.

### HR exit

```text
employment status becomes resigned or terminated
  → report: assignments with returned_at null
  → human returns each asset (handover close + condition)
  → assignments are not deleted or auto-closed
```

### Finance

```text
acquisition_cost, currency, acquired_on, cost_center_id stored on the asset or on the receipt
disposal / write-off approval
  → asset status changes
  → asset_finance_events row (type, amount, asset ids, cost center)
  → no journal
```

---

## G. V1 Feature Matrix

| Feature | V1 | Note |
|---------|----|------|
| Item, category, unit, tracking mode | Required | `SERIALIZED` or `QUANTITY` only |
| Serialized asset registry, status, condition, serial, asset code | Required | New table, not Employee `assets` |
| Quantity ledger and balance | Required | Ledger is the source of truth |
| Location under existing work locations | Required | Floor/room only inside `asset_locations` |
| Employee and department assignment, return, history | Required | Blocked until the legacy-asset decision is approved |
| Physical verification session | Required | Serialized and quantity in one session model |
| Stock count, variance, adjustment document | Required | Count does not change the balance |
| QR/barcode tag and authenticated scan | Required | Token resolves to the asset. The image does not contain the asset record. |
| Handover acknowledgement | Required | User, time, condition, attachment. No e-sign vendor. |
| Transfer approval | Required | Platform gateway |
| Disposal approval | Required | Platform gateway |
| Write-off approval | Required | Platform gateway |
| Category custom attributes | Required | Definitions on the category, values on the item and the instance |
| Warranty | Required | Dates, provider, coverage, document |
| AMC / service contract | Required | Contract, assets covered, documents. Provider is the V1 stand-in for vendor. |
| Opening balance import | Required | Preview, error file, confirm. Rejects duplicates. Does not update in place. |
| Manual receipt | Required | Same application service as a future GRN, because Procurement does not exist |
| Thin maintenance history | Required | `asset_service_events` only. Not work orders. |
| Acquisition cost and finance outbox | Required | Cost facts plus `asset_finance_events`. Not depreciation. |
| Employee exit outstanding-asset list | Required | Read-only. Uses employment status. |
| Parent/child components | Future | |
| Employee asset request | Future | |
| Loan, reservation, overdue | Future | |
| Gate pass | Future | |
| Meter-based maintenance | Future | |
| Lost/stolen incident and insurance | Future | `LOST` status is V1. Investigation is not. |
| Reorder level and purchase suggestion | Future | |
| Automated notifications | Future | |
| Lot / batch tracking | Future | |
| GL, depreciation, disposal journals | Future | Needs a Finance module |
| Real GRN integration | Future | Needs a Procurement module |
| Work orders | Future | Needs a Maintenance module |
| E-sign | Future | No provider in the repo |

---

## H. Proposed Database Model

All new tenant tables use `id()`, `company_id` with `BelongsToCompany`, real foreign keys, and `restrict` on delete for masters that are referenced. No soft delete on ledger or assignment rows. No second table named `assets`.

### New tables

**`asset_categories`** — owner Asset. `company_id`, `name`, `code`, `parent_id` nullable, `status`. Unique `(company_id, code)`. Index `(company_id, parent_id)`.

**`asset_units`** — owner Asset. `company_id`, `name`, `code`, `decimal_places`. Unique `(company_id, code)`.

**`asset_items`** — owner Asset. What the organization manages. `category_id`, `unit_id`, `name`, `code`, `tracking_mode` (`SERIALIZED`|`QUANTITY`), `status`, `description`. Unique `(company_id, code)`. Index `(company_id, tracking_mode, category_id)`. `tracking_mode` cannot change after a serialized asset or a ledger row exists.

**`asset_attribute_definitions`** — owner Asset. `category_id`, `code`, `label`, `data_type` (`TEXT`|`NUMBER`|`BOOLEAN`|`DATE`|`SELECT`|`MULTI_SELECT`), `required`, `sort_order`, `status`. Unique `(category_id, code)`.

**`asset_attribute_options`** — owner Asset. Options for select types. `definition_id`, `value`, `label`, `sort_order`. Unique `(definition_id, value)`.

**`asset_item_attribute_values`** — catalog defaults. `item_id`, `definition_id`, `value_text`. Unique `(item_id, definition_id)`.

**`asset_locations`** — owner Asset. Physical child of a Configuration work location. `work_location_id`, `parent_id` nullable, `branch_id` nullable, `name`, `code`, `kind` (`site`|`floor`|`room`|`store`), `status`. Unique `(company_id, code)`. Index `(work_location_id, parent_id)`.

**`serialized_assets`** — owner Asset. One physical instance. `item_id`, `asset_code`, `serial_number` nullable, `status`, `condition`, `current_location_id`, `acquired_on`, `acquisition_cost` decimal(15,2), `currency`, `cost_center_id` nullable, `legacy_asset_id` nullable (filled only by the approved bridge). Unique `(company_id, asset_code)`. Unique `(company_id, item_id, serial_number)` where serial is not null. Index `(company_id, status, current_location_id)`.

**`serialized_asset_attribute_values`** — instance values. `serialized_asset_id`, `definition_id`, `value_text`. Unique `(serialized_asset_id, definition_id)`. V1 stores the current value and logs the change in `activity_logs`. A separate value-history table waits until a report needs it.

**`asset_tags`** — owner Asset. `serialized_asset_id`, `token` (opaque, unique globally), `symbology` (`QR`|`BARCODE`), `status` (`active`|`replaced`|`void`). Unique `token`. One active tag per asset, enforced by retiring the previous row in the same transaction.

**`asset_tag_events`** — replacement history. `tag_id`, `event` (`issued`|`replaced`|`voided`), `reason`, `actor_id`, `created_at`.

**`serialized_asset_assignments`** — owner Asset. `serialized_asset_id`, `holder_type` (`employee`|`department`), `employee_id` nullable, `department_id` nullable, `location_id`, `assigned_at`, `returned_at`, `assigned_by`, `returned_by`, `condition_at_issue`, `condition_at_return`, `handover_id` nullable. Index `(serialized_asset_id, returned_at)`, `(company_id, employee_id, returned_at)`. Exactly one of employee or department. One open assignment per asset, enforced by locking the asset row.

**`asset_handovers`** — owner Asset. `assignment_id`, `direction` (`issue`|`return`), `status` (`pending`|`acknowledged`|`rejected`), `condition`, `issued_by`, `received_by` nullable, `acknowledged_at` nullable, `notes`, `file_path` nullable, `original_name` nullable. Acknowledgement is a user id and timestamp. The file is an optional receipt image, same shape as `contract_attachments`.

**`asset_transfers`** — `serialized_asset_id`, `from_location_id`, `to_location_id`, `from_assignment_id` nullable, `to_holder_type` nullable, `to_employee_id` nullable, `to_department_id` nullable, `status` (`draft`|`pending_approval`|`rejected`|`completed`|`cancelled`), `approval_request_id` nullable, `requested_by`. Completing it writes assignment history and the new location in one Asset transaction.

**`asset_disposals`** — `serialized_asset_id`, `reason`, `status` (same family as transfer), `approval_request_id`, `requested_by`, `disposed_at` nullable. Terminal statuses only after executor success.

**`asset_write_offs`** — same shape as disposal, plus `amount`, `cost_center_id`. Operational status becomes `WRITTEN_OFF`. A finance outbox row is written in the same Asset transaction.

**`verification_sessions`** — owner Asset. `location_id` nullable, `category_id` nullable, `mode` (`serialized`|`quantity`|`mixed`), `status` (`draft`|`in_progress`|`completed`|`reviewed`|`cancelled`), `started_by`, `reviewed_by` nullable, `started_at`, `completed_at` nullable. Snapshot of expected rows is copied onto lines when the session starts so later movement does not rewrite the expected set.

**`verification_lines`** — `session_id`, `line_type` (`serialized`|`quantity`), `serialized_asset_id` nullable, `item_id` nullable, `location_id`, `expected_qty` decimal(18,4), `counted_qty` nullable, `result` (`pending`|`found`|`missing`|`extra`|`wrong_location`|`wrong_holder`|`damaged`), `condition` nullable, `notes`. Unique `(session_id, serialized_asset_id)` for asset lines. Quantity lines unique `(session_id, item_id, location_id)`.

**`inventory_transactions`** — source of truth. `item_id`, `location_id`, `type` (`RECEIPT`|`ISSUE`|`TRANSFER_OUT`|`TRANSFER_IN`|`RETURN`|`ADJUSTMENT`|`DAMAGE`|`DISPOSAL`|`OPENING_BALANCE`), `quantity` signed decimal(18,4), `balance_after`, `reference_type`, `reference_id`, `idempotency_key`, `occurred_on`, `created_by`. Unique `(company_id, idempotency_key)`. Index `(item_id, location_id, id)`. No updates and no deletes.

**`inventory_balances`** — projection. `item_id`, `location_id`, `quantity`, `reserved_quantity` default 0. Unique `(company_id, item_id, location_id)`. `available = quantity - reserved`. V1 reservation stays 0. Updated only in the same transaction as the ledger insert, after `lockForUpdate`.

**`inventory_adjustments`** — header the ledger points at. `reason`, `status` (`draft`|`pending_approval`|`rejected`|`posted`|`cancelled`), `approval_request_id`, `verification_session_id` nullable, `requested_by`. Posting writes one `ADJUSTMENT` ledger row per line.

**`asset_service_providers`** — V1 stand-in, owner Asset. `name`, `code`, `phone`, `email`, `status`. Unique `(company_id, code)`. This is not a global vendor master. See section K.

**`asset_warranties`** — `serialized_asset_id`, `provider_id`, `starts_on`, `ends_on`, `coverage`, `terms`, `reference_no`, `file_path` nullable. Index `(company_id, ends_on)`.

**`asset_service_contracts`** — AMC. `provider_id`, `contract_no`, `starts_on`, `ends_on`, `coverage`, `sla`, `frequency`, `cost`, `currency`, `renewal_status` (`active`|`expired`|`renewed`|`cancelled`). Unique `(company_id, contract_no)`.

**`asset_service_contract_assets`** — `contract_id`, `serialized_asset_id`. Unique pair.

**`asset_service_contract_files`** — attachment rows, `contract_attachments` shape.

**`asset_service_events`** — thin maintenance log. `serialized_asset_id`, `contract_id` nullable, `warranty_id` nullable, `occurred_on`, `summary`, `cost` nullable, `external_reference` nullable. Not a work order.

**`asset_import_batches`** — `file_name`, `status` (`uploaded`|`validated`|`confirmed`|`failed`|`cancelled`), `total_rows`, `valid_rows`, `failed_rows`, `error_report_path`, `created_by`. Confirm is a separate action. A confirmed batch does not accept the same file again as an update.

**`asset_import_rows`** — staging. `batch_id`, `row_number`, `payload` json, `errors` json nullable, `result` (`pending`|`valid`|`invalid`|`created`|`skipped`). Confirm creates domain rows only from `valid` lines and stamps `created`.

**`asset_receipt_inbox`** — future GRN and current manual/import receipt. `source_type`, `source_id`, `source_line_id`, `item_id`, `location_id`, `quantity`, `status` (`pending`|`applied`|`failed`), `error`. Unique `(company_id, source_type, source_id, source_line_id)`.

**`asset_finance_events`** — outbox. `event` (`acquisition`|`disposal`|`write_off`), `serialized_asset_id` nullable, `amount`, `currency`, `cost_center_id` nullable, `reference_type`, `reference_id`, `status` (`pending`|`accepted`|`void`). Unique `(company_id, event, reference_type, reference_id)`. No GL columns.

### Existing tables to reuse, unmodified

`companies`, `users`, `employee_personal_infos`, `employee_employments`, `employee_organization_assignments`, `employee_timeline_events`, `work_locations`, `branches`, `departments`, `cost_centers`, `document_types`, `activity_logs`, all `approval_*` tables, Project `contracts`.

### Existing tables to extend — only after section N approval, not in Phase 1

- `employee_assets`: nullable `serialized_asset_id` once the bridge is approved.
- `config/actions.php`: new `asset` module section (registration, not a behavior change to Employee).
- `modules_statuses.json`: enable `Asset`.

### Existing tables that must not be modified

`assets` (Employee), `employee_assets` until the bridge phase, `work_locations`, `contracts`, `geo_*`, payroll, attendance, commissioning, approval schema, `companies`.

### Integrity rules

- Duplicate asset code, serial, QR token, item code, contract number: unique indexes.
- Negative stock: after locking the balance, reject if `quantity + delta < 0`. No configuration flag in V1.
- Concurrent stock updates: one transaction, lock balance, insert ledger, update projection.
- Duplicate GRN: inbox unique key. Applied inbox returns the existing result.
- Assignment conflict: lock `serialized_assets` row. Second open assignment fails.
- Transfer: source location and open assignment are re-read inside the executor transaction. A stale approval payload does not move the asset if state changed.
- Disposal of a disposed asset, write-off of a written-off asset: executor checks terminal status and fails the approval as `failed` rather than applying twice. Platform already has request status `failed`.
- Maintenance on retired assets: service events rejected when status is `DISPOSED` or `WRITTEN_OFF`. `RETIRED` can still receive a closing service note.
- Finance posting failure: asset status and the outbox row commit together. A later Finance module consumes `pending` rows. Asset does not roll back because Finance is down, because Finance does not exist.
- Partial receipt: serialized inbox line stores `expected_qty` and `created_qty`. Status stays `pending` until they match. Quantity receipt posts the received quantity only.
- Partial count: session can be completed with lines still `pending`. Those lines become `missing` only when the user explicitly completes the session. They do not post.
- Employee exit: report only.
- Verification consistency: expected set is snapshotted at start. Scans after start do not add to expected. Unknown codes become `extra` lines.
- Wrong location or wrong holder: line result, not a silent move.

---

## I. Lifecycle Flows

### 1. Purchase → serialized asset

```text
Future GRN line, or V1 manual receipt / opening import
  → asset_receipt_inbox (idempotent)
  → one serialized_assets row per unit, status RECEIVED, location set, cost copied
  → optional tag issued
  → mark RECEIVED → AVAILABLE when put away
```

Procurement’s own documents stay in Procurement.

### 2. Purchase → quantity

```text
Same inbox
  → inventory_transactions type RECEIPT
  → inventory_balances increased under lock
```

### 3. Asset → employee

```text
Asset AVAILABLE
  → assignment row, holder_type employee, location unchanged or updated
  → handover pending
  → status ASSIGNED
  → Employee timeline event through the Employee port
```

A second open assignment is rejected.

### 4. Asset → location

```text
current_location_id is the physical place
department is not a location
a move across locations without a holder change is still a transfer document
```

### 5. Asset → handover

```text
Issue: custodian creates handover (condition, issuer)
Employee acknowledges (user id, time) or rejects
Return: new handover direction return, condition_at_return, assignment.returned_at set
Evidence rows are never updated into silence. Rejection leaves the asset ASSIGNED and the handover rejected.
```

Exit uses the same return path. Outstanding rows are those with `returned_at` null.

### 6. Asset → QR scan

```text
Label encodes only the tag token (URL /a/{token} or the token itself)
Authenticated GET resolves token → serialized asset
Response depends on permission:
  view: identity, holder, location, status, condition, warranty summary
  verify: only inside an open session
  assign / transfer: only with those permissions, and transfer still creates a request
Hidden from a normal employee: acquisition cost, write-off, dispose, quantity adjustment
Inactive or replaced token returns not found
```

### 7. Physical verification

```text
Start session (location and/or category)
  → copy expected serialized assets (and/or quantity balances) onto lines
Scan token → line result found, or extra if not in snapshot
Wrong location / wrong holder / damaged are explicit results, not moves
Complete session
  → missing = expected and not found
Review
  → create adjustment or asset follow-up documents
  → those documents do not change stock until approved and posted
```

### 8. Stock count

```text
Quantity line: expected_qty from the snapshotted balance, counted_qty entered
variance = counted - expected
stored on the line, not on the balance
```

### 9. Adjustment

```text
From a reviewed session or a direct request
  → inventory_adjustments pending_approval
  → gateway
  → executor inserts ADJUSTMENT ledger rows (signed quantity, reason, user, time, location)
  → balance updated under lock
A rejected adjustment changes nothing in the ledger.
```

### 10. Maintenance

```text
V1: append asset_service_events (date, summary, optional warranty or AMC, optional cost)
Status may move to UNDER_MAINTENANCE and back
No work order, no technician entity
Future Maintenance module reads AssetContextPort and writes its own work orders. Asset then stores the external reference.
```

### 11. Warranty / AMC

```text
Warranty belongs to one serialized asset and one service provider
AMC belongs to one provider and many assets
Files hang off the warranty or the contract
Expiry is data for a future notification. V1 does not send alerts.
```

### 12. Finance

```text
Cost is captured at receipt or opening import
Disposal or write-off executor writes asset_finance_events
Nothing hits a journal
Reports of net book value are out of V1 because depreciation does not exist
```

### 13. Disposal

```text
Request → gateway → executor
  asset status DISPOSED (via RETIRED in the same transaction)
  open assignment must already be returned, or the request is rejected
  finance outbox row
A disposed asset cannot be assigned, transferred, or disposed again.
```

### 14. Inventory transfer

```text
TRANSFER_OUT at source and TRANSFER_IN at destination, same idempotency key family, one transaction
Both balance rows locked in stable order (lower id first) to avoid deadlock
Source cannot go negative
```

### 15. Opening balance

```text
Upload → stage rows → validate (unknown location, unknown employee, duplicate code, bad tracking mode, bad attribute)
  → error report, nothing created
Confirm → receipt service with source_type opening_batch
  serialized rows created, or OPENING_BALANCE ledger rows posted
Re-confirm of an applied row is a no-op via the inbox key
Existing codes are errors, not updates
```

---

## J. Permissions and Approval Matrix

Register these in `config/actions.php` under module `asset`. Route middleware checks the permission. The service checks the business rule (open assignment, terminal status, session state, approver).

| Action | View | Create / edit master | Assign / return | Transfer | Verify / count | Adjust stock | Dispose | Write-off | Warranty / AMC |
|--------|------|----------------------|-----------------|----------|----------------|--------------|---------|-----------|----------------|
| Asset manager | yes | yes | yes | request | yes | request | request | request | yes |
| Custodian / IT | yes | no | yes | request | yes | no | no | no | view |
| Employee (own assets) | own holder only | no | acknowledge own handover | no | no | no | no | no | view own warranty |
| Auditor | yes | no | no | no | perform count | no | no | no | view |
| Approver | inbox via platform | no | no | approve | no | approve | approve | approve | no |
| Finance reader | cost fields | no | no | no | no | no | no | view | view cost |

Suggested slugs:

- `asset.items.view`
- `asset.items.manage`
- `asset.assets.view`
- `asset.assets.assign`
- `asset.assets.transfer`
- `asset.assets.dispose`
- `asset.assets.write-off`
- `asset.inventory.view`
- `asset.inventory.adjust`
- `asset.verification.perform`
- `asset.verification.review`
- `asset.handover.acknowledge`
- `asset.contracts.manage`
- `asset.import.confirm`
- `asset.tags.issue`
- `asset.finance.view`

Approval uses existing company settings. V1 registers four actions and executors for transfer, disposal, write-off, and stock adjustment.

| Operation | Module action | When approval is off | When approval is on |
|-----------|---------------|----------------------|---------------------|
| Transfer | `asset.transfer` | Gateway runs the move immediately | Request waits. Executor moves location and assignment. |
| Disposal | `asset.dispose` | Immediate retire/dispose | Executor is the only writer of `DISPOSED` |
| Write-off | `asset.write-off` | Immediate status + finance outbox | Same, after approval |
| Stock adjustment | `asset.adjust-stock` | Posts the ledger immediately | Posts only in the executor |

Rejection stores the platform reason and sets the document `rejected`. Cancellation uses the platform `cancelled` status and leaves the asset untouched. The asset row does not change while the request is `pending`.

A normal employee token can acknowledge a handover where `received_by` is their user and can open a scan for an asset they hold or that their permission allows them to view. Scan does not grant transfer, dispose, write-off, or quantity change.

---

## K. Risks and Ambiguities

These need a decision before the phase that would otherwise guess.

1. **Legacy `assets` / `employee_assets`.** Recommended path: leave both tables untouched through Phases 1–3. Before assignment goes live, migrate rows into `serialized_assets`, point the Employee tab at the new module through a port, and stop creating legacy rows. Do not delete the old tables. Extending `assets` in place mixes a free-text HR list with the item master and blocks quantity tracking. Running both as writable registers will double-assign the same laptop.
2. **Service provider vs vendor.** V1 needs a name on warranties. There is no vendor table. Recommended: `asset_service_providers` inside Asset, documented as temporary, to be mapped when Procurement adds vendors. Do not use onboarding user type `vendor`.
3. **Item master owner** when Procurement arrives. Recommended: Asset keeps items. Procurement references `asset_items.id`. Confirm before Phase 1 so the table is not built in the wrong module.
4. **Floor and room.** Recommended: `asset_locations` under `work_locations`. Do not change the Configuration location tables in V1. Confirm that work location is the site grain operations will pick.
5. **Stock adjustment approval.** The gateway already allows a company to turn approval off. Recommended default: seed approval settings disabled, and turn them on per company. Confirm whether adjustment must be mandatory even when the company setting is off.
6. **Handover without an employee user.** Some employees may lack a login. Acknowledgement then stays `pending` and a custodian with `asset.handover.acknowledge` records it, with the attachment as evidence. Confirm that proxy acknowledgement is acceptable.
7. **Serial required or not.** Recommended: required for `SERIALIZED` items, unique per company and item. Confirm exceptions (AC units with no plate).
8. **Acquisition cost on quantity.** Recommended: optional unit cost on the opening/receipt line, stored on the ledger row, not a weighted-average engine. Weighted average is a Finance concern and is out of V1.
9. **Maintenance history ownership.** The thin event log will need a later handoff. Accept that V1 history is Asset-owned.
10. **Global unique codes on some Configuration tables** (`branch_code`, `department_code`) are not company-scoped. New Asset codes must be unique per company. Do not copy the global unique.

---

## L. Phased Implementation Plan

Each phase is one reviewable commit series. No phase starts until the previous STOP is accepted. Phases 1–3 do not write `assets` or `employee_assets`.

### Phase 1 — Domain foundation

1. **Objective.** Register the Asset module and the catalog: category, unit, item, tracking mode, attribute definitions.
2. **Scope.** Module skeleton, migrations, models, services, CRUD APIs, permissions, frontend masters. No assets, no stock.
3. **Files.** New `backend/Modules/Asset/**`, `module.json`, `modules_statuses.json`, `backend/config/actions.php` (asset section only), `frontend/src/modules/asset/**` masters.
4. **New tables.** `asset_categories`, `asset_units`, `asset_items`, `asset_attribute_definitions`, `asset_attribute_options`, `asset_item_attribute_values`, `asset_locations`.
5. **Existing tables modified.** `actions` catalog only, via the existing sync. No Employee tables.
6. **Interfaces.** Item, category, unit, attribute, and location service and repository interfaces.
7. **Integration.** FK to `work_locations`, `branches`. Read-only.
8. **Rules.** Tracking mode immutable once set. Attribute data types validated. Location code unique per company. Category tree cannot cycle.
9. **Permissions.** `asset.items.view`, `asset.items.manage`.
10. **Approval.** None.
11. **Tests.** Feature tests for unique codes, tracking mode, required attributes, location parent within company.
12. **Risks.** Building the item master in the wrong module if decision 3 in section K is reversed.
13. **Migration.** Empty tables. No data backfill.
14. **Rollback.** Drop the new module migrations. Disable the module in `modules_statuses.json`.
15. **Dependencies.** Section K decisions 3 and 4.
16. **Commit boundary.** Module boots, masters CRUD, permissions seeded, tests green.
17. **STOP.** Human confirms catalog and location grain before any instance or stock table.

### Phase 2 — Serialized registry and tags

1. **Objective.** One row per physical asset, with status, condition, code, serial, and tag token.
2. **Scope.** Registry CRUD, status transitions that do not involve assignment or approval, tag issue and replace, QR image endpoint.
3. **Files.** Asset module registry, tag, and a small QR generator. Composer dependency for QR (none exists today). Frontend asset list and detail.
4. **New tables.** `serialized_assets`, `serialized_asset_attribute_values`, `asset_tags`, `asset_tag_events`.
5. **Existing tables modified.** None.
6. **Interfaces.** Serialized asset service, tag service.
7. **Integration.** Item and location from Phase 1. `cost_centers` optional FK.
8. **Rules.** Code and serial uniqueness. Status machine from section D excluding assignment edges. Tag token opaque. Replacing a tag voids the old one in the same transaction. Scan requires auth.
9. **Permissions.** `asset.assets.view`, `asset.tags.issue`.
10. **Approval.** None yet. Dispose and transfer endpoints are not exposed.
11. **Tests.** Duplicate code/serial/token, illegal status jump, tag replace, scan auth denied.
12. **Risks.** QR library choice. Keep the payload to the token only.
13. **Migration.** No import yet. Manual create only.
14. **Rollback.** Drop Phase 2 tables. Phase 1 catalog remains.
15. **Dependencies.** Phase 1.
16. **Commit boundary.** Create asset, print tag, resolve token.
17. **STOP.** Confirm status list and whether serial is mandatory.

### Phase 3 — Quantity ledger

1. **Objective.** Append-only stock movements and a locked balance projection.
2. **Scope.** Receipt, issue, transfer, damage, disposal quantity, opening type. Manual receipt API. No employee issue of serialized assets.
3. **Files.** Inventory services inside Asset. Frontend stock by location.
4. **New tables.** `inventory_transactions`, `inventory_balances`, `asset_receipt_inbox`.
5. **Existing tables modified.** None.
6. **Interfaces.** Ledger service. Receipt application service (shared with later import and GRN).
7. **Integration.** Items with `tracking_mode = QUANTITY` only. Locations from Phase 1.
8. **Rules.** Ledger insert plus balance update in one transaction. Lock balances. Reject negative. Idempotency key required. Transfer locks both rows in id order. Serialized items rejected by this service.
9. **Permissions.** `asset.inventory.view`. Posting receipt limited to `asset.items.manage` until adjustment permission exists.
10. **Approval.** Not on operational receipt. Adjustment comes in Phase 6.
11. **Tests.** Balance math, negative rejection, duplicate idempotency key, concurrent transfer deadlock avoidance, partial quantity.
12. **Risks.** Balance drift if any code path updates quantity without a ledger row. Forbid that in review.
13. **Migration.** Empty ledger. Opening balances come in Phase 8, not here.
14. **Rollback.** Drop ledger tables only if no production quantities exist.
15. **Dependencies.** Phase 1.
16. **Commit boundary.** Receipt and transfer change balances only through the ledger.
17. **STOP.** Confirm negative stock stays hard-blocked and that unit cost is informational only.

### Phase 4 — Assignment and handover

1. **Objective.** Custody history and acknowledgement, without breaking the Employee tab.
2. **Scope.** This phase starts only after the legacy decision. Implement the approved bridge, assignment, return, handover, exit outstanding list.
3. **Files.** Asset assignment services. Employee `EmployeeAssetService` and `AssetTab` only as the approved bridge specifies. Employee timeline port.
4. **New tables.** `serialized_asset_assignments`, `asset_handovers`.
5. **Existing tables modified.** Only if the bridge is approved: nullable `serialized_asset_id` on `employee_assets`. No column changes to `assets` itself if the copy is one-way.
6. **Interfaces.** `EmployeeTimelinePort`, `EmployeeDirectoryPort` (read employee and employment status).
7. **Integration.** Employee identity. Timeline events. Exit report reads employment status.
8. **Rules.** One open assignment. Lock asset row. Handover required on employee issue. Return captures condition. Resigned employee does not auto-return. Proxy acknowledgement only with permission.
9. **Permissions.** `asset.assets.assign`, `asset.handover.acknowledge`. Employee sees own rows.
10. **Approval.** None for issue/return. Transfer is not enabled until Phase 6.
11. **Tests.** Double assignment, acknowledge, reject, return condition, exit report still lists open rows, legacy bridge does not duplicate.
12. **Risks.** Two live writers. The bridge must disable new writes on the old service in the same release as the new assignment API.
13. **Migration.** Copy existing `assets` and open `employee_assets` into the new tables. Match on `(company_id, code)`. Fail the batch on collision. Leave old rows in place as history.
14. **Rollback.** Keep old tables. Feature flag the new assignment API off and restore the old service if the copy has not been declared source of truth. Do not delete copied rows blindly.
15. **Dependencies.** Phase 2 and section K decision 1. Decision 6 on proxy acknowledgement.
16. **Commit boundary.** Assign, acknowledge, return, exit list. Old tab either reads the new API or is explicitly frozen.
17. **STOP.** Human verifies one real employee’s old assignments appear once, and the old assign button cannot create a second register.

### Phase 5 — Verification and count

1. **Objective.** Expected versus found, for assets and quantities, with no silent stock change.
2. **Scope.** Session, scan-to-verify, quantity count, results, review. Adjustment documents created as draft only.
3. **Files.** Verification services and frontend count/scan flow.
4. **New tables.** `verification_sessions`, `verification_lines`.
5. **Existing tables modified.** None.
6. **Interfaces.** Verification service.
7. **Integration.** Tags from Phase 2. Balances from Phase 3. Assignments from Phase 4 for wrong-holder.
8. **Rules.** Snapshot at start. Extra, missing, wrong location, wrong holder, damaged are results. Complete does not post. Partial lines become missing only on explicit complete.
9. **Permissions.** `asset.verification.perform`, `asset.verification.review`.
10. **Approval.** None inside the session. Posting waits for Phase 6.
11. **Tests.** Missing asset, extra asset, quantity variance stored, balance unchanged after complete.
12. **Risks.** A long session goes stale. The snapshot is the contract. Show the start time in the UI.
13. **Migration.** None.
14. **Rollback.** Drop session tables. Stock untouched because this phase does not write it.
15. **Dependencies.** Phases 2, 3, and 4 for holder checks. Quantity-only sessions can ship before Phase 4 if needed.
16. **Commit boundary.** A finished session lists variances and the balance is unchanged.
17. **STOP.** Human runs one serialized scan session and one chair count against sample data (see [First Physical Verification Cycle Runbook](./FIRST_PHYSICAL_VERIFICATION_CYCLE_RUNBOOK.md)).

### Phase 6 — Approval

1. **Objective.** Transfer, disposal, write-off, and stock adjustment go through the existing gateway.
2. **Scope.** Documents, executors, action registration, rejection and failure handling. Enable the transfer API here, not in Phase 4.
3. **Files.** `Asset/Approval/*Executor.php`, provider registration, `actions.php` action slugs, frontend request screens. Platform approval UI already lists requests. Do not change the engine.
4. **New tables.** `asset_transfers`, `asset_disposals`, `asset_write_offs`, `inventory_adjustments`, and `asset_finance_events` so disposal can write the outbox in the same release.
5. **Existing tables modified.** None in platform schema. Approval rows are created at runtime by the engine.
6. **Interfaces.** Executors for transfer, disposal, write-off, and adjustment. Each implements `execute` and `reject`.
7. **Integration.** `ApprovalGatewayContract`. Finance outbox row on disposal and write-off.
8. **Rules.** Pending request locks the operation (no second transfer). Executor re-checks state under lock. Terminal assets fail the request. Adjustment posts ledger rows. Reject writes nothing to ledger or status.
9. **Permissions.** Transfer, dispose, write-off, adjust. Approvers use existing approval permissions.
10. **Approval.** The four actions above. Seed settings with approval disabled until a company workflow is attached. “Disabled” means immediate execute, matching bank and leave behavior today.
11. **Tests.** Off = immediate. On = pending then execute. Reject leaves status. Double dispose fails. Adjustment rejection does not change balance. Stale transfer fails.
12. **Risks.** Applying the payload twice. Idempotent on document status `completed`.
13. **Migration.** None.
14. **Rollback.** Disable the four module actions. In-flight pending requests must be cancelled by an admin, not deleted.
15. **Dependencies.** Phases 2, 3, 5. Assignment for transfer of held assets (Phase 4).
16. **Commit boundary.** One approved transfer and one rejected adjustment demonstrated.
17. **STOP.** Human confirms approver resolution uses an existing resolver (direct manager or named users). Do not invent a resolver.

### Phase 7 — Warranty and AMC

1. **Objective.** Coverage records and files, using a local service provider.
2. **Scope.** Provider, warranty, contract, covered assets, files.
3. **Files.** Asset contract services and frontend.
4. **New tables.** `asset_service_providers`, `asset_warranties`, `asset_service_contracts`, `asset_service_contract_assets`, `asset_service_contract_files`.
5. **Existing tables modified.** None. Do not touch Project `contracts`.
6. **Interfaces.** Warranty and contract services.
7. **Integration.** Serialized assets only. `document_types` optional on files.
8. **Rules.** Contract number unique per company. End date after start. Overlapping warranties are allowed and listed, unless the Phase 7 STOP decides otherwise.
9. **Permissions.** `asset.contracts.manage`. Employees view warranty on assets they hold.
10. **Approval.** None in V1.
11. **Tests.** Date order, file upload, asset link, provider scoped to company.
12. **Risks.** Provider table becomes a shadow vendor master. Keep columns minimal.
13. **Migration.** None.
14. **Rollback.** Drop these tables. Assets remain.
15. **Dependencies.** Phase 2. Section K decision 2.
16. **Commit boundary.** Warranty and AMC visible on the asset.
17. **STOP.** Confirm overlapping coverages are allowed.

### Phase 8 — Opening import and receipt port

1. **Objective.** Load the existing register and accept a future GRN without a second code path.
2. **Scope.** Spreadsheet validate, error file, confirm. Manual receipt already calls the same service. Inbox is the GRN port.
3. **Files.** Import service modeled on Employee import, but reject-on-duplicate and confirm-required. Job for large files.
4. **New tables.** `asset_import_batches`, `asset_import_rows`. Inbox already from Phase 3.
5. **Existing tables modified.** None.
6. **Interfaces.** `AssetReceiptService` used by import, manual receipt, and later Procurement.
7. **Integration.** Items, locations, employees (assignee must already exist), attributes, ledger, serialized assets.
8. **Rules.** Confirm is explicit. Invalid rows are skipped only if the user confirms “valid only”. Otherwise the batch stays unconfirmed. No updates of existing asset codes. Idempotent inbox. Opening ledger type `OPENING_BALANCE`.
9. **Permissions.** `asset.import.confirm`.
10. **Approval.** Opening balance does not go through approval. It is a controlled migration. Adjustment of a bad import uses Phase 6.
11. **Tests.** Error report, duplicate code rejected, re-confirm does not double stock, partial valid confirm, custom attribute required.
12. **Risks.** Employee import’s update-in-place behavior leaking in. Tests must forbid it.
13. **Migration.** This is the data migration. Dry-run on a copy first.
14. **Rollback.** Import rows record created ids. A reversal script posts reversing ledger quantities and marks imported assets void only if none have been assigned since. Do not hard-delete if any downstream row exists.
15. **Dependencies.** Phases 1–3, and Phase 4 if the file includes assignees. Warranty columns can be a second sheet after Phase 7.
16. **Commit boundary.** One fixture file previews errors and confirms a clean file once.
17. **STOP.** Human signs off the template columns before the first real company file.

### Phase 9 — Maintenance handoff point

1. **Objective.** Record service history on the asset without building work orders.
2. **Scope.** `asset_service_events`, status `UNDER_MAINTENANCE`, `AssetContextPort` for a future module.
3. **Files.** Asset service-event API and a port interface with a real implementation.
4. **New tables.** `asset_service_events`.
5. **Existing tables modified.** None.
6. **Interfaces.** `AssetContextPort` (get asset, warranty, active AMC, status).
7. **Integration.** Warranty and AMC optional links. No Maintenance module calls yet.
8. **Rules.** Reject new events on `DISPOSED` and `WRITTEN_OFF`. Cost is informational.
9. **Permissions.** Reuse `asset.contracts.manage` for create, `asset.assets.view` for read. Split later if needed.
10. **Approval.** None.
11. **Tests.** Event on a live asset, rejection on a disposed asset, port returns coverage.
12. **Risks.** This log gets mistaken for a CMMS. Keep the columns to date, summary, cost, references.
13. **Migration.** None.
14. **Rollback.** Drop the event table.
15. **Dependencies.** Phases 2 and 7.
16. **Commit boundary.** History shows on the asset page.
17. **STOP.** Do not add technicians, schedules, or meters in this phase.

### Phase 10 — Finance facts

1. **Objective.** Hold acquisition cost and emit disposal/write-off intents.
2. **Scope.** Cost fields already on the asset from Phase 2. Outbox write from the Phase 6 executor. Read API for pending events. No journals.
3. **Files.** Finance event writer inside the executors. Read-only list.
4. **New tables.** `asset_finance_events` if it was not already added in Phase 6.
5. **Existing tables modified.** None in payroll or a nonexistent GL.
6. **Interfaces.** `AssetFinanceEventWriter` used only by receipt and by disposal/write-off executors.
7. **Integration.** `cost_centers` FK. Future Finance consumes `pending` rows.
8. **Rules.** One event per source document. Amount copied from the asset cost at posting time, stored on the event so a later edit does not rewrite history.
9. **Permissions.** `asset.finance.view` for cost fields.
10. **Approval.** Already required for write-off and disposal.
11. **Tests.** Event written once, amount frozen, no journal tables touched.
12. **Risks.** Someone treats the outbox as a ledger. Name the API “finance events”, not “journals”.
13. **Migration.** Backfill events for opening balances as `acquisition` during import confirm, not as a second job that can double-fire. Use the same unique key.
14. **Rollback.** Outbox rows can stay. They post nothing.
15. **Dependencies.** Phases 6 and 8.
16. **Commit boundary.** A write-off produces one pending event and a `WRITTEN_OFF` asset.
17. **STOP.** Finance owner confirms they do not want depreciation in this module.

### Phase 11 — Reporting and hardening

1. **Objective.** Registers and concurrency/security passes. No new lifecycle.
2. **Scope.** Asset register, by employee, by location, assignment history, verification results, missing and wrong-location lists, stock on hand, movement, variance, adjustment history. Warranty and AMC expiry lists as queries, not notifications. Acquisition cost list. No net book value.
3. **Files.** Report services and frontend report pages. Indexes added only where a report query shows a sequential scan.
4. **New tables.** None. Read models only if a measured query needs one. Default is direct queries with pagination.
5. **Existing tables modified.** Index migrations only, with a reason on each.
6. **Interfaces.** Report service, read-only.
7. **Integration.** Employee names via the directory port. No new writes to Employee.
8. **Rules.** Reports are paginated. Company scoped. Cost columns respect `asset.finance.view`.
9. **Permissions.** Reuse view permissions. No report-only bypass.
10. **Approval.** None.
11. **Tests.** Authorization on cost columns, pagination, variance report matches posted adjustments, concurrent receipt test from Phase 3 still passes.
12. **Risks.** Unbounded exports. Follow the activity-log export pattern and stream.
13. **Migration.** Index-only.
14. **Rollback.** Drop added indexes.
15. **Dependencies.** Phases 1–10 for a complete set. Reports for a slice can ship with that slice.
16. **Commit boundary.** Each report family is its own commit.
17. **STOP.** Human accepts the report list before any performance rewrite.

### Test plan (across phases)

**Unit:** tracking mode immutability, status transitions, attribute validation, one-open-assignment, handover required on employee issue, ledger balance math, verification variance, adjustment does not overwrite, transfer consistency, disposal terminal check.

**Integration:** manual receipt → serialized asset, manual receipt → quantity ledger, employee assignment and timeline port, location FK, approval on / off for transfer, disposal, write-off, and adjustment. Procurement, Maintenance, and Finance stay at the port or outbox until those modules exist.

**Feature:** QR token resolve, verification session, stock count, handover acknowledge, opening import preview and confirm, warranty, AMC.

**Concurrency:** two receipts on the same balance, duplicate inbox key, two assignments of one asset, two adjustments of one count.

---

## M. Future Roadmap

Not in any V1 phase:

- Parent/child components (server and disks, laptop and charger), including add, remove, and replace.
- Employee asset request → approval → issue → handover.
- Temporary loan and reservation with due date, return, and overdue.
- Gate pass for assets leaving the premises.
- Meter readings (km, running hours) and threshold maintenance.
- Lost/stolen case, investigation, recovery, insurance. Status `LOST` stays in V1. The case file does not.
- Reorder level and a suggested purchase request.
- Notifications for warranty, AMC, maintenance, loan, verification, and reorder.
- Lot and batch tracking.
- Procurement GRN caller, once that module exists, using `asset_receipt_inbox`.
- Maintenance work orders, taking ownership of service history.
- Finance journals, capitalization, depreciation, and disposal gain/loss, consuming `asset_finance_events`.
- E-sign provider for handover.
- Moving `asset_service_providers` into a real vendor master.

---

## N. Approval Checkpoints

Implementation does not start until these are answered.

| Checkpoint | Needed before | Decision requested |
|------------|---------------|--------------------|
| Legacy asset register | Phase 4, and any edit to `assets` or `employee_assets` | Approve the one-way copy and freeze of the Employee writer. Reject dual writable registers. |
| Item master owned by Asset | Phase 1 | Confirm Procurement will reference Asset items later. |
| Service provider table | Phase 7 | Confirm a temporary Asset-owned provider, not a fake global vendor. |
| Location grain | Phase 1 | Confirm `asset_locations` under `work_locations`, and no Configuration schema change. |
| Serial required | Phase 2 | Confirm serialized items require a unique serial, or list exceptions. |
| Proxy handover | Phase 4 | Confirm a custodian may acknowledge when the employee has no login. |
| Adjustment approval default | Phase 6 | Confirm company setting may leave approval off, matching the current gateway. |
| Overlapping warranty | Phase 7 | Confirm two active coverages on one asset are allowed. |
| Import template | Phase 8 | Sign the column list before a production file is loaded. |
| No depreciation in Asset | Phase 10 | Finance confirms the outbox is enough. |
| Each phase STOP | Next phase | Explicit go-ahead. No phase bundles the next one. |

Phase 1 waits for approval of this plan and of the Phase 1 checkpoints (item-master owner, location grain).
