Modernizing an Insurance Platform
Overview
FCI Claims Platform is the internal claims management system used by FCI’s claims teams to process, approve, and pay out claims across a network of dealerships. Built on Laravel and Vue.js with Redis-backed WebSocket infrastructure, the platform handles the full lifecycle of a claim — from intake and adjuster review, through approval, through payment — for teams working the same shared claim queue in real time.
Over [2022–2025], I worked as full-stack developer on this platform, owning several engineering initiatives that took the system from a flat, single-user CRUD workflow to one built around real-time collaboration, external financial integrations, and multi-level organizational data. Alongside the engineering work, I served as the primary technical point of contact with FCI directly — scoping what was feasible, discussing trade-offs, and proposing ideas rather than just implementing tickets — across roughly three years and two different FCI client representatives.
The three initiatives below share a common thread: each one extends the same core real-time infrastructure — WebSocket health checks, scheduled reconciliation jobs — into a new part of the business rather than building parallel systems from scratch. Expand any section for the full technical breakdown, including problem framing, implementation detail, and specific engineering trade-offs.
Business Challenge
Before these initiatives, the platform had no mechanisms for:
- Concurrent editing safety — adjusters working a shared claim queue could silently overwrite each other’s changes with no warning or record of what was lost.
- Organizational depth — dealership data synced daily from a third-party XML feed, but only added or disabled dealership records, with no way to represent dealer groups, territory managers, or territory regions.
- Controlled, auditable payment — claims were paid out manually, with no system-enforced spending limits and no separation between who approved a claim and who released its funds.
Engineering Initiatives
Real-Time Claim Ownership
Eliminated silent overwrites between adjusters sharing a claim queue by
introducing real-time ownership, lock reconciliation, and supervisor
override permissions.
Real-Time Claim Ownership
Eliminated silent overwrites between adjusters sharing a claim queue by introducing real-time ownership, lock reconciliation, and supervisor override permissions.
Problem
Adjusters worked from a shared claim queue rather than individually assigned claims, which meant two adjusters could open and edit the same claim at the same time. Whoever saved last silently overwrote the other’s changes — no warning, no conflict message, no record of what was lost.
This surfaced a few concrete problems:
- Adjusters unknowingly overwrote each other’s edits on the same claim.
- There was no visibility into who else was currently working on a claim.
- Supervisors had no way to intervene when a claim appeared “stuck.”
- As claim volume grew across more dealerships, collisions became more frequent, not less.
Solution
I built a real-time ownership and locking system that assigns a claim to the first user who opens it and broadcasts that lock instantly to anyone else viewing the same claim.
When a second adjuster opens a locked claim, they immediately see who has it — by name and role — and every editable control is disabled rather than hidden, so they can still review the claim without acting on it. Supervisors with claims-administration permissions can reclaim a lock from a lower-permission user to unblock a stuck claim, but the override explicitly can’t take a lock from another admin, preserving accountability between peers.
Technical Highlights
- Locks are cross-referenced against the claim’s live WebSocket presence channel rather than trusted on their own.
- A scheduled reconciliation cron independently re-validates every locked claim against live channel occupancy, and fails open (releases all locks) if WebSockets themselves are down.
- The admin-override rule — reclaim from adjusters, never from other admins — lives directly in the authorization check.
- Lock state syncs through a shared Vuex-backed mixin, referenced 175+ times across the frontend, instead of each component re-deriving it independently.
- Lock records are soft-deleted, preserving a full history of claim ownership.
Engineering Considerations
The system originally ran on Pusher for WebSocket broadcasting. At one point, Pusher experienced a service failure that stopped locking and unlocking entirely across the platform. The outage happened outside business hours and was resolved with a hotfix before adjusters were back at their desks, but it made clear that a third-party dependency this central needed its own safety net rather than an assumption that it would stay up.
Out of that incident came three changes: a manual kill-switch to disable
WebSockets outright without a deploy; a real health check
(WebSocketService::validateWebsockets(), used in 18 places across 12 files)
that verifies the service is actually reachable, not just enabled; and a
reconciliation cron (UnlockClaimCron) that releases every lock platform-wide
if the health check fails. Longer-term, I migrated the WebSocket layer from
Pusher to Soketi, a self-hosted, protocol-compatible alternative, without
changing the application-level API — so the migration touched none of the
175+ places in the app that consumed the locking system.
Outcome
- Eliminated silent overwrites between adjusters working the same claim.
- Gave supervisors a safe, permission-gated way to unblock stuck claims.
- Survived a live production outage of a core third-party dependency with no business disruption, and used that incident to build lasting safeguards.
- Remained one of the most heavily used pieces of frontend logic on the platform for 2+ years under my maintenance.
XML Data Synchronization & Dealership Hierarchy
Built an automated daily sync to reconcile dealer groups, territory managers, and territory regions against a nested XML feed — with real-time cascading disablement
XML Data Synchronization & Dealership Hierarchy
Built an automated daily sync to reconcile dealer groups, territory managers, and territory regions against a nested XML feed — with real-time cascading disablement
Problem
FCI Claims pulls dealership data from the FCI third-party API through daily XML feeds. There was no existing automated process to keep dealership records in the app in sync with that feed — dealership data had to be handled outside of any scheduled reconciliation. I built the automated sync job myself: a daily process that parses the XML feed and reconciles dealership records against it, including detecting new dealerships and disabling ones that dropped out.
That sync had to handle more than a flat dealership record from day one, because the business had introduced a new layer of organizational structure sitting on top of dealerships:
- Dealer Groups — collections of dealerships under a shared parent.
- Territory Managers — users responsible for a set of dealerships.
- Territory Regions — a geographic/organizational grouping tying managers and dealerships together.
Every one of these new entities exists in relationship to a dealership, and dealerships themselves change constantly through the daily feed — added, disabled, reassigned. So the sync I was building couldn’t just be an add/disable loop against one table:
- New and existing dealerships needed their group/manager/region relationships created or updated, not just their own record.
- Disabling a dealership couldn’t just flip a flag — it had knock-on effects on the dealer groups, territory managers, and territory regions tied to it.
- None of this could wait for the next page load. If a dealership was disabled, any user whose access depended on it needed to be logged out immediately, and any admin currently viewing a live table of dealerships, users, or the org hierarchy needed to see the change without refreshing.
Solution
I built the sync and the surrounding data model so that the daily XML job reconciles the full hierarchy, not just dealership records, and so that disabling a dealership cascades correctly and immediately across the app.
Data model. Rather than building a separate pivot table for every role type that can relate to a user (dealer group, territory manager, territory region), I used a single polymorphic user_relationships table keyed on relationable_id / relationable_type, with a composite unique constraint to prevent duplicate relationship rows. Dealerships themselves were extended with direct foreign keys to dealer_group_id, territory_manager_id, and territory_region_id, and a many-to-many pivot (territory_manager_territory_region) handles the fact that a single manager can cover multiple regions. Dealer groups and territory managers are soft-deleted, preserving history rather than losing the relationship the moment something is removed from the feed.
Sync reconciliation. The scheduled job walks the incoming XML and reconciles relationships in both directions — creating new dealer groups/managers/regions as they appear in the feed, updating existing dealership-to-hierarchy links as they change, and disabling dealerships that drop out.
Cascading disable, in real time. Disabling a dealership isn’t just a status flip on one row. I used model observers to react to that event and cascade it out to everything hanging off the dealership — its dealer group, territory manager, and territory region associations — and then used WebSockets to push the consequences out immediately: users whose access depended on the now-disabled dealership are logged out on the spot, and any admin with a live table of dealerships, users, or groups open in their browser sees the record disappear or update in real time, without a refresh.
Technical Highlights
- A purpose-built XML parser, because the existing one couldn’t handle this shape of data. The codebase already had an
XmlParserTraitused elsewhere for claims XML processing (parsing FCI’s claim-component responses and building the XML payloads submitted back to FCI), built around anormalizeSimpleXMLmethod that recursively walks aSimpleXMLElementinto an array. It has no way to handle repeated sibling tags at the same nesting level — which is exactly what the dealer group / territory manager / territory region data looks like once nested inside each dealer record. Rather than bend that trait to a shape it wasn’t designed for, I wroteXmlUtilityfrom scratch for the sync: it strips the SOAP envelope down to the relevant body content, walks the structure withxml_parse_into_struct, and — the genuinely hard part — detects and correctly indexes repeated tags at the same nesting level so siblings don’t clobber each other in the resulting array. AflattenArraypass then collapses single-item arrays and normalizes empty or integer keys down to a consistentNAME/Namekey, so the rest of the sync could work with a predictable shape regardless of how sparse or repetitive a given day’s feed is. - Polymorphic relationship table.
user_relationshipsunifies three otherwise-identical join tables (user-to-dealer-group, user-to-territory-manager, user-to-territory-region) into one table viarelationable_id/relationable_type, with a composite unique index (user_relation_unique) preventing the same relationship from being recorded twice. - Reconciliation, not just insertion.
DealerInfoServicewalks every dealer record in the parsed XML each run and callsupdateOrCreate— keyed on the externaluid, not the local primary key — for the dealer group, territory manager, and territory region tied to it, associating each back to the dealership as it goes. Because the incoming XML never sends an active/inactive flag,activate()explicitly stampsis_active = trueon every record seen in the current feed — which matters for reactivating something that had been deactivated in a previous run and reappeared. - Deactivation by absence. Rather than looking for explicit “removed” markers, the service tracks every dealer group/manager/region ID it saw in this run and, at the end, flips
is_active = falseon anything in the database not in that set. That singledeactivate()step is what triggers the cascade — it’s a natural side effect of diffing “what’s in today’s feed” against “what’s in the database,” rather than a separate reconciliation pass. - Pivot table rebuilt fresh every run, not merged. Territory manager-to-region assignments are collected during the loop, then
syncTerritoryRegionManagers()clears every manager’s and region’s pivot rows and re-syncs them from scratch. That guarantees the pivot table exactly matches the current day’s XML rather than accumulating stale assignments a previous day’s data never cleaned up. - Cascade logic centralized in the service, not duplicated across observers.
DealerGroupObserver,TerritoryManagerObserver, andTerritoryRegionObserverare all deliberately thin — each just callsDealerInfoService::removeAllRelations($model)onupdating. That method checksis_active, nulls out the relevant foreign key on any dealerships pointing to the now-inactive record, detaches theuser_relationshipspivot, and detaches the manager↔region pivot — one place owns what “disabled” means for each entity type, instead of three observers each reimplementing it slightly differently. - Two WebSocket channels doing two different jobs.
DealershipObserverdiffsgetDirty()against the model’s fillable attributes on every update, and only if something a user would actually see changed does it broadcast — and only afterWebSocketService::validateWebsockets()confirms the socket layer is actually reachable, reusing the same health check built for claim locking rather than re-deriving it. That broadcast goes out on a publicAuthorizedChannel(RefreshDealershipData), and the frontend maps the current route name to a targeted Vuex reload — dashboard activity, dealership profile, dealership listing, or claim search options — instead of blindly refetching everything. A second, separate event,LogoutDealershipUsers, broadcasts on a private per-dealership channel (DealershipChannel.{hash}) and is what forces an immediate router-level logout for any user currently sitting inside a dealership that just went inactive.
Engineering Considerations
The existing XmlParserTrait/normalizeSimpleXML approach in the codebase was built for a different job — parsing and building claims-related XML for FCI — and had no real way to handle repeated sibling tags at the same nesting depth, which the Dealer Group / Territory Manager / Territory Region data made unavoidable, since a single dealer record could now contain multiple nested blocks at the same level. Adapting it wasn’t realistic; the repeated-tag indexing had to be correct from the ground up or every level below it would silently misassign values. That’s what drove building XmlUtility as a dedicated parser for the new sync, with findRepeatedTags as its own explicit pass before the main array is built, so the rest of the conversion logic could assume the indexing was already correct.
Outcome
- Built an automated dealership sync where none existed before, using a purpose-built parser (
XmlUtility) capable of handling nested, repeated XML structures that the codebase’s existing XML-parsing trait couldn’t survive. - Turned dealership data management into a full reconciliation of the dealership hierarchy — dealer groups, territory managers, and territory regions all stay consistent with the source feed automatically, including reactivation if something reappears in a later feed.
- Eliminated stale org-structure data: a dealer group, manager, or region going inactive now correctly cascades to every dealership, user, and pivot relationship tied to it, instead of leaving orphaned records behind.
- Closed the real-time gap between a backend data change and its consequences for active users — disabled-dealership users are logged out immediately via a dedicated private channel, not on their next request.
- Gave admins accurate, live views of dealerships and org structure via a targeted, route-aware refresh rather than a blind reload, without needing to touch the page to see changes made by the sync or by another admin.
Virtual Credit Card (VCC) Payments via USBank
Built an idempotent, retryable payment pipeline with enforced dual-authorization, turning "approve a claim" from an instant status flip into a resilient process that survives a third-party payment API being slow, erroring, or unreliable
Virtual Credit Card (VCC) Payments via USBank
Built an idempotent, retryable payment pipeline with enforced dual-authorization, turning "approve a claim" from an instant status flip into a resilient process that survives a third-party payment API being slow, erroring, or unreliable
Problem
Claims were paid the same way regardless of amount, dealership, or who was approving them — a claim got approved, and payment happened as a manual, out-of-band step with no system-level check on who was allowed to authorize what, and no automatic linkage between “approved” and “money actually moved.” There was no per-user spending authority, no separation between the person approving a claim and the person releasing funds for it, and no record tying a specific payment instrument to a specific claim.
The business wanted to let dealerships opt into being paid by virtual credit card instead, with USBank as the issuer. That introduced requirements a manual process never had to satisfy:
- Eligibility had to be checked before a card was ever requested. Not every user is authorized to pay every dollar amount, and a single user shouldn’t be able to both approve a claim and release its payment.
- A real financial instrument now had a lifecycle. A virtual card is created, has a balance, gets used, and eventually needs to be closed — none of which a flat “approved” status could represent on its own.
- The claim’s
active_statuscouldn’t just flip to “paid” the moment a user clicked approve. Card creation is a network call to a third party that can fail, time out, or need to be retried, so the status change had to be decoupled from the approval action and made resilient to USBank’s API being slow or unreliable.
What kicks off a payment. A user with edit access hits approveClaim() on an approved claim. If the dealership has VCC enabled ($dealership->vccEnabled()), the request runs through a chain of eligibility checks — checkTotal(), pay-point/pay-limit validation, platinumShieldApproval() — before anything is sent anywhere. Only once those pass does the claim either move straight to paid (non-VCC dealerships) or into payment_in_progress, and only then does CreditCardRemit get dispatched to actually talk to USBank.
What USBank’s API does. It’s a card-issuing API, not a full payment gateway — it doesn’t push money on its own or notify FCI when a card is used. UsBankClient authenticates with a client-credentials OAuth2 flow over mutual TLS (cert/ssl_key per environment), caches the resulting bearer token for 14 minutes (just under its 15-minute expiry), and then makes card operations against /virtual-cards/v1/cards: a POST with an Idempotency-Key to create a card and get back a card ID, CVV, and effective-until date; a /{id}/realtime-credit-details GET to pull a card’s live available balance; and a /{id}/close POST to close it out. Every request carries a fresh Correlation-ID for tracing a single call through USBank’s logs.
What had to be built around it. Because card creation is an external network call sitting inside a business process, almost everything interesting in this system exists to make that call safe to retry, safe to audit, and safe to leave half-finished without corrupting claim state.
Solution
I built the eligibility and dual-authorization logic into the approval endpoint itself, and everything downstream of “eligible” — card creation, status transition, notification, and eventual closure — into an idempotent, retryable job pipeline that reconciles with USBank rather than assuming a single request-response cycle will always succeed.
Eligibility and dual authorization live in approveClaim(). A user’s UserLimit (per program) determines both their pay_limit (max claim total they can release) and their pay_points (a weighted authority score). Two rules are enforced before a card is ever requested: the same user can’t be recorded as both the claim’s approver and its payer — checked by counting statusHistory() entries with a status of approved or pending_invoice against Auth::id() — and a single user’s pay_points must meet or exceed the claim’s ProgramLimit threshold, or the claim is parked in payment_in_progress so a second, different user’s points can combine with the first’s to clear it.
Card creation and status change happen inside CreditCardRemit, not the request/response cycle. The job builds the USBank payload from the claim’s XML-formatted data, requests the card with a cached idempotency key, and only updates virtual_card_id, is_virtual_card_open, vcc_effective_until, and vcc_balance once USBank confirms creation. If a card already exists for the claim (a retry after a partial failure), it skips straight to the status transition instead of creating a second card for the same claim.
Retry and failure handling assume USBank itself is unreliable. A ServerException on a 500 triggers a re-dispatch of the same job five minutes later, up to five times, per USBank’s own documented retry guidance — reusing the same idempotency key so a retried request can’t accidentally create a duplicate card. After five failures, or on any client-side error, failed() resets the claim’s in-progress flag and emails a dedicated USBank failure inbox so a stuck claim doesn’t sit silently.
Closure is a separate, independently reconcilable lifecycle step. Cards don’t close themselves just because a claim reached paid — CreditCardClose handles that as its own job with its own retry/backoff logic, and claims:close-credit-cards can run in bulk (auditing every open card’s live balance against USBank) or target a single claim by ID for manual intervention.
Technical Highlights
- Idempotency key reuse across retries.
generateIdempotencyKey()checks the cache for an existing key tied to the claim before generating a new one, and only clears it locally when testing — meaning a retried card-creation request after a transient failure reuses the exact same key USBank already saw, which is what prevents a retry from minting a second card for the same claim. - A dedicated logging channel instead of the default log.
UsBankLogis a facade that walks the exception backtrace to capture the calling line, then writes throughLog::channel('usbanklog')with a customLineFormatter(UsBankLogger) — every VCC-related log line is both traceable to its exact call site and physically separated from general application logs, since debugging a third-party payment integration needed its own signal-to-noise ratio. - Same dual-purpose health check as claim locking, reused rather than reimplemented.
WebSocketService::validateWebsockets()— built for the locking system — gates every VCC broadcast (CreditCardUpdate,ClaimsCreditCardUpdate,GetActiveClaim) exactly the way it gates lock broadcasts, so “is the socket layer actually reachable” is answered once, in one place, for the whole platform. - A cron reconciliation job with the same shape as the locking system’s
UnlockClaimCron.ResetClaimUsBankRequestInProgressindependently sweeps for claims stuck withus_bank_request_in_progress = truewhere either 30 minutes have passed or avirtual_card_idalready exists (meaning the flag should’ve cleared but didn’t), flips it back to false, and — only if a presence channel for that claim is currently occupied — broadcasts a refresh so anyone actively viewing it sees the corrected state. It’s the same “don’t trust a single event to fire correctly, verify and self-heal on a schedule” pattern as the locking cron, applied to a different failure mode. - Two events doing two different real-time jobs, mirroring the org-sync case study’s split.
CreditCardUpdateis claim-specific — success or failure toasts scoped to whoever’s looking at that claim, including auser_hashon failure so the message can be targeted.ClaimsCreditCardUpdateis dealership-scoped, broadcasting onDealershipClaimsChannel.{hash}, and its only job is telling the claims table to quietly re-run its current search rather than pushing new data itself — the frontend listener callssearchClaims(null, this.all?.data?.current_page, this.columnActive), preserving whatever page and sort the user was already on instead of resetting them to page one. - Manual bypass for the one thing outside the system’s control. USBank’s webhooks weren’t reliable — a gap outside the platform’s own boundary, and not something retry logic on our side could fix.
claims:close-credit-cards --bypasslets an admin close a specific card by claim ID after manually verifying its state with USBank directly, while the non-bypass path audits every open card againstrealtime-credit-detailsand reconciles automatically. - Duplicate frontend authorization logic, deliberately.
canPay()in bothClaimSearchPageandClaimReviewHeaderindependently re-derives the same rule as the backend: not the same user who last approved, and the combinedpay_pointsof the current user and the claim’sactive_status.usermust meet the program’s required threshold. It’s re-implemented rather than shared because the two contexts (a full table row vs. a single claim’s action button) needed the check available synchronously in slightly different component shapes — the backend check inapproveClaim()remains the actual source of truth either way.
Engineering Considerations
The dual-approval, combined-pay-points rule was the hardest part of this system to get right, not because any single check was complicated, but because “approver and payer must differ” and “one person’s authority might not be enough, but two people’s combined might be” needed to be true consistently across three different surfaces: the approval endpoint itself, the claims table’s row-level canPay(), and the individual claim view’s canPay(). Each of those had access to slightly different data shapes — the table has a flat claim listing, the review header has the fully loaded active claim with its nested active_status.user — so the same business rule had to be re-derived against each shape correctly, rather than written once and trusted everywhere.
The failure mode that made this worth getting right: without the approver/payer check, a single user with sufficient combined authority across two logins or sessions could approve and pay a claim entirely alone, defeating the reason payment_in_progress existed in the first place. Without the combined-points check reading the correct other user ($claim->activeStatus->user, not just whoever’s currently authenticated), a claim could get silently stuck in payment_in_progress forever, because the math would never find a second party’s points to add — or worse, under-verify and let a claim clear with less combined authority than the program required. Getting the identity of “the other approver” right — pulled from the claim’s own status history rather than assumed from context — was what made the combined-limit check trustworthy enough to gate an actual financial instrument being created.
Outcome
- Gave FCI a payment path with real, enforced financial controls — spending limits per user, and a structural guarantee that approval and payment authority can’t collapse into a single person acting alone.
- Turned “approve a claim” from an instant status flip into a resilient pipeline that survives USBank being slow, erroring, or timing out, without ever risking a duplicate card for the same claim.
- Closed the gap USBank’s own webhooks left open with manual and automated reconciliation tools (
claims:close-credit-cards,ResetClaimUsBankRequestInProgress) rather than depending on a third party’s callback reliability. - Extended the platform’s existing real-time infrastructure — the same WebSocket health check and the same “reconcile on a schedule, don’t trust a single event” pattern from claim locking — into a payments context instead of building parallel infrastructure from scratch.
- Gave admins and dealership users live, accurate visibility into card and claim status without disrupting their place in a paginated, sorted table.
Claims Dashboard & Response-Time Analytics
Built the claims dashboard from scratch — role-aware activity feeds, pending-task
breakdowns, and two analytics charts — then rebuilt the underlying time
calculation after a business-hours miscalculation surfaced in production.
Claims Dashboard & Response-Time Analytics
Built the claims dashboard from scratch — role-aware activity feeds, pending-task breakdowns, and two analytics charts — then rebuilt the underlying time calculation after a business-hours miscalculation surfaced in production.
Problem
FCI’s claims teams had no single view into what needed attention or how the team was performing. Adjusters and admins had to work claim-by-claim through search filters to understand what was pending, and there was no visibility into how quickly claims were actually moving through the process.
The dashboard needed to serve two different audiences with two different permission levels — claims administrators and dealership-level users — without duplicating the page for each:
- Activity and task visibility had to reflect only the statuses and actions each role was allowed to act on, not a generic list filtered after the fact.
- Performance metrics — how long claims sat before a decision, and how long they took to actually process once submitted for payment — had no existing definition at all. What counted as the start and end of “response time” or “processing time” hadn’t been formally defined before this was built.
Solution
I built the dashboard as four independent panels — recent claim activity, a pending-tasks breakdown, two response/processing time charts, and a claim status summary — each backed by its own Vuex module state and API endpoint, so any panel can reload or filter independently of the others.
Role-aware pending tasks. DashboardModuleLoader::pendingTasks() builds an
entirely different task taxonomy depending on whether the user can edit claims.
An admin sees tasks grouped around approval authority — a single “Approvals”
bucket absorbing both Pending Approval and Re-Submitted claims, with
Re-Submitted also feeding a separate “Invoice Review” bucket filtered to
claims with an attached invoice. A dealership user instead sees their own
claim’s progress — information requests, invoice requests, awaiting payment —
with no approval-authority tasks at all, since those aren’t actionable for
their role. Each bucket’s count comes from a shared count_claims() closure
that re-applies the same status and attachment filters used to build the
bucket in the first place, rather than a second, independently-written query.
Two charts, two different definitions of “time.” DashboardService
computes average response time (claim submission → approval/decline, in
minutes) and average processing time (submitted for payment → paid, in days)
per claim type — tire & rim, key & remote, platinum shield, auto guard — off
the same underlying status-history walk, differing only in which status pairs
count and whether the result is measured in minutes or days.
Technical Highlights
- A single algorithm walking status-history pairs, parameterized by
direction.
findAvgTimeBetweenStatuses()walks each claim’s ordered status history looking for a transition from a defined “previous” status into a defined “active” status. Which status pairs qualify — and whether the result is measured in business minutes or calendar days — is entirely driven by theRespondedStatuses/ProcessedStatusesconstant passed in, so the same method backs both charts instead of two parallel implementations. - A rolling average computed inside the loop, not after it. Rather than
summing every status-change gap and dividing once at the end,
$total /= $status_change_countrecalculates the running average after every qualifying transition — which matters because a single claim can have more than one previous→active transition in its history (e.g., re-submitted and re-approved), and each one needed to be folded into that claim’s average as it was found. - Status IDs cached, not requeried per calculation.
getCachedStatusIds()caches the resolved status-ID lists for both chart types for 24 hours, since they’re looked up on every dashboard load and every filter change but change essentially never. - A debug export command for verifying the math independently of the UI.
dashboard:average-response-timeauthenticates as a super-admin, runs the sameDashboardServicelogic in a “testing” mode that returns full claim-level status data — not just the averages — then writes it to a temporary file behind a signed URL that expires in five minutes and deletes itself after download. Built specifically so the underlying claim-level data behind a suspicious average could be pulled and checked by date range, without needing direct database access. - Claim summary counts collapsed for non-admin views.
claimSummary()foldspayment_in_progresscounts intosubmitted_paymentand hides thepayment_in_progressrow entirely for non-FCIC-admin users — a payment sub-status that’s meaningful internally but not something a dealership user needs to distinguish.
Engineering Considerations
Roughly a year and a half after this shipped, following a permissions and role restructure elsewhere in the platform, a client-side executive flagged that the average response time on the dashboard looked wrong — the reported number was higher than it should reasonably be. Diagnosing it took real back-and-forth: the original ticket’s definition of “response time” wasn’t fully unambiguous, and confirming what the client actually wanted to measure required working through a client-side representative transition mid-thread, since the original contact who’d have known the original intent had since moved on.
The root cause turned out to be that the calculation counted raw wall-clock
time between status changes, including nights, weekends, and any gap where a
claim simply sat untouched outside business hours. I rebuilt the calculation
to count only business hours — 6AM to 6PM, excluding weekends — via
calculateBusinessMinutes(), which walks day-by-day between the two
timestamps, skips weekend days entirely, and clips each day’s contribution to
the 6AM–6PM window (including partial first and last days). That single
change dropped the reported average significantly and brought it in line with
what the business actually expected the number to represent.
Verifying the fix wasn’t a one-shot deploy — it went through a UAT round checking specific claims against specific dates, including edge cases like claims created or actioned outside official hours, before it was signed off as correct.
Outcome
- Gave claims teams and dealership users a single dashboard reflecting only the actions and data relevant to their own permission level, instead of a generic view filtered after the fact.
- Defined and shipped the platform’s first formal response-time and processing-time metrics, broken out per claim type.
- Diagnosed and corrected a real production data-accuracy issue flagged by the client, rebuilding the core time calculation around actual business hours rather than wall-clock time — verified through a full UAT pass before sign-off.
- Built lasting debug tooling (
dashboard:average-response-time) that lets the underlying claim-level data behind any reported average be pulled and audited independently of the dashboard UI.
Architecture
The platform is built on a Laravel backend and a Vue 2 / Vuex frontend, with MySQL for persistence and Redis-backed WebSockets (originally Pusher, later migrated to self-hosted Soketi) driving everything that needs to happen live rather than on next page load.
A single pattern recurs across every major initiative: verify state
independently rather than trusting one event to have fired correctly.
WebSocketService::validateWebsockets() became the shared health check gating
real-time broadcasts across locking, org sync, and VCC payments alike, each
paired with its own scheduled reconciliation job so a dropped connection or a
failed third-party callback can’t leave the system stuck.
Results
- Eliminated silent overwrites between adjusters, with a locking system that ran continuously in production for 2+ years (175+ component references).
- Survived a live production outage of a core third-party dependency with no business disruption, turning the incident into a lasting architecture pattern reused across every later real-time feature.
- Turned a shallow nightly sync into full reconciliation of the dealership organizational hierarchy, including real-time cascading disablement.
- Gave FCI a virtual credit card payment path with enforced financial controls and an idempotent retry pipeline that guarantees no duplicate card is ever issued for the same claim.
- Closed a reliability gap in USBank’s webhooks with automated and manual reconciliation tooling.
Client Collaboration
Beyond implementation, I worked directly with FCI’s client-side representative as the day-to-day technical point of contact — scoping what was and wasn’t feasible, walking through trade-offs on proposed features, and bringing my own ideas to the table. That relationship spanned roughly three years and two different representatives on FCI’s side, carrying the working relationship and platform context across the handoff when the original contact moved on.
Scope
Backend
- Real-time claim ownership & locking
- WebSocket infrastructure (Pusher → Soketi migration)
- Scheduled reconciliation jobs (locking, org sync, payments)
- XML data sync & dealership hierarchy modeling
- Virtual credit card payment pipeline (USBank integration)
- Dual-authorization approval logic
Frontend
- Real-time lock state via Vuex-backed mixins
- Live claim, dealership, and org-structure table updates
- Row-level and claim-level payment authorization checks
Infrastructure
- MySQL, Redis
- Self-hosted Soketi WebSocket server
- USBank card-issuing API (OAuth2 client credentials, mutual TLS)