Skoolsewa - School Management System Docs
Developer ResourcesAssets

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

AreaFiles InspectedWhat Was Verified
ControllersAll 13 apps/api/src/modules/assets/**/*.controller.tsEvery route, method, guard, permission
Schemapackages/db/src/schema/school/assets.ts, asset-contract.tsEnums and constraints surfaced in responses
Error codesapps/api/src/common/types/error-codes.tsAll 36 ASSET_* codes plus STAFF_HAS_ACTIVE_PAY_OR_ASSETS
Permission catalogpackages/db/src/authorization/permission-catalog.tsAll 9 asset permission modules
Module compositionapps/api/src/config/swagger/swagger-documents.ts, apps/api/src/modules/mobile/mobile.module.tsWhich document each route ships in

2. Module Summary

FieldValue
Module nameAssets
Module slugassets
Primary actorsOffice/asset administrator (permissioned, superadmin-granted), any staff member (own assets only), the accounting application
API surfacesadmin, mobile (portal)
Base route prefixes/api/assets, /api/asset-catalog, /api/asset-settings, /api/asset-reports, /api/asset-sync, /api/mobile/staff/assets
Auth modelJwtAuthGuard + RoleGuard + @Permissions("Asset*_ACTION"/"Assets_ACTION") (admin); JwtAuthGuard + RoleGuard + no permission, self-resolved (portal)
PersistencePostgreSQL (packages/db)
Runtime source of truthassets and its asset_events timeline
Sibling docsFeatures/flows, Backend

3. Concepts and Terminology

TermMeaningSource
Asset codeAST-YYYY-NNNNNN, allocated once at creation, never reissuedassets.ts:assets_code_format
StatusOne of 10 values; see the feature doc's state machineasset-contract.ts:ASSET_STATUSES
CustodianThe staff member and/or department currently holding an asset (assigned only)assets.ts
PlacementThe room or location an asset is installed in (in_use only)assets.ts
Accounting fieldsaccountingStatus, accountingReference, accountingBookValuePaisa — written only via the inbound sync pathasset-contract.ts:ASSET_ACCOUNTING_STATUSES

4. Admin Routes — Catalog

MethodPathPermissionPurpose
GET/api/asset-catalog/categoriesAssetCatalog_READList categories
POST/api/asset-catalog/categoriesAssetCatalog_CREATECreate a category
PATCH/api/asset-catalog/categories/:idAssetCatalog_UPDATEEdit a category
DELETE/api/asset-catalog/categories/:idAssetCatalog_DELETEDelete a category (refused if in use — ASSET_CATEGORY_IN_USE)
GET/api/asset-catalog/typesAssetCatalog_READList types
POST/api/asset-catalog/typesAssetCatalog_CREATECreate a type
PATCH/api/asset-catalog/types/:idAssetCatalog_UPDATEEdit a type
DELETE/api/asset-catalog/types/:idAssetCatalog_DELETEDelete a type (refused if in use — ASSET_TYPE_IN_USE)
GET/api/asset-catalog/locationsAssetCatalog_READList locations
POST/api/asset-catalog/locationsAssetCatalog_CREATECreate a location
PATCH/api/asset-catalog/locations/:idAssetCatalog_UPDATEEdit a location
DELETE/api/asset-catalog/locations/:idAssetCatalog_DELETEDelete a location (refused if in use — ASSET_LOCATION_IN_USE)
GET/api/asset-catalog/roomsAssetCatalog_READRoom 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/buildingsAssetCatalog_READDistinct building names of active rooms, case-insensitively ordered — populates the room picker's building filter
GET/api/asset-catalog/staffAssetCatalog_READStaff picker for the custodian field
GET/api/asset-catalog/departmentsAssetCatalog_READDepartment picker for the custodian field

5. Admin Routes — Register

MethodPathPermissionPurpose
GET/api/assetsAssets_READPaginated, searchable asset list
POST/api/assetsAssets_CREATERegister a new asset (draft)
GET/api/assets/:idAssets_READOne asset's detail
PATCH/api/assets/:idAssets_UPDATEEdit editable fields (optimistic version)
DELETE/api/assets/:idAssets_DELETESoft-delete a draft asset (ASSET_NOT_DRAFT otherwise)
POST/api/assets/:id/activateAssets_UPDATEdraft → available
GET/api/assets/:id/eventsAssets_READThe asset's asset_events timeline
POST/api/assets/:id/attachmentsAssets_UPDATEUpload an attachment (ASSET_ATTACHMENT_LIMIT enforced)
GET/api/assets/:id/attachmentsAssets_READList attachments
GET/api/assets/:id/attachments/:attachmentId/downloadAssets_READDownload an attachment
DELETE/api/assets/:id/attachments/:attachmentIdAssets_UPDATERemove an attachment
POST/api/assets/:id/assignAssetAssignments_UPDATEAssign to a staff member and/or department
POST/api/assets/:id/installAssetAssignments_UPDATEPlace in a room or location
POST/api/assets/:id/transferAssetAssignments_UPDATEMove custody/placement without a return
POST/api/assets/:id/returnAssetAssignments_UPDATECustodian returns the asset
POST/api/assets/:id/uninstallAssetAssignments_UPDATERemove from its room/location
POST/api/assets/:id/conditionAssetMaintenance_UPDATERecord a condition change
POST/api/assets/:id/mark-damagedAssetMaintenance_UPDATEMark damaged
POST/api/assets/:id/mark-lostAssetMaintenance_UPDATEMark lost
POST/api/assets/:id/return-to-serviceAssetMaintenance_UPDATEBring back to available
POST/api/assets/:id/maintenanceAssetMaintenance_CREATEOpen a maintenance ticket
POST/api/assets/:id/maintenance/:maintenanceId/closeAssetMaintenance_UPDATEClose a ticket
POST/api/assets/:id/maintenance/:maintenanceId/cancelAssetMaintenance_UPDATECancel a ticket
GET/api/assets/:id/maintenanceAssets_READMaintenance history
POST/api/assets/:id/retireAssetDisposal_UPDATERetire
POST/api/assets/:id/disposeAssetDisposal_UPDATEDispose (requires method/reason)
POST/api/assets/:id/archiveAssetDisposal_UPDATEArchive

6. Admin Routes — Sync, Reports, Settings

MethodPathPermissionPurpose
GET/api/asset-sync/eventsAssetSync_READInbound accounting event log
GET/api/asset-sync/events/:idAssetSync_READOne inbound event's detail
POST/api/asset-sync/events/:id/resolveAssetSync_UPDATEAccept 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

MethodPathPermissionPurpose
GET/api/mobile/staff/assetsnone (AssetPortalService.resolveOwnStaffId is the control)The assets currently assigned to the caller

8. Error Codes

CodeMeaning
ASSET_ATTACHMENT_INVALIDAttachment upload failed validation
ASSET_ATTACHMENT_LIMITPer-asset attachment cap reached
ASSET_ATTACHMENT_NOT_FOUNDReferenced attachment does not exist
ASSET_CATEGORY_CODE_TAKEN / ASSET_CATEGORY_NAME_TAKENDuplicate category code/name
ASSET_CATEGORY_IN_USECategory still referenced by an asset
ASSET_CATEGORY_NOT_FOUNDUnknown category
ASSET_CATEGORY_TYPE_MISMATCHCategory does not belong to the asset's type
ASSET_CODE_RANGE_EXHAUSTEDA year's six-digit asset code range is spent
ASSET_CONFLICT_ALREADY_RESOLVEDResolving an already-resolved inbound conflict
ASSET_CONFLICT_NOT_FOUNDUnknown inbound conflict
ASSET_CUSTODIAN_NOT_FOUNDReferenced staff/department does not exist
ASSET_CUSTODY_INVALIDCustody assignment violates the status/custody invariant
ASSET_DISPOSAL_INVALIDDisposal payload missing a required field
ASSET_INBOUND_CLIENT_NOT_DESIGNATEDNo accounting client configured for asset sync
ASSET_INBOUND_EVENT_ID_REUSEDIdempotency key reused with a different payload
ASSET_INBOUND_EVENT_NOT_FOUNDUnknown inbound event
ASSET_INBOUND_FIELD_NOT_ALLOWEDAccounting payload touched a field it may not own
ASSET_INBOUND_TIMESTAMP_INVALIDMalformed or stale inbound timestamp
ASSET_LOCATION_IN_USE / ASSET_LOCATION_NAME_TAKEN / ASSET_LOCATION_NOT_FOUNDLocation lifecycle guards
ASSET_MAINTENANCE_NOT_FOUNDUnknown maintenance ticket
ASSET_MAINTENANCE_NOT_OPENClosing/cancelling a ticket that is not open
ASSET_MAINTENANCE_OPEN_EXISTSA second open ticket on the same asset
ASSET_NOT_DRAFTDeleting an asset that is not draft
ASSET_NOT_FOUNDUnknown asset
ASSET_PLACE_INVALIDRoom and location both supplied, or neither, where one is required
ASSET_ROOM_NOT_FOUNDUnknown room
ASSET_SERIAL_TAKENSerial number already active on another asset
ASSET_STATUS_TRANSITION_INVALIDThe requested status change is not on the state machine
ASSET_TYPE_CODE_TAKEN / ASSET_TYPE_IN_USE / ASSET_TYPE_NAME_TAKEN / ASSET_TYPE_NOT_FOUNDType lifecycle guards
ASSET_VERSION_CONFLICTOptimistic version mismatch on an edit
STAFF_HAS_ACTIVE_PAY_OR_ASSETSStaff 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:

DTORouteFields beyond version
ActivateAssetDtoPOST :id/activateroomId, locationId, custodianStaffId, custodianDepartmentId — all optional; place the asset directly on activation, or leave it available
AssignAssetDtoPOST :id/assigncustodianStaffId/custodianDepartmentId (optional — at least one expected), roomId/locationId (optional), occurredOn (optional AD date, defaults to today), notes, reason (optional nullable)
InstallAssetDtoPOST :id/installroomId/locationId (optional — one expected), occurredOn, notes, reason
TransferAssetDtoPOST :id/transferSame custody/place fields as assign, plus reason required (3-2000) — a transfer always needs a stated reason, unlike assign/install
ReturnAssetDtoPOST :id/returnroomId/locationId (optional — where it lands after return), occurredOn, notes, reason (optional nullable)
UninstallAssetDtoPOST :id/uninstalllocationId (optional), occurredOn, notes, reason (optional nullable)
ChangeConditionDtoPOST :id/conditioncondition (required, AssetCondition), occurredOn, notes
MarkDamagedDtoPOST :id/mark-damagedreason (required, 3-2000), condition (optional, damaged/critical, default damaged), occurredOn, notes
MarkLostDtoPOST :id/mark-lostreason (required, 3-2000), occurredOn, notes
ReturnToServiceDtoPOST :id/return-to-servicecustodianStaffId/custodianDepartmentId/roomId/locationId (optional — where/to whom it returns), condition (optional), reason (required, 3-2000), occurredOn, notes
RetireAssetDtoPOST :id/retirereason (required, 3-2000), roomId/locationId (optional — rare, a retired asset is normally unheld and unplaced), occurredOn, notes
DisposeAssetDtoPOST :id/disposedisposedOn (required date), method (required — an AssetDisposalMethod value), reason (required, 3-2000), proceedsPaisa (optional nullable, 0-999,999,999,999), reference (optional nullable, ≤128), notes
ArchiveAssetDtoPOST :id/archivenone beyond version
OpenMaintenanceDtoPOST :id/maintenancedescription (required, 3-2000), vendor (optional nullable, ≤150), costPaisa (optional nullable), reference (optional nullable, ≤128), openedOn (optional, defaults today), notes
CloseMaintenanceDtoPOST :id/maintenance/:maintenanceId/closeoutcome (required, returned/damaged), conditionAfter (optional), closedOn (optional, defaults today), costPaisa (optional nullable), notes
CancelMaintenanceDtoPOST :id/maintenance/:maintenanceId/cancelnotes (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 groupCodes it can returnConsumer guidance
Types, Categories, LocationsASSET_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_RANGEASSET_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_INVALIDASSET_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 / ConditionASSET_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.
DisposalASSET_DISPOSAL_INVALID, ASSET_STATUS_TRANSITION_INVALIDASSET_DISPOSAL_INVALID: every one of disposedOn/method/reason is required together — there is no partial disposal record.
AttachmentsASSET_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 SyncASSET_INBOUND_CLIENT_NOT_DESIGNATED/_EVENT_ID_REUSED/_EVENT_NOT_FOUND/_FIELD_NOT_ALLOWED/_TIMESTAMP_INVALID, ASSET_CONFLICT_ALREADY_RESOLVED/_NOT_FOUNDThese 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 integrationSTAFF_HAS_ACTIVE_PAY_OR_ASSETSReturned 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.