Assets Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the asset management module.
Assets Features and Flows
1. Documentation Evidence
| Source Type | Files or Docs | What Was Extracted |
|---|---|---|
| Schema | packages/db/src/schema/school/assets.ts, packages/db/src/schema/school/asset-contract.ts | Every table, closed vocabulary, and CHECK constraint behind the status machine |
| Controllers | apps/api/src/modules/assets/**/*.controller.ts (13 files) | Every admin and portal route, its permission, and its purpose |
| Permission catalog | packages/db/src/authorization/permission-catalog.ts (lines 671-688) | The 9 asset permission modules and their SUPERADMIN_ONLY default |
| Notification matrix | packages/db/src/schema/school/asset-contract.ts | The 8 asset notification events, their audiences, and default channels |
| Notifier | apps/api/src/modules/assets/notify/asset-notifier.service.ts | How an event is turned into a notification row |
| Inbound sync | apps/api/src/modules/assets/inbound/asset-sync.controller.ts | The accounting application's write path and conflict handling |
| Portal | apps/api/src/modules/assets/portal/asset-portal.controller.ts | The staff self-service "my assets" screen |
| People integration | apps/api/src/modules/people/shared/people-deletion.service.ts, apps/api/src/modules/people/shared/people-code.service.ts | The deletion refusal and asset code allocation |
| Error codes | apps/api/src/common/types/error-codes.ts (ASSET_*, STAFF_HAS_ACTIVE_PAY_OR_ASSETS) | Every asset-specific failure |
2. Feature Summary
| Field | Value |
|---|---|
| Module | assets |
| Submodules | catalog, register (including attachments), inbound, portal, reports, settings, notify, shared |
| Primary user value | The school keeps one authoritative register of the physical property it owns — computers, furniture, lab equipment — knowing who holds each item, where it sits, its condition, and its accounting status, and lets a linked accounting application keep its own book value and depreciation state in sync without a second data entry. |
| Actors | Admin/office staff (holding Asset*/Assets* codes, all SUPERADMIN_ONLY by default), any staff member reading their own held assets, the accounting application (via integration) |
| Main entry points | /api/assets/** and /api/asset-catalog/**//api/asset-settings/**//api/asset-reports/** admin routes, /api/mobile/staff/assets portal route, /api/asset-sync/** inbound accounting routes |
| Main outputs | Asset records and their event timeline, maintenance history, attachments, the asset change feed (consumed by integration), in-app/push/email/SMS notifications |
| Related docs | Backend, API |
3. Actor Matrix
| Actor | Can Do | Cannot Do | Auth Requirement | Notes |
|---|---|---|---|---|
Office/asset administrator (granted Asset*/Assets* codes) | Manage the catalogue (types, categories, locations), register assets, assign/transfer/return/install/uninstall, change condition, open/close maintenance, mark damaged/lost, retire/dispose/archive, resolve accounting conflicts, export the register, configure settings and the notification matrix | Anything a code they were not granted covers | JWT + RoleGuard + the specific Asset*_ACTION/Assets_ACTION code | Every asset module except MyAssets is on SUPERADMIN_ONLY_MODULES; a school grants them to a named role deliberately |
Any staff member (MyAssets_READ) | Read the list of assets currently assigned to them | See another staff member's assets, change anything | JWT + RoleGuard, no @Permissions() on the handler — AssetPortalService.resolveOwnStaffId is the control | GET /api/mobile/staff/assets |
Accounting application (via an integration client holding assets:read/assets:write) | Read asset events through the change feed, report accounting status/book value/depreciation, raise a conflict when its own field disagrees with what the school recorded | Change custody, location, or operational status | API-key auth on integration's own guard, scoped to the granted scopes | See the integration module's docs for the client-facing contract; granting or revoking assets:* needs AssetSync_UPDATE on the granting admin — INTEGRATION_SCOPE_GRANT_FORBIDDEN otherwise |
4. The Asset Status Machine
draft is a registered-but-not-yet-active record: it may hold none of custodian, room, location, or disposal fields, and it is the only status a soft delete may target (assets_deleted_only_draft). available means unheld and unplaced (assets_available_unheld); in_use means placed in a room or location and unheld (assets_in_use_placed); assigned means held by a staff member and/or a department (assets_assigned_held); retired, disposed and archived hold nothing and sit nowhere (assets_retired_unheld, assets_gone_nowhere). disposed additionally requires the disposal date, method, reason and authorizing admin to already be recorded (assets_disposed_recorded); disposal fields may only be set once the asset is disposed or archived (assets_disposal_only_when_disposed). "Active" in the product vocabulary is available, in_use, or assigned together (ASSET_ACTIVE_STATUSES); "transferred" is an event on the timeline, not a status of its own.
5. Custody and Placement
An asset is in exactly one of three places at a time, enforced by the status CHECKs above:
- Held by a staff member (
custodianStaffId) and/or a department (custodianDepartmentId) — statusassigned. - Placed in a room (
roomId) or a location (assetLocations.id) — statusin_use. A room and a location are mutually exclusive (assets_place_exclusive). - Neither — status
available,retired,disposed, orarchived.
Every custody or placement change is one of five actions on AssetAssignmentsController (assets/register/asset-assignments.controller.ts), each behind AssetAssignments_UPDATE:
| Action | Route | Effect |
|---|---|---|
| Assign | POST /api/assets/:id/assign | Hand the asset to a staff member and/or department; drops any room/location |
| Install | POST /api/assets/:id/install | Place the asset in a room or location; drops any custodian |
| Transfer | POST /api/assets/:id/transfer | Move custody or placement without an intervening return |
| Return | POST /api/assets/:id/return | Custodian hands the asset back; becomes available |
| Uninstall | POST /api/assets/:id/uninstall | Remove the asset from its room/location; becomes available |
Every transition appends an asset_events row (created, assigned, transferred, returned, status_changed, condition_changed, ...) carrying a human-readable *_holder_label alongside the id, so the timeline survives the referenced room, location, staff row, or department being deleted or renamed later.
The room field on every custody/placement action reuses the school's own room structure rather than a separate asset-only list. The picker (GET /api/asset-catalog/rooms) shows each active room's floor and the classes currently timetabled into it — the ACTIVE classes of the CURRENT academic session whose home room it is — so a clerk placing a projector can see at a glance whether a room is a classroom in active use this year. The picker is searchable across room number, name and building, and filterable to one building at a time; GET /api/asset-catalog/rooms/buildings lists the distinct building names to populate that filter. The register list and its CSV export can themselves be narrowed to one building, or to the assets currently placed in one class's home room, with both filters applying identically to the export so a filtered screen and its download always agree. None of this reads or writes the school structure — it is read-only reuse of rooms/grades/sections/classes.
6. Maintenance and Condition
AssetMaintenanceController gates condition changes and the maintenance lifecycle behind AssetMaintenance_UPDATE/AssetMaintenance_CREATE:
POST /:id/condition— record a newAssetCondition(new,good,fair,poor,damaged,critical) without opening a maintenance ticket.POST /:id/mark-damaged/POST /:id/mark-lost— move the status todamaged/lost(available fromavailable,in_use, orassigned).POST /:id/return-to-service— bring adamaged(or maintained) asset back toavailable.POST /:id/maintenance— open a ticket (assetMaintenancerow, statusopen);ASSET_MAINTENANCE_OPEN_EXISTSrefuses a second open ticket on the same asset.POST /:id/maintenance/:maintenanceId/close/.../cancel— close (completed) or cancel a ticket;ASSET_MAINTENANCE_NOT_OPENguards both.GET /:id/maintenance— the asset's maintenance history, behind plainAssets_READ.
7. Disposal
AssetDisposalController (AssetDisposal_UPDATE) offers retire, dispose, and archive as the terminal path. Disposal records the date, an AssetDisposalMethod (sold, scrapped, donated, written_off, returned_to_vendor, other), a reason, the authorizing admin, proceeds in paisa, and an optional reference — all four of date/method/reason/authorizer are mandatory once the status becomes disposed (assets_disposed_recorded). Archiving is the final state; neither disposal nor archival may be reversed through this surface.
8. Accounting Sync (Inbound)
The accounting application owns accountingStatus (unrecorded, capitalized, expensed, fully_depreciated, written_off, disposed), accountingReference, and accountingBookValuePaisa — SkoolSewa only stores what it reports, never computes depreciation itself. AssetSyncController (asset-sync.controller.ts) exposes:
GET /api/asset-sync/events(AssetSync_READ) — the inbound event log.GET /api/asset-sync/events/:id(AssetSync_READ) — one event's detail.POST /api/asset-sync/events/:id/resolve(AssetSync_UPDATE) — accept or dismiss aheld_conflict.
An inbound event lands in one of three states (ASSET_INBOUND_STATES): applied (accepted and written), stale (a timestamp older than what is already recorded — refused, not stored as a failure), or held_conflict (the accounting application's own field disagrees with a school-recorded value and needs a human decision — asset_accounting_conflict fires an admin-only email+in-app alert). Resolving a conflict is accepted (apply the incoming value) or dismissed (keep the school's value); either way the resolution is recorded, never silently dropped.
9. Notifications
AssetNotifierService (assets/notify/asset-notifier.service.ts) turns an event into rows on the shared notification pipeline, using the matrix in asset-contract.ts:
| Event | Audience | Default Channels |
|---|---|---|
asset_assigned | custodian (admin optional) | in-app, push |
asset_transferred | custodian (admin optional) | in-app, push |
asset_returned | custodian (admin optional) | in-app, push |
asset_damaged | admin | in-app |
asset_lost | admin | in-app |
asset_disposed | admin | in-app |
asset_accounting_conflict | admin | in-app, email |
asset_accounting_updated | admin | in-app |
No asset notification variable ever carries a monetary figure or an identity number — it names what happened and links to the register entry.
10. Reports and Export
AssetReportsController (asset-reports.controller.ts) exposes GET /api/asset-reports/dashboard (Assets_READ — counts by status/category/type) and GET /api/asset-reports/export (AssetExports_READ — a CSV of the register).
11. Cross-Module Effects
- People:
PeopleDeletionService.assertStaffHoldsNothingrefuses to delete a staff member who is stillcustodian_staff_idon anyassetsrow —STAFF_HAS_ACTIVE_PAY_OR_ASSETS, alongside the equivalent salary check. Return the assets first. - People code allocation:
PeopleCodeServiceallocates theassetprefix (AST) using the same six-digit sequential allocator that issues student and staff codes, producingAST-<year>-<6 digits>(assets_code_formatCHECK). - Integration: the
integrationmodule'sassets:read/assets:writescopes gate the accounting application's visibility into the change feed and its write path intoaccountingStatus/accountingBookValuePaisa; granting either scope needsAssetSync_UPDATEon the granting admin (INTEGRATION_SCOPE_GRANT_FORBIDDENotherwise).
12. Edge Cases
| Case | Behavior |
|---|---|
| Serial number reused across two active (non-deleted) assets | Refused by a case-insensitive partial unique index (assets_serial_unique); a soft-deleted asset's serial is free to reuse |
| Register an asset with a quantity greater than 1 and a serial number | Refused — a serialized asset is always singular (assets_serial_single_quantity) |
Assign an asset that is already assigned | Treated as a transfer, not a second assignment; the previous custodian's hold ends and the timeline records transferred |
| Delete a non-draft asset | Refused — soft delete only ever targets draft (assets_deleted_only_draft); a live asset is retired/disposed/archived instead |
| An inbound accounting event arrives with an older timestamp than what is recorded | Rejected as stale, not stored as a failure — no exception is raised to the caller beyond the recorded state |
| A field the accounting application is not allowed to touch appears in its payload | ASSET_INBOUND_FIELD_NOT_ALLOWED |
| A room's class belongs to a non-current academic session | It never appears in the room picker's classes list, even if the class is still is_active |
| No academic session is current | Every room in the picker returns classes: [] |
The register's classId filter names a class with no home room | Matches no assets — never falls back to an unfiltered list |
The register's building filter is applied to an asset placed in a location, not a room | Never matches — the filter only ever reaches room-placed assets |