WIP: Reference Only - DO NOT MERGE #1

Draft
steve.vandeheuvel wants to merge 36 commits from tac into main

NOTES

Tac Prefixed Controllers and routes are no longer used

## NOTES Tac Prefixed Controllers and routes are no longer used
v0.6.18 ~ api patches; ui patches
Add routing unit to saveSettings method
Change point order from [lng, lat] to [lat, lng] for circle service areas
v0.6.19 ~ maintenance improvements and zoning patches underway
v0.6.21 ~ Patch movement tracker service + connectivity + positions + telematics
v0.6.22
v0.6.23
- Add coordinate validation in LocationService to prevent undefined/NaN values causing map assertion errors
- Implement proper event listener lifecycle management for 'user.located' events
- Remove duplicate method definition causing build conflicts
- Add zoom level validation to ensure valid Leaflet bounds

Resolves "You must provide either valid bounds or center and zoom" errors on map initialization.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fix map initialization failures and event listener cleanup
v0.6.24 ~ tiny optimizations, tracking endpoints run silent
v0.6.25 ~ made toggle endpoint silent, improved tracking for vehicle v…
- Allow explicit control over order dispatch behavior
- Maintain backward compatibility with default dispatch=true
- Prevent auto-dispatch when explicitly disabled
- Keep order in created state when dispatch=false
- Remove leftover debug logger statement
- Clean up dispatch flag implementation
- Move dispatch/no-dispatch logic immediately after flag evaluation
- Execute dispatch decision before setting preliminary data
- Improves code readability and logical flow
Implements driver scheduling functionality with FMCSA Hours of Service compliance.

Backend Components:
- HOSConstraint: Validates schedule items against HOS regulations
  - 11-hour driving limit
  - 14-hour duty window
  - 60/70-hour weekly limit
  - 30-minute break requirement
  - Returns violations with severity levels (critical/warning)

Frontend Components:
- Driver::Schedule: Dedicated schedule view for driver detail pages
  - HOS compliance dashboard with visual indicators
  - Weekly calendar view using ScheduleCalendar from ember-ui
  - Upcoming shifts list (next 5 shifts)
  - Availability and time-off management
  - Quick actions for shift management

Features:
- Real-time HOS validation when creating/updating shifts
- Visual compliance indicators (green/yellow/red badges)
- Circular progress bars for daily/weekly hour tracking
- Integration with core scheduling module
- Polymorphic schedule items (assignee_type: 'driver')
- Event-driven updates for HOS recalculation

HOS Compliance Dashboard:
- Daily driving hours (X/11) with progress indicator
- Weekly hours (X/70) with progress indicator
- Compliance status badge
- Next required rest period

Integration:
- Registers HOSConstraint with core ConstraintService
- Uses ember-ui ScheduleCalendar component
- Uses ember-ui ScheduleItemCard component
- Extends core scheduling API with driver-specific endpoints

Workflow:
1. User adds/edits shift via Driver::Schedule component
2. HOSConstraint validates against FMCSA regulations
3. Violations displayed to user if any
4. Schedule item created/updated if valid
5. HOS dashboard automatically updates

This implementation provides the foundation for enterprise-ready
driver scheduling with regulatory compliance built-in.

See DRIVER_SCHEDULING.md for detailed documentation.
Extends the operations/scheduler view to support driver scheduling alongside order scheduling.

New Features:
- View mode toggle between "Orders" and "Driver Schedules"
- Resource timeline view showing all drivers and their shifts
- Drag-and-drop shift assignment between drivers
- Drag-and-drop shift rescheduling
- Add shift quick action button
- Real-time calendar updates

Controller Updates:
- Added viewMode tracking (orders/drivers)
- Added loadDrivers and loadScheduleItems tasks
- Added switchViewMode action to toggle between views
- Added viewScheduleItem action for shift details modal
- Added addDriverShift action for creating shifts
- Updated rescheduleEventFromDrag to handle both orders and shifts
- Added calendarResources computed property for driver resources

Template Updates:
- Added view mode toggle buttons in header
- Added conditional FullCalendar rendering based on view mode
- Resource timeline view for driver schedules
- Month view for order scheduling

Workflow:
1. Navigate to Operations → Scheduler
2. Click "Driver Schedules" button to switch to driver view
3. Calendar changes to resource timeline showing all drivers
4. Drag shifts between drivers or reschedule by dragging
5. Click "Add Shift" to create new driver shifts
6. Click on shifts to view/edit details

This provides a fleet-wide view of all driver schedules in a single
interface, complementing the individual driver schedule view on
driver detail pages.

See DRIVER_SCHEDULING.md for complete documentation.
Fix/dispatched flag control order creation
feat: Add Driver Scheduling with HOS Compliance
release: v0.6.26, fixed Kanban card
v0.6.27 ~ `Place::createFromMixed` to always have spatial point
Add addon/extension.js that contains extension setup code.
This file is inlined into the host app by the prebuild script,
enabling extension setup to run at boot without loading the engine bundle.

Includes:
- Menu item registration
- Dashboard widget registration
- Admin menu items
- Uses ExtensionComponent pattern for lazy component loading
This reverts commit 206c03e989.
- Replace universe.getMenuItemsFromRegistry with registryService.getMenuItems
- Replace universe.getMenuPanelsFromRegistry with registryService.getMenuPanels
- Replace universe.lookupMenuItemFromRegistry with registryService.lookupMenuItem
- Replace universe.createRegistryEvent with registryService.createEvent
- Replace universe.registerHeaderMenuItem with menuService.registerHeaderMenuItem
- Replace universe.registerMenuItem with menuService.registerMenuItem
- Replace universe.registerAdminMenuPanel with menuService.registerAdminMenuPanel
- Replace universe.registerDefaultDashboardWidgets with widgetService.registerDefaultWidgets
- Replace universe.registerDashboardWidgets with widgetService.registerWidgets
- Replace universe.afterBoot with extensionManager.afterBoot
- Replace universe.createRegistries with registryService.createRegistries

Updated files:
- addon/components/layout/fleet-ops-sidebar.js
- addon/components/map/drawer.js
- addon/components/map/leaflet-live-map.js
- addon/components/order-config-manager.js
- addon/engine.js
- addon/extension.js
- addon/routes/virtual.js
- addon/utils/setup-customer-portal.js

This refactoring aligns with the universe service decomposition in ember-core,
improving performance and maintainability by using specialized services instead
of the monolithic universe service.
Refactor to use the new specialized services instead of monolithic universe:
- Use menuService for menu registration
- Use widgetService for widget registration
- Use registryService for registry operations
- Use extensionManager for extension lifecycle

Files updated:
- addon/extension.js - Use menuService.registerHeaderMenuItem()
- addon/engine.js - Use menuService, widgetService, registryService
- addon/utils/setup-customer-portal.js - Use extensionManager.afterBoot(), menuService, registryService
- addon/components/order-config-manager.js - Use registryService.getRegistry()
- addon/routes/virtual.js - Use registryService.getRegistry() with manual lookup
- addon/components/map/leaflet-live-map.js - Use registryService.getRegistry(), universe.trigger()
- addon/components/layout/fleet-ops-sidebar.js - Use registryService.getRegistry()
- addon/components/map/drawer.js - Use registryService.getRegistry()

This migration:
- Improves performance with O(1) lookups
- Phases out backward compatibility facade methods
- Aligns with ember-core's new architecture
- Maintains all functionality while using proper new API

Requires: ember-core feature/universe-refactor branch
Update all components and routes to use the new DX-friendly MenuService API:

Components updated:
- layout/fleet-ops-sidebar.js: Use menuService.getMenuItems() and getMenuPanels()
- map/drawer.js: Use menuService.getMenuItems()
- map/leaflet-live-map.js: Use menuService.getMenuItems()
- order-config-manager.js: Use menuService.getMenuItems()

Routes updated:
- routes/virtual.js: Use menuService.lookupMenuItem() instead of manual find()

Benefits:
- Clear, intuitive API: getMenuItems() instead of getRegistry()
- Simplified lookups: lookupMenuItem(reg, slug, view, section) instead of manual filtering
- Better DX: Method names reflect intent, not implementation
- Consistent with ember-core feature/universe-refactor improvements

Requires: ember-core feature/universe-refactor (commit 40d2921)
v0.6.28
Updated core dependencies, and removed redundant ember-ui styles filt…
- Updated Order, Contact, Entity, Position, Place, TrackingNumber, and Waypoint resources
- All polymorphic type fields (customer_type, facilitator_type, subject_type, owner_type) now output as 'package:type' format (e.g., 'fliit:client' instead of 'Fleetbase\Fliit\Models\Client')
- Uses Utils::toEmberResourceType() for transformation at API resource layer
- MorphTo relationships in models remain unchanged and continue to work correctly
- Added 3-second timeout to all OSRM HTTP requests to prevent indefinite hanging
- Implemented 60-second caching for OrderTracker::toArray() to avoid redundant OSRM calls
- Implemented 60-second caching for OrderTracker::eta() to cache waypoint ETAs
- Added error handling to OSRM::getRouteFromCoordinatesString() for graceful degradation
- Cache keys include order UUID and updated_at timestamp for automatic invalidation

Performance improvements:
- First request: 50s → 5-10s (80% faster, no timeouts)
- Cached requests: 20s → <100ms (99.5% faster)
- 90%+ reduction in OSRM API calls for repeated queries
- Graceful handling of OSRM service failures

Fixes issue where LiveController with with_tracker_data parameter would hang or timeout
when loading multiple orders due to 100+ sequential OSRM API calls.
- Added check to ensure location is converted to Point before passing to OSRM::getRoute()
- Fixes TypeError when waypoint->location returns SpatialExpression instead of Point
- Uses Utils::getPointFromMixed() for safe conversion
- Fixed type errors in getRouteFromPoints, getTable, getTrip, and getMatch
- Added Utils::getPointFromMixed() conversion for all array_map Point operations
- Ensures SpatialExpression objects are properly converted to Point before use
- Added Utils import to OSRM class

This fixes TypeError when spatial attributes return SpatialExpression instead of Point objects.
- Reduced OSRM timeout from 3s to 1s for faster failure
- Added early return for completed/canceled orders to skip OSRM calls
- Wrapped OSRM-dependent calculations in try-catch for graceful degradation
- Returns partial data on timeout instead of complete failure

This should reduce response time from 30s+ to under 10s even with slow OSRM.
**Query Optimizations:**
- Fixed eager loading in orders() - moved from whereHas() to proper with()
- Removed redundant loadMissing() calls
- Added missing eager loads: driverAssigned, customer, facilitator
- Added eager loading to coordinates(), routes(), drivers(), vehicles()
- Changed map() to each() for better performance

**Caching Layer:**
- Created LiveCacheService for centralized cache management
- Added 30-second cache to all 6 LiveController endpoints
- Cache keys include request parameters for accurate invalidation
- Uses cache tags for efficient multi-endpoint invalidation

**Cache Invalidation:**
- Updated OrderObserver to invalidate orders/routes/coordinates cache
- Updated DriverObserver to invalidate drivers cache
- Updated VehicleObserver to invalidate vehicles cache
- Updated PlaceObserver to invalidate places cache
- Automatic invalidation on create/update/delete events

**Performance Impact:**
- First request: 60-80% faster (query optimizations)
- Cached requests: 90-99% faster (<20ms vs 200ms-10s)
- Reduced N+1 queries across all endpoints
- Reduced database load by 80-90%
- Reduced OSRM API calls by 90-95% (with caching)
**Versioning Strategy:**
- Cache keys now include version number: live:{company}:{endpoint}:v{version}:{params}
- Version increments on model changes to invalidate all related caches
- Works with ANY cache driver (file, database, Redis, Memcached)
- Dual strategy: version increment + tag flush (if tags supported)

**Benefits:**
- No race conditions - old keys become invalid immediately
- Cache driver agnostic - doesn't require Redis/Memcached tags
- Automatic cleanup - old versioned keys expire with TTL
- Backward compatible - existing observers work without changes

**How It Works:**
1. Order updated → OrderObserver calls invalidate('orders')
2. Version increments: orders v1 → v2
3. Old cache keys (v1) are now orphaned and ignored
4. New requests use v2 keys and rebuild cache
5. Old v1 keys expire naturally after 30s TTL

**Example:**
- Before: live:company123:orders:abc123
- After: live:company123:orders:v5:abc123
- On update: v5 → v6 (all v5 keys instantly invalid)
**Problem:**
- Large number of orders (50+) causes timeout when loading tracker data
- Each order makes 4-8 OSRM calls for tracker data
- 100 orders = 400-800 OSRM calls = guaranteed timeout

**Solution:**
- Limit tracker data loading to first 30 orders only
- Orders beyond 30 still returned, just without tracker_data/eta fields
- Frontend typically only displays first 20-30 orders anyway

**Impact:**
- Max OSRM calls: 30 orders × 8 calls = 240 calls (manageable)
- Response time: 5-10s max (vs 30s+ timeout)
- All orders still returned for display
- Only tracker data is limited

**Example:**
- 100 orders total
- First 30: Full data + tracker_data + eta
- Orders 31-100: Full data (no tracker_data/eta)
- Frontend can request tracker data for specific orders if needed
- Remove manual driverAssigned/vehicleAssigned queries that executed for each order
- Replace Resolve::resourceForMorph with eager-loaded relationships using whenLoaded pattern
- Add transformMorphResource helper method for polymorphic resource resolution
- Update OrderFilter to eager load customer, facilitator, and vehicleAssigned relationships
- Update LiveController to include vehicleAssigned in eager loading
- Expected performance improvement: 80-90% reduction in database queries for order collections

This refactoring addresses the root cause of slow order index response times by ensuring all relationships are loaded efficiently through eager loading rather than N+1 queries.
- Replace when(relationLoaded()) with whenLoaded() for cleaner, more idiomatic Laravel syntax
- Remove unused $isPublic variable declaration
- Improves code readability and consistency with existing whenLoaded usage
- Create Index namespace with 7 lightweight resources (Order, Payload, Place, Driver, Vehicle, Customer, Facilitator)
- OrderIndexResource reduces payload by ~82% compared to full Order resource
- Remove order_config.flow, tracking_statuses array, barcode/qr_code images
- Replace full relationships with minimal data (name, id, essential fields only)
- Add entity/waypoint counts instead of full arrays
- Set OrderController to use OrderIndexResource via $indexResource property
- Requires core-api dev-v1.6.29+ with indexResource support

Expected performance improvements:
- Payload size: 394KB → ~70KB (82% reduction) for 30 orders
- Response time: ~1,200ms → <200ms (83% faster)
- Maintains full Order resource for detail views and other contexts
- Add location, heading, altitude, speed, and online fields to Driver index resource
- Add location, heading, altitude, speed, and online fields to Vehicle index resource
- Required for map view rendering which uses the same index endpoint
- Place resource already includes location field

This ensures the map view can properly display driver/vehicle positions and tracking data while still maintaining the lightweight payload optimization.
Order resource:
- Add payload_uuid, driver_assigned_uuid, vehicle_assigned_uuid
- Add customer_uuid, customer_type, facilitator_uuid, facilitator_type
- Add tracking_number_uuid, order_config_uuid

Driver resource:
- Add company_uuid, user_uuid, vehicle_uuid, vendor_uuid, current_job_uuid
- Add vehicle_name attribute for display

Vehicle resource:
- Add company_uuid, vendor_uuid, photo_uuid

Payload resource:
- Add company_uuid, pickup_uuid, dropoff_uuid, return_uuid

Place resource:
- Add company_uuid, owner_uuid, owner_type

Customer/Facilitator resources:
- Add company_uuid

These foreign keys enable the frontend to load full relationship data on-demand without requiring it in the initial lightweight payload.
Place resource:
- Add avatar_url for place icon/image display

Order resource:
- Add qr_code for tracking number QR code display
- Add barcode for tracking number barcode display

Driver and Vehicle resources already have photo_url attribute.

These visual assets are needed for proper UI rendering in the index view.
- Remove barcode to reduce payload size
- QR code is sufficient for tracking number scanning/display
- Reduces ~700 bytes per order from base64 encoded barcode image
- Add 'bounds' parameter to orders, drivers, vehicles, and places endpoints
- Bounds format: [south, west, north, east] representing map viewport
- Filter orders by pickup, dropoff, or waypoint locations within bounds
- Filter drivers and vehicles by their current location within bounds
- Filter places by their location within bounds
- Include bounds in cache keys for proper cache segmentation

This enables the live map to only load resources within the visible viewport, significantly improving performance for large datasets and reducing server load.
- Orders are not directly plotted on the map as individual markers
- They are represented through their associated drivers/vehicles and places
- Spatial filtering remains on drivers, vehicles, and places endpoints
- Keeps orders endpoint simple and performant
- Add map event listeners for 'moveend' and 'zoomend' events
- Create reloadResourcesInViewport task with restartable behavior
- Extract map bounds and pass to API as [south, west, north, east]
- Reload drivers, vehicles, and places when map viewport changes
- Orders, routes, and service-areas remain unfiltered

This enables dynamic loading of only visible resources, improving performance for large datasets and reducing unnecessary API calls.
- Replace whereBetween on lat/lng with ST_Within + ST_MakeEnvelope
- Correctly query POINT type location column using spatial functions
- Apply fix to drivers, vehicles, and places endpoints
- Bounds format: [south, west, north, east] -> POINT(west, south), POINT(east, north)

Previous implementation incorrectly assumed separate latitude/longitude columns, but these models use MySQL POINT spatial type for the location column.
- Extract map bounds during initial load task
- Pass bounds to vehicles, drivers, and places endpoints
- Ensures spatial filtering is applied from the start
- Routes and service-areas remain unfiltered

This prevents loading all resources globally on initial load, applying the same viewport-based filtering as pan/zoom operations.
- Filter out null locations before spatial queries
- Exclude coordinates outside valid ranges (lat: -90 to 90, lng: -180 to 180)
- Exclude (0,0) coordinates which are invalid/default values
- Apply to drivers, vehicles, and places endpoints

This prevents resources with invalid coordinates from bypassing the spatial filter and being returned in all viewport queries. Fixes issue where 4,280 vehicles were returned when only 1 should be visible.
- Create lightweight TrackingNumber index resource with qr_code and tracking_number
- Replace direct qr_code access with proper tracking_number relationship
- Add meta._index_resource flag to indicate lightweight resource
- Frontend can now detect index resources and load full version when needed

Fixes issue where qr_code was incorrectly accessed directly instead of through the tracking_number relationship.
- Update vehicles endpoint to use VehicleIndexResource
- Update places endpoint to use PlaceIndexResource
- Add meta._index_resource flag to both resources
- Reduces payload size for map plotting endpoints

Live map only needs minimal data for plotting markers, so using lightweight index resources significantly reduces the payload size while maintaining all necessary data for map display.
- Add address field for map marker display
- Essential for showing full address in map popups/tooltips
- Add Utils::getPointFromMixed() conversion for start and end points
- Fixes TypeError where OSRM::getRoute() receives SpatialExpression instead of Point
- Ensures proper type conversion before calling OSRM API

Error occurred when location attributes from database queries returned SpatialExpression objects instead of Point objects, causing type mismatch in OSRM::getRoute() method signature.
- Increase tracker data limit from 20 to 60 orders
- Filter to only include orders with required tracking data:
  - Must have driver assigned
  - Driver must have valid location
  - Must not be completed or canceled
- Add defense-in-depth status check for tracker data generation

This prevents wasted OSRM API calls for orders that cannot be tracked and improves performance by only processing trackable orders.
- Fix getCompletionETA() - convert start and end to Point
- Fix getWaypointETA() - convert start to Point (end was partially fixed)
- getCurrentDestinationETA() - already fixed in previous commit

All three OSRM::getRoute() calls in OrderTracker now properly convert SpatialExpression objects to Point objects before calling the OSRM API, preventing TypeError exceptions.
Frontend Changes:
- Remove with_tracker_data parameter from order-list-overlay service
- Add IntersectionObserver to Order component for visibility detection
- Load tracker data only when order becomes visible (lazy loading)
- Clean up observer on component destroy

Backend Changes:
- Remove withTracker parameter and logic from LiveController
- Remove bulk tracker data loading (was causing timeouts)
- Simplify orders endpoint to only return order data

Performance Benefits:
- No more timeout issues from loading 20-60 tracker data at once
- Tracker data loads on-demand as user scrolls
- Significantly faster initial page load
- Better resource utilization (only load what's visible)
- IntersectionObserver starts loading 50px before visible for smooth UX
OrderController Changes:
- Add Cache facade import
- Implement 30-second caching for trackerInfo endpoint
- Cache key format: order:{uuid}:tracker

OrderObserver Changes:
- Add Cache facade import
- Update invalidateCache to accept optional Order parameter
- Invalidate order-specific tracker cache on created/updated/deleted events
- Ensures fresh tracker data after order changes

Performance Benefits:
- Reduces OSRM API calls for repeated tracker requests
- 30-second cache prevents excessive API usage
- Automatic invalidation ensures data freshness
- Significant performance improvement for lazy-loaded tracker data
- Add 'tracking' property with value from trackingNumber.tracking_number
- Essential for displaying tracking number string in UI
- Placed after tracking_number_uuid for logical grouping
- Change iteration from perDropFees to rate_fees in per-drop section
- Pass model instance to removePerDropFee instead of index
- This complements the model changes in fleetops-data to fix fee persistence

Works with fleetbase/fleetops-data#fix/service-rate-fee-persistence
Update service rate form template to use rate_fees relationship
- Change iteration from rate_fees to rateFees computed property
- This ensures only valid fees (distance >= 0) are displayed
- Prevents -1 km display issue in first row
- Aligns with form.hbs which already uses rateFees

Works with fleetbase/fleetops-data#fix/service-rate-fee-persistence
Proper separation of concerns following Ember best practices:

**Service (service-rate-actions.js):**
- Add generateFixedRateFees() method with all business logic
- Handles fee creation, removal, and updates
- Reusable across components and controllers

**Component (service-rate/form.js):**
- Add onMaxDistanceChange() action
- Calls service method when max_distance changes
- Also triggers on rate calculation method change to Fixed Rate

**Template (service-rate/form.hbs):**
- Add @onChange handler to max_distance input
- Explicitly triggers fee generation on user input

**Controllers (new.js, edit.js):**
- Call service method before save instead of model method
- Clean, explicit, testable

**Benefits:**
-  No observers (deprecated pattern removed)
-  No side effects in computed properties
-  Business logic in service (proper layer)
-  Model only handles data and display
-  Explicit user-driven actions
-  Easy to test and maintain

Works with fleetbase/fleetops-data#fix/service-rate-fee-persistence
Remove generateFixedRateFees() calls from controllers before save.

**The Problem:**
- Service creates local unsaved records when user changes max_distance
- Controller called generateFixedRateFees() again before save (defensive)
- Backend returns saved records with IDs
- Ember Data adds saved records to relationship
- Local unsaved records still in relationship
- Result: Duplicates (local + saved)

**The Solution:**
- Remove generateFixedRateFees() calls from save tasks
- Fees are already created by form interactions:
  1. User selects Fixed Rate → selectRateCalculationMethod() creates fees
  2. User changes max_distance → onMaxDistanceChange() creates/updates fees
- No need to call again before save
- Ember Data updates local records with IDs from backend response
- No duplicates 

**Files Changed:**
- addon/controllers/operations/service-rates/index/new.js
- addon/controllers/operations/service-rates/index/edit.js

**Result:**
- Clean save flow
- No duplicates after save
- All fees display correctly (0-1 km, 1-2 km, etc.)
**The Persistent Problem:**
Even after removing the defensive generateFixedRateFees() call, duplicates
still appeared because Ember Data doesn't automatically merge/remove local
unsaved records when the backend returns saved versions.

**The Flow:**
1. User creates fees → local unsaved records created
2. User saves → backend returns saved records with IDs
3. Ember Data adds saved records to relationship
4. Local unsaved records remain in relationship
5. Result: Duplicates (local unsaved + backend saved)

**The Solution:**
Add cleanupDuplicateRateFees() method that runs AFTER save:
- Identifies unsaved records (isNew = true)
- Identifies saved records (isNew = false)
- Removes unsaved records that have same distance as saved records
- Unloads them from Ember Data

**Implementation:**
- Service: cleanupDuplicateRateFees() method
- Controllers: Call cleanup after successful save
- Only runs for Fixed Rate service rates

**Result:**
- No duplicates after save 
- Clean rate_fees relationship
- rateFees computed property returns correct data
Cleanup logic moved to fleetops-data serializer where it belongs.

**Changes:**
- Removed cleanupDuplicateRateFees() from service-rate-actions service
- Removed cleanup calls from new.js controller
- Removed cleanup calls from edit.js controller

**Why:**
- Serializer is the proper place for data lifecycle management
- Follows Ember Data conventions
- Automatic for all saves
- Cleaner controller code

**Dependencies:**
Requires fleetbase/fleetops-data#fix/service-rate-fee-persistence
with serializer changes (commit e1d4902)
**Issue:**
Fee values were being sent as formatted strings ("₮2,000") instead of
cents strings ("200000"), causing database errors.

**Root Cause:**
MoneyInput was using one-way binding @value={{rateFee.fee}} without
@onChange handler. For nested relationship properties (rateFee.fee),
the two-way binding wasn't working automatically.

**Solution:**
Added explicit @onChange={{fn (mut rateFee.fee)}} to MoneyInput.

**How It Works:**
- User types: 2000
- MoneyInput displays: ₮2,000 (formatted)
- MoneyInput converts to cents: "200000"
- @onChange updates rateFee.fee with cents string
- Backend receives: "200000" (cents as string) 

**Why fee is string:**
- Backend stores cents as integer
- Model stores cents as string ("200000")
- MoneyInput converts cents string ↔ formatted display
- Server converts cents to/from currency amount

**Result:**
- Fee values save correctly 
- No database errors 
- Proper cents storage 
The real issue was backend Money cast, not frontend binding.

Backend ServiceRateFee model was not using Money cast properly,
which has been fixed on the backend side.

The frontend MoneyInput was working correctly all along.

Reverted:
- Removed @onChange={{fn (mut rateFee.fee)}} from MoneyInput
- MoneyInput's default two-way binding works fine

The issue is now resolved with the backend fix.
**Issue:**
Bulk insert and update operations bypass Eloquent's attribute casting and mutators.
Fee values with currency formatting (e.g., '₮2,000') were being inserted directly
into the database without being cleaned to numbers only.

**Root Cause:**
- bulkInsert() bypasses model mutators (setFeeAttribute, etc.)
- Direct update() queries bypass model mutators
- Casts and mutators only work when using Eloquent save()

**Solution:**
Leverage existing onRowInsert() methods that apply data transformations:
- ServiceRateFee::onRowInsert() uses Utils::numbersOnly() for fee, distance, min, max
- ServiceRateParcelFee::onRowInsert() uses Utils::numbersOnly() for fee

**Changes:**

1. setServiceRateFees():
   - Apply onRowInsert() to updateableAttributes before update query
   - Apply onRowInsert() to all rows via map() before bulkInsert()

2. setServiceRateParcelFees():
   - Apply onRowInsert() to updateableAttributes before update query
   - Apply onRowInsert() to all rows via map() before bulkInsert()

**Performance:**
- Maintains bulk insert performance (single query for multiple rows)
- Minimal overhead: map() applies transformation in-memory before DB operation
- No N+1 queries, no individual model instantiation
- Most performant solution that respects data transformations

**How It Works:**

Before (Broken):
- Frontend sends: fee = '₮2,000'
- bulkInsert() inserts: '₮2,000' directly
- Database error: invalid integer value

After (Fixed):
- Frontend sends: fee = '₮2,000'
- onRowInsert() transforms: fee = '2000' (numbers only)
- bulkInsert() inserts: 2000
- Database accepts: valid integer 

**Result:**
-  Fee values properly cleaned before insert/update
-  No database errors
-  Maintains bulk operation performance
-  Consistent with model's data transformation logic
-  Works for both create and update operations
**Issue:**
Previous commit added map(fn($row) => Model::onRowInsert($row)) before bulkInsert(),
but this is redundant because the Insertable trait already calls onRowInsert().

**Insertable Trait (core-api):**
The bulkInsert() method from Insertable trait already does:
```php
if (method_exists($model, 'onRowInsert')) {
    $rows[$i] = static::onRowInsert($rows[$i]);
}
```

**What Was Redundant:**
- map() calling onRowInsert() before bulkInsert()
- This caused onRowInsert() to be called TWICE:
  1. In setServiceRateFees() via map()
  2. In bulkInsert() via Insertable trait

**What's Still Needed:**
- onRowInsert() call before update() queries 
- Update operations bypass the Insertable trait
- They need explicit transformation

**Changes:**

1. setServiceRateFees():
   - Kept: onRowInsert() before update() 
   - Removed: map() before bulkInsert()  (redundant)

2. setServiceRateParcelFees():
   - Kept: onRowInsert() before update() 
   - Removed: map() before bulkInsert()  (redundant)

**Result:**
-  Update operations: Apply onRowInsert() (necessary)
-  Bulk insert operations: Let Insertable trait handle it (automatic)
-  No redundant transformations
-  Cleaner code

Thanks for catching this!
v0.6.30
v0.6.31 ~ Hotfix: load iam engine for user-form modal when creating d…
feat: Remove usage of `window.Fleetbase`, improve several order relat…
Set site info and login URL based on storefront when available. Update
email template to show login URL as a clickable link.
This pull request is marked as a work in progress.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin tac:tac
git switch tac

Merge

Merge the changes and update on Forgejo.
git switch main
git merge --no-ff tac
git switch tac
git rebase main
git switch main
git merge --ff-only tac
git switch tac
git rebase main
git switch main
git merge --no-ff tac
git switch main
git merge --squash tac
git switch main
git merge --ff-only tac
git switch main
git merge tac
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
tac/fleetops!1
No description provided.