Asset Operations Platform
Internal, Atulaya
- Role
- Software Engineer
- Type
- Internal · Production
- Systems
- Laravel Nova · LDAP · MySQL
- Scale
- 15 branches · 12 asset types
Objective
Atulaya runs IT and biomedical hardware — laptops, desktops, network gear, and diagnostic imaging equipment ("modalities") — across 15 branches, with no lifecycle system tracking who held what, when it moved, or when its warranty or service contract expired. This platform is that system: assignment, transfer, repair, warranty, and AMC/CMC service-contract tracking, from a machine's first registration to the day it's scrapped.
Assign a laptop → auto-closes any prior assignment → bundles peripherals → generates a signable undertaking PDF → the whole exchange is one append-only history row.
Why this is hard: this isn't one asset type — it's twelve, each with genuinely different attributes, all needing the same cross-cutting concerns: who has it now, what's its full history, is it under warranty, is it under an active AMC. The system has to unify without pretending every asset is the same shape.
Decisions
The assignment model
Every asset type — laptops, desktops, printers, network switches, modalities — is its own table with its own attributes. What unifies them is a polymorphic Assign model that any asset type can relate to; the row with unassigned_at IS NULL is the current holder, and the table itself, never purged, is the full audit trail. Assignment isn't one asset to one person — it's a bundle: the AssignAsset action attaches a checklist of up to 15 peripheral types, validates no duplicate device type in the submission, and re-checks each peripheral is still unassigned immediately before attaching, closing a real race window between page load and submit.
The service contract subsystem
Diagnostic modalities carry AMC, CMC, warranty, or MNC service contracts with real payment schedules — the most load-bearing part of the codebase, since it has direct financial consequences if it silently corrupts data. Contract numbers auto-generate sequentially per year, and creating a contract auto-spawns its scheduled service visits and payment installments. Editing a live contract's terms runs inside a transaction that deletes only future, not-yet-completed records before recalculating, so correcting a contract never destroys already-completed service history. A PaymentRecord's updating hook throws if a record's status is changed away from paid — a payment can't be silently un-marked, only superseded by an explicit new record.
Permissions & access control
A role column (admin, superadmin, inventory, maintenance, modalities, modality_manager) gates every Nova resource policy. Destructive actions are superadmin-only, independent of who can view or edit. Authentication runs through the corporate LDAP directory, not a locally-managed password table.
Architecture
Runtime architecture
Ops user → LDAP → Nova admin (RBAC) → Assign action → Assign table / peripheral pivot / PDF → Branch summary
- Every internal screen is Laravel Nova — no hand-rolled admin UI.
- The Branch Summary is a live, custom Nova dashboard card, not a static report.
Assignment action flow
Submit request → Auto-unassign existing → Validate peripherals → Race-condition check → Rejected / Create assign row → Attach peripherals → Generate PDF
- The race-condition check re-queries is_assigned immediately before attaching, closing the exact bug class this action exists to prevent.
Result
Fifteen branches' worth of IT and biomedical hardware now runs through one lifecycle system instead of spreadsheets and institutional memory: every assignment produces a signed undertaking and an audit-trail row, every peripheral handover is bundled and tracked, every modality's service contract auto-schedules its own visits and payments, and a live per-branch stock-count card replaces manual counting.
By the numbers
Stack
Laravel, Nova, MySQL, Redis/Horizon, LDAP, Maatwebsite Excel, Dompdf
Lessons learned
- Departments were originally modeled one-to-one per branch — a production migration later found duplicate department rows sharing a name across branches, and had to dedupe them and convert the relationship to a proper many-to-many pivot without losing referential integrity. I'd model organizational relationships as many-to-many by default now.
- The denormalized is_assigned flag, kept manually in sync with the Assign table, works today because every write path goes through a small set of Nova actions — but it has no database-level guarantee behind it. I'd derive that flag at read time instead of storing it, rather than trust every future write path to keep two sources of truth in sync by hand.
- Peripheral cascade-unassignment walks and closes every attached peripheral's Assign row outside of a database transaction — correct on the happy path, but a mid-loop failure could leave state inconsistent. I'd wrap that cascade in a transaction before it ever needs to run at higher volume.
