Assets API Reference
Complete API contracts for the asset management module, including routes, auth, DTOs, responses, errors, and integration notes.
Assets - API Reference
Audience: Frontend engineers, mobile/panel engineers, backend engineers, QA, and the accounting integration's developers (for the read-only vocabulary the accounting API mirrors).
Scope: Admin (/api/assets/**, /api/asset-catalog/**, /api/asset-settings/**, /api/asset-reports/**, /api/asset-sync/**) and mobile portal (/api/mobile/staff/assets) routes owned by the asset management module. The accounting-facing sync payload shape is documented alongside the integration module.
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | All 13 apps/api/src/modules/assets/**/*.controller.ts | Every route, method, guard, permission |
| Schema | packages/db/src/schema/school/assets.ts, asset-contract.ts | Enums and constraints surfaced in responses |
| Error codes | apps/api/src/common/types/error-codes.ts | All 36 ASSET_* codes plus STAFF_HAS_ACTIVE_PAY_OR_ASSETS |
| Permission catalog | packages/db/src/authorization/permission-catalog.ts | All 9 asset permission modules |
| Module composition | apps/api/src/config/swagger/swagger-documents.ts, apps/api/src/modules/mobile/mobile.module.ts | Which document each route ships in |
2. Module Summary
| Field | Value |
|---|---|
| Module name | Assets |
| Module slug | assets |
| Primary actors | Office/asset administrator (permissioned, superadmin-granted), any staff member (own assets only), the accounting application |
| API surfaces | admin, mobile (portal) |
| Base route prefixes | /api/assets, /api/asset-catalog, /api/asset-settings, /api/asset-reports, /api/asset-sync, /api/mobile/staff/assets |
| Auth model | JwtAuthGuard + RoleGuard + @Permissions("Asset*_ACTION"/"Assets_ACTION") (admin); JwtAuthGuard + RoleGuard + no permission, self-resolved (portal) |
| Persistence | PostgreSQL (packages/db) |
| Runtime source of truth | assets and its asset_events timeline |
| Sibling docs | Features/flows, Backend |
3. Concepts and Terminology
| Term | Meaning | Source |
|---|---|---|
| Asset code | AST-YYYY-NNNNNN, allocated once at creation, never reissued | assets.ts:assets_code_format |
| Status | One of 10 values; see the feature doc's state machine | asset-contract.ts:ASSET_STATUSES |
| Custodian | The staff member and/or department currently holding an asset (assigned only) | assets.ts |
| Placement | The room or location an asset is installed in (in_use only) | assets.ts |
| Accounting fields | accountingStatus, accountingReference, accountingBookValuePaisa — written only via the inbound sync path | asset-contract.ts:ASSET_ACCOUNTING_STATUSES |
4. Admin Routes — Catalog
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/asset-catalog/categories | AssetCatalog_READ | List categories |
| POST | /api/asset-catalog/categories | AssetCatalog_CREATE | Create a category |
| PATCH | /api/asset-catalog/categories/:id | AssetCatalog_UPDATE | Edit a category |
| DELETE | /api/asset-catalog/categories/:id | AssetCatalog_DELETE | Delete a category (refused if in use — ASSET_CATEGORY_IN_USE) |
| GET | /api/asset-catalog/types | AssetCatalog_READ | List types |
| POST | /api/asset-catalog/types | AssetCatalog_CREATE | Create a type |
| PATCH | /api/asset-catalog/types/:id | AssetCatalog_UPDATE | Edit a type |
| DELETE | /api/asset-catalog/types/:id | AssetCatalog_DELETE | Delete a type (refused if in use — ASSET_TYPE_IN_USE) |
| GET | /api/asset-catalog/locations | AssetCatalog_READ | List locations |
| POST | /api/asset-catalog/locations | AssetCatalog_CREATE | Create a location |
| PATCH | /api/asset-catalog/locations/:id | AssetCatalog_UPDATE | Edit a location |
| DELETE | /api/asset-catalog/locations/:id | AssetCatalog_DELETE | Delete a location (refused if in use — ASSET_LOCATION_IN_USE) |
| GET | /api/asset-catalog/rooms | AssetCatalog_READ | Room picker for the "install" action, searchable and filterable by building, each row carrying its floor and the current session's active classes |
| GET | /api/asset-catalog/rooms/buildings | AssetCatalog_READ | Distinct building names of active rooms, case-insensitively ordered — populates the room picker's building filter |
| GET | /api/asset-catalog/staff | AssetCatalog_READ | Staff picker for the custodian field |
| GET | /api/asset-catalog/departments | AssetCatalog_READ | Department picker for the custodian field |
5. Admin Routes — Register
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/assets | Assets_READ | Paginated, searchable asset list |
| POST | /api/assets | Assets_CREATE | Register a new asset (draft) |
| GET | /api/assets/:id | Assets_READ | One asset's detail |
| PATCH | /api/assets/:id | Assets_UPDATE | Edit editable fields (optimistic version) |
| DELETE | /api/assets/:id | Assets_DELETE | Soft-delete a draft asset (ASSET_NOT_DRAFT otherwise) |
| POST | /api/assets/:id/activate | Assets_UPDATE | draft → available |
| GET | /api/assets/:id/events | Assets_READ | The asset's asset_events timeline |
| POST | /api/assets/:id/attachments | Assets_UPDATE | Upload an attachment (ASSET_ATTACHMENT_LIMIT enforced) |
| GET | /api/assets/:id/attachments | Assets_READ | List attachments |
| GET | /api/assets/:id/attachments/:attachmentId/download | Assets_READ | Download an attachment |
| DELETE | /api/assets/:id/attachments/:attachmentId | Assets_UPDATE | Remove an attachment |
| POST | /api/assets/:id/assign | AssetAssignments_UPDATE | Assign to a staff member and/or department |
| POST | /api/assets/:id/install | AssetAssignments_UPDATE | Place in a room or location |
| POST | /api/assets/:id/transfer | AssetAssignments_UPDATE | Move custody/placement without a return |
| POST | /api/assets/:id/return | AssetAssignments_UPDATE | Custodian returns the asset |
| POST | /api/assets/:id/uninstall | AssetAssignments_UPDATE | Remove from its room/location |
| POST | /api/assets/:id/condition | AssetMaintenance_UPDATE | Record a condition change |
| POST | /api/assets/:id/mark-damaged | AssetMaintenance_UPDATE | Mark damaged |
| POST | /api/assets/:id/mark-lost | AssetMaintenance_UPDATE | Mark lost |
| POST | /api/assets/:id/return-to-service | AssetMaintenance_UPDATE | Bring back to available |
| POST | /api/assets/:id/maintenance | AssetMaintenance_CREATE | Open a maintenance ticket |
| POST | /api/assets/:id/maintenance/:maintenanceId/close | AssetMaintenance_UPDATE | Close a ticket |
| POST | /api/assets/:id/maintenance/:maintenanceId/cancel | AssetMaintenance_UPDATE | Cancel a ticket |
| GET | /api/assets/:id/maintenance | Assets_READ | Maintenance history |
| POST | /api/assets/:id/retire | AssetDisposal_UPDATE | Retire |
| POST | /api/assets/:id/dispose | AssetDisposal_UPDATE | Dispose (requires method/reason) |
| POST | /api/assets/:id/archive | AssetDisposal_UPDATE | Archive |
6. Admin Routes — Sync, Reports, Settings
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/asset-sync/events | AssetSync_READ | Inbound accounting event log |
| GET | /api/asset-sync/events/:id | AssetSync_READ | One inbound event's detail |
| POST | /api/asset-sync/events/:id/resolve | AssetSync_UPDATE | Accept or dismiss a held_conflict |
AssetSyncController carries @RequiresAccountingIntegration() at the class level, so all three
/api/asset-sync/** routes above — reads included — answer 404 ACCOUNTING_INTEGRATION_DISABLED while
the school's accounting-integration switch is off, checked after JwtAuthGuard/RoleGuard. See the
integration module's docs for the switch itself.
| GET | /api/asset-reports/dashboard | Assets_READ | Counts by status/category/type |
| GET | /api/asset-reports/export | AssetExports_READ | CSV export of the register |
| GET | /api/asset-settings | AssetSettings_READ | Read global asset settings |
| PATCH | /api/asset-settings | AssetSettings_UPDATE | Update global asset settings |
| GET | /api/asset-settings/notifications | AssetSettings_READ | Read the notification matrix override |
| PUT | /api/asset-settings/notifications/:eventKey | AssetSettings_UPDATE | Override one event's audiences/channels |
7. Mobile/Portal Route
| Method | Path | Permission | Purpose |
|---|---|---|---|
| GET | /api/mobile/staff/assets | none (AssetPortalService.resolveOwnStaffId is the control) | The assets currently assigned to the caller |
8. Error Codes
| Code | Meaning |
|---|---|
ASSET_ATTACHMENT_INVALID | Attachment upload failed validation |
ASSET_ATTACHMENT_LIMIT | Per-asset attachment cap reached |
ASSET_ATTACHMENT_NOT_FOUND | Referenced attachment does not exist |
ASSET_CATEGORY_CODE_TAKEN / ASSET_CATEGORY_NAME_TAKEN | Duplicate category code/name |
ASSET_CATEGORY_IN_USE | Category still referenced by an asset |
ASSET_CATEGORY_NOT_FOUND | Unknown category |
ASSET_CATEGORY_TYPE_MISMATCH | Category does not belong to the asset's type |
ASSET_CODE_RANGE_EXHAUSTED | A year's six-digit asset code range is spent |
ASSET_CONFLICT_ALREADY_RESOLVED | Resolving an already-resolved inbound conflict |
ASSET_CONFLICT_NOT_FOUND | Unknown inbound conflict |
ASSET_CUSTODIAN_NOT_FOUND | Referenced staff/department does not exist |
ASSET_CUSTODY_INVALID | Custody assignment violates the status/custody invariant |
ASSET_DISPOSAL_INVALID | Disposal payload missing a required field |
ASSET_INBOUND_CLIENT_NOT_DESIGNATED | No accounting client configured for asset sync |
ASSET_INBOUND_EVENT_ID_REUSED | Idempotency key reused with a different payload |
ASSET_INBOUND_EVENT_NOT_FOUND | Unknown inbound event |
ASSET_INBOUND_FIELD_NOT_ALLOWED | Accounting payload touched a field it may not own |
ASSET_INBOUND_TIMESTAMP_INVALID | Malformed or stale inbound timestamp |
ASSET_LOCATION_IN_USE / ASSET_LOCATION_NAME_TAKEN / ASSET_LOCATION_NOT_FOUND | Location lifecycle guards |
ASSET_MAINTENANCE_NOT_FOUND | Unknown maintenance ticket |
ASSET_MAINTENANCE_NOT_OPEN | Closing/cancelling a ticket that is not open |
ASSET_MAINTENANCE_OPEN_EXISTS | A second open ticket on the same asset |
ASSET_NOT_DRAFT | Deleting an asset that is not draft |
ASSET_NOT_FOUND | Unknown asset |
ASSET_PLACE_INVALID | Room and location both supplied, or neither, where one is required |
ASSET_ROOM_NOT_FOUND | Unknown room |
ASSET_SERIAL_TAKEN | Serial number already active on another asset |
ASSET_STATUS_TRANSITION_INVALID | The requested status change is not on the state machine |
ASSET_TYPE_CODE_TAKEN / ASSET_TYPE_IN_USE / ASSET_TYPE_NAME_TAKEN / ASSET_TYPE_NOT_FOUND | Type lifecycle guards |
ASSET_VERSION_CONFLICT | Optimistic version mismatch on an edit |
STAFF_HAS_ACTIVE_PAY_OR_ASSETS | Staff deletion refused — assets still in custody (shared with salary) |
9. Integration Notes
The accounting application never talks to /api/assets/** directly. It reads the asset change feed (asset.* events, gated by the assets:read scope) and writes only through the inbound sync path, gated by assets:write. See the integration module's docs for the client-facing request/response shapes and INTEGRATION_EVENT_SCOPE.
10. DTOs — Catalog
CreateAssetTypeDto: code (required, ^[A-Z][A-Z0-9_]{1,29}$) · name (required, ≤80) · description (optional nullable, ≤500) · sortOrder (optional int ≥0, default 0). UpdateAssetTypeDto: name/description/sortOrder optional, plus status (optional, AssetCatalogStatus) and required version. AssetTypeDto (response) adds publicId, categoryCount, assetCount, createdAt/updatedAt.
CreateAssetCategoryDto: typeId (required uuid) · code (required, same pattern) · name (required, ≤80) · description (optional nullable, ≤500) · accountingCode (optional nullable, ≤64 — the chart-of-accounts mapping). UpdateAssetCategoryDto: typeId is immutable — every other field optional, plus status and version. AssetCategoryDto adds type ({publicId, code, name}), assetCount.
CreateAssetLocationDto: name (required, ≤120) · locationType (required, AssetLocationType) · description (optional nullable, ≤500). UpdateAssetLocationDto: all fields optional plus isActive (bool) and version. AssetLocationDto adds assetCount.
AssetVersionBodyDto/AssetVersionQueryDto (shared, asset-common.dto.ts): a single version field (int ≥1) — the body form on every mutating POST/PATCH, the query form (?version=) on the three catalog DELETE routes, since a DELETE has no body. AssetSubResourceQueryDto (events/maintenance-history lists): pagination (default true), page/size (default 1/20, size capped 100).
Lookup DTOs (asset-catalog/rooms, /staff, /departments): AssetRoomLookupDto (publicId, roomNumber, name nullable, building, floor, classes — an array of AssetRoomClassDto: id the class's public_id, label its display name), AssetStaffLookupDto/AssetDepartmentLookupDto (publicId, name, and for staff employeeCode) — all read-only pickers behind AssetCatalog_READ, each accepting the shared QueryDto search/pagination params only. AssetRoomLookupQueryDto adds building (optional, ≤128 — exact match, case-insensitive); the inherited search matches roomNumber, name or building, each case-insensitively. classes lists the ACTIVE classes of the CURRENT academic session (academic_sessions.is_current) whose home room is this one, ordered by grade then section, each labelled by the class's own name when set or "<grade name> — <section name> — <Morning|Day>" otherwise, the same words the Classes screen uses; a room with no such class, or when no session is current, gets []. GET /api/asset-catalog/rooms/buildings returns ResponseDto<string[]> — no request DTO, no pagination envelope, just the distinct building names of active rooms.
Both the classes rule above and the building list come from RoomOccupancyService (academic-structure/rooms/room-occupancy.service.ts), owned by the academic-structure module and injected here because AssetCatalogModule imports AcademicStructureModule. The school's own Rooms API (GET /api/rooms, GET /api/rooms/buildings) calls the identical service — a room's classes and buildings read the same way regardless of which surface asks, and the school Rooms API additionally exposes assetCount per room (non-deleted assets whose status is not disposed), which this picker has no use for and does not return.
11. DTOs — Register
CreateAssetDto: name (required, ≤150) · typeId/categoryId (both required uuid) · description (optional nullable, ≤2000) · serialNumber (optional nullable, ≤120) · model/manufacturer (optional nullable, ≤120 each) · vendor (optional nullable, ≤150) · purchaseDate (optional nullable ISO date) · purchaseReference (optional nullable, ≤128) · purchaseCostPaisa (optional nullable int, 0-999,999,999,999) · warrantyExpiresOn (optional nullable ISO date) · warrantyNotes (optional nullable, ≤2000) · quantity (optional int, 1-100,000, default 1) · condition (optional, AssetCondition, default new) · notes (optional nullable, ≤2000). Registers as status: draft — there is no status field on this DTO; use activate afterward. UpdateAssetDto: every descriptive field above except typeId/categoryId/quantity-shape is patchable, plus required version; status, custody, place, accounting and disposal columns are never touched by this route — they move only through the dedicated action endpoints below.
ListAssetsQueryDto extends QueryDto with: status (optional array of AssetStatus, accepts a single value or a list), typeId/categoryId/custodianStaffId/custodianDepartmentId/roomId/locationId (all optional uuid filters), building (optional, ≤128 — assets whose room's building matches case-insensitively; an asset placed in a location, not a room, never matches), classId (optional uuid — assets placed in that class's home room; a class with no room, or an unknown class, matches nothing), condition (optional), sort (name/assetCode/purchaseDate/createdAt, default createdAt), warrantyExpiresWithinDays (optional int, 1-365 — matches assets whose warrantyExpiresOn falls between today, on the school calendar, and today + N days inclusive, and excludes drafts). The inherited search matches the asset name case-insensitively, with % and _ treated literally. The same DTO drives GET /api/asset-reports/export: every filter, including building and classId, the search and the warranty window apply there exactly as on the list, so an export of a filtered view carries the same assets. The export ignores sort and order and always orders rows by asset code, and it refuses with VALIDATION_FAILED when more than 5,000 rows match.
Action bodies, all requiring version except where noted:
| DTO | Route | Fields beyond version |
|---|---|---|
ActivateAssetDto | POST :id/activate | roomId, locationId, custodianStaffId, custodianDepartmentId — all optional; place the asset directly on activation, or leave it available |
AssignAssetDto | POST :id/assign | custodianStaffId/custodianDepartmentId (optional — at least one expected), roomId/locationId (optional), occurredOn (optional AD date, defaults to today), notes, reason (optional nullable) |
InstallAssetDto | POST :id/install | roomId/locationId (optional — one expected), occurredOn, notes, reason |
TransferAssetDto | POST :id/transfer | Same custody/place fields as assign, plus reason required (3-2000) — a transfer always needs a stated reason, unlike assign/install |
ReturnAssetDto | POST :id/return | roomId/locationId (optional — where it lands after return), occurredOn, notes, reason (optional nullable) |
UninstallAssetDto | POST :id/uninstall | locationId (optional), occurredOn, notes, reason (optional nullable) |
ChangeConditionDto | POST :id/condition | condition (required, AssetCondition), occurredOn, notes |
MarkDamagedDto | POST :id/mark-damaged | reason (required, 3-2000), condition (optional, damaged/critical, default damaged), occurredOn, notes |
MarkLostDto | POST :id/mark-lost | reason (required, 3-2000), occurredOn, notes |
ReturnToServiceDto | POST :id/return-to-service | custodianStaffId/custodianDepartmentId/roomId/locationId (optional — where/to whom it returns), condition (optional), reason (required, 3-2000), occurredOn, notes |
RetireAssetDto | POST :id/retire | reason (required, 3-2000), roomId/locationId (optional — rare, a retired asset is normally unheld and unplaced), occurredOn, notes |
DisposeAssetDto | POST :id/dispose | disposedOn (required date), method (required — an AssetDisposalMethod value), reason (required, 3-2000), proceedsPaisa (optional nullable, 0-999,999,999,999), reference (optional nullable, ≤128), notes |
ArchiveAssetDto | POST :id/archive | none beyond version |
OpenMaintenanceDto | POST :id/maintenance | description (required, 3-2000), vendor (optional nullable, ≤150), costPaisa (optional nullable), reference (optional nullable, ≤128), openedOn (optional, defaults today), notes |
CloseMaintenanceDto | POST :id/maintenance/:maintenanceId/close | outcome (required, returned/damaged), conditionAfter (optional), closedOn (optional, defaults today), costPaisa (optional nullable), notes |
CancelMaintenanceDto | POST :id/maintenance/:maintenanceId/cancel | notes (optional nullable) |
Response DTOs: AssetSummaryDto (list/detail base) carries publicId, assetCode, name, type/category refs, status, condition, serialNumber, quantity, holder (AssetHolderDto: nullable staff/department refs), place (AssetPlaceDto: nullable room/location refs — the room ref, AssetRoomRefDto, carries publicId, label ("<building> · <roomNumber>[ (<name>)]", the same shape the school Rooms screen writes; the same label is used in the asset timeline, the My assets portal and the CSV Place column) and floor; floor is typed nullable but is always populated whenever room itself is present, since a room's floor column is required — place.room as a whole is null for an asset that is not room-placed), warrantyExpiresOn (nullable AssetDateDto), accountingStatus, updatedAt, version. AssetDetailDto extends it with description, model/manufacturer/vendor, purchaseDate/purchaseReference/purchaseCostPaisa, warrantyNotes, notes, assignedAt, accounting (AssetAccountingDto: status/reference/bookValuePaisa/updatedAt), disposal (nullable AssetDisposalDto), openMaintenance (nullable AssetMaintenanceDto), attachmentCount, createdAt, allowedActions. AssetEventDto (GET :id/events) mirrors the asset_events columns documented in the backend page. AssetAttachmentDto: publicId, fileName, contentType, sizeBytes, uploadedByName, createdAt — the upload route (AssetAttachmentUploadDto) documents only file (multipart binary; multer strips it from the body before validation runs).
12. DTOs — Sync, Reports, Settings
ListAssetSyncEventsQueryDto: state (optional, AssetInboundState), assetId (optional uuid). ResolveAssetSyncEventDto: action (required, accept/dismiss), method (optional, an AssetDisposalMethod, default written_off — used only when accepting a disposal-shaped conflict), note (required, 3-2000). AssetSyncEventDto (response): publicId, asset ({publicId, assetCode, name}), state, incoming/previous (raw JSON snapshots for the diff view), externalUpdatedAt, resolution (nullable), resolvedAt/resolvedByName/resolutionNote (nullable), receivedAt.
AssetDashboardDto (GET asset-reports/dashboard): total, byStatus/byCondition (free-form {value: count} records), byType (array of AssetTypeCountDto: typeId — the type's public_id, grouped so two types sharing a name are never merged — type, count; ordered by count desc then name asc), warrantyExpiringWithin30Days, openMaintenance, heldAccountingConflicts, recentEvents (array of AssetDashboardEventDto, which extends AssetEventDto with assetId — the triggering asset's public_id — assetCode, assetName; not filtered by the asset's own deletion state, since this is history). warrantyExpiringWithin30Days is the dashboard's fixed-window count for the same predicate ListAssetsQueryDto.warrantyExpiresWithinDays=30 applies to the register list.
UpdateAssetSettingsDto: accountingClientId (required, nullable uuid — null clears the designated client), version. AssetSettingsDto (response): accountingClient (nullable {publicId, name, status}), version, updatedAt. UpdateAssetNotificationSettingDto: isEnabled (required bool), audiences (required array, subset of AssetNotificationAudience: custodian/admin), channels (required array, subset of in_app/push/email/sms), version. AssetNotificationSettingDto (response) adds allowedAudiences — the matrix's ceiling for that event key.
13. DTOs — Self Route
AssetPortalItemDto (GET /api/mobile/staff/assets, no request DTO — the route takes no query beyond auth): id, assetCode, name, category, type, condition, assignedAt (nullable ISO timestamp), place (nullable {kind: "room" | "location", name}). Never exposes purchase cost, accounting fields, or disposal data — this is the custodian's own read, not the register.
14. Error Codes by Route Group
Every mutating route can also return the shared codes: VALIDATION_FAILED, PERMISSION_INSUFFICIENT, and the group's own _NOT_FOUND.
| Route group | Codes it can return | Consumer guidance |
|---|---|---|
| Types, Categories, Locations | ASSET_TYPE_CODE_TAKEN/_NAME_TAKEN/_NOT_FOUND/_IN_USE, ASSET_CATEGORY_CODE_TAKEN/_NAME_TAKEN/_NOT_FOUND/_IN_USE/_TYPE_MISMATCH, ASSET_LOCATION_NAME_TAKEN/_NOT_FOUND/_IN_USE, ASSET_ROOM_NOT_FOUND | _IN_USE on delete: retire the row (status: inactive/archived) instead — a type/category/location referenced by any asset cannot be deleted. _TYPE_MISMATCH: the category must belong to the asset's own typeId. |
| Register (create/edit/activate) | ASSET_NOT_FOUND, ASSET_NOT_DRAFT, ASSET_SERIAL_TAKEN, ASSET_CODE_RANGE_EXHAUSTED, ASSET_VERSION_CONFLICT, CALENDAR_BS_DATE_OUT_OF_RANGE | ASSET_CODE_RANGE_EXHAUSTED is a hard stop for the whole school for the rest of the calendar year — escalate rather than retry. CALENDAR_BS_DATE_OUT_OF_RANGE (400): purchaseDate falls outside BS 2000-2090 — send a date inside that range, or null to clear it. ASSET_NOT_DRAFT on delete: the asset is past draft; retire/dispose/archive it instead of deleting. |
| Assignments (assign/install/transfer/return/uninstall) | ASSET_CUSTODIAN_NOT_FOUND, ASSET_CUSTODY_INVALID, ASSET_PLACE_INVALID, ASSET_STATUS_TRANSITION_INVALID | ASSET_PLACE_INVALID: exactly one of roomId/locationId, never both, for an install/return. ASSET_CUSTODY_INVALID: an assign needs at least one of custodianStaffId/custodianDepartmentId. |
| Maintenance / Condition | ASSET_MAINTENANCE_NOT_FOUND/_NOT_OPEN/_OPEN_EXISTS, ASSET_STATUS_TRANSITION_INVALID | _OPEN_EXISTS: close or cancel the existing ticket before opening another on the same asset. |
| Disposal | ASSET_DISPOSAL_INVALID, ASSET_STATUS_TRANSITION_INVALID | ASSET_DISPOSAL_INVALID: every one of disposedOn/method/reason is required together — there is no partial disposal record. |
| Attachments | ASSET_ATTACHMENT_INVALID, ASSET_ATTACHMENT_LIMIT, ASSET_ATTACHMENT_NOT_FOUND | _LIMIT: remove an existing attachment before adding another once the per-asset cap is reached. _INVALID: content type must be one of image/jpeg/image/png/image/webp/application/pdf, and size ≤10 MiB. |
| Inbound Sync | ASSET_INBOUND_CLIENT_NOT_DESIGNATED/_EVENT_ID_REUSED/_EVENT_NOT_FOUND/_FIELD_NOT_ALLOWED/_TIMESTAMP_INVALID, ASSET_CONFLICT_ALREADY_RESOLVED/_NOT_FOUND | These surface to the accounting application's own client, not the admin panel — see the integration module's docs for its retry guidance. _CONFLICT_ALREADY_RESOLVED: re-fetch the event; another admin already resolved it. |
| People integration | STAFF_HAS_ACTIVE_PAY_OR_ASSETS | Returned by people's staff-deletion route, not an assets route — listed here because it names assetsInCustody. Return every asset the staff member holds before retrying the deletion. |
Assets Backend Documentation
Backend architecture, data model, services, and operational behavior for the asset management module.
Accounting Integration Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for the accounting integration module.