Multiple Service Requests Per Customer — Design Document
Author
- Ashik Ikbal
Last Updated Date
[2026-07-10]
SRS References
- Service Request Management Module (current implementation:
VanTrackerService/Services/ServiceRequest/,VanTrackerService/DAL/ServiceRequest/) - Realtime Tracking & Notification Module (
VanTrackerService/Hubs/RealTimeHub.cs,VanTrackerService/Services/RealTimeService.cs)
Version History
| Version | Date | Changes | Author |
|---|---|---|---|
| 1.0 | 2026-07-10 | Initial draft — design for letting a customer have multiple service requests active at the same time, with independent per-request broadcasting | Ashik Ikbal |
1. Problem Statement
Today, a customer can only have one active service request at a time. The system hard-blocks creation of a second request while any request is Pending / Assigned / EnRoute / InProgress / Stopped, so at most one technician can ever be en route or servicing a given customer at once.
We want to support a customer having multiple service requests active at the same time — e.g. two vehicles, each with its own technician — where one request can be Servicing while another is at the same time EnRoute, and each is broadcast to the customer independently and correctly.
2. Current Behavior
CreateServiceRequest(ServiceRequestService.cs:250-263) blocks a new request if the customer has any active request, regardless of vehicle.- Because of this, downstream code rarely has to deal with a customer having 2+ live requests, though most of the realtime layer was actually already built generically (see §4).
- The customer-facing
GET /service-request/initial-dataREST endpoint returns a singleLocationBroadcastModelobject — it assumes one "live" request per customer.
3. Proposed Behavior
- Change the rule from "one active request per customer" to "one active request per vehicle." A customer with 2 vehicles can have both serviced at the same time by two different technicians. The same vehicle still cannot have two overlapping active requests.
GET /service-request/initial-datareturns a list for customers — one entry per live active request — instead of a single object. (For technicians it stays a single object, since one technician can only actively work one job at a time.)- Broadcasting, assignment, and notification logic need no structural change — they're already scoped per service request (see §4). We're removing the single artificial bottleneck that prevented this from being exercised, plus fixing the two spots that assumed "one live request."
4. What Already Works (no change needed)
Research into the codebase confirmed the realtime/broadcast layer was already designed to be multi-request-safe per customer:
- SignalR groups are per-request, not per-customer. Every status broadcast (
journey_started,service_started,technician_status_updated, etc.) goes to aRequest-{requestId}group and carriesRequestIdin its payload — two requests running at the same time already produce fully distinguishable events. JoinCustomerRequestGroupsAsync(RealTimeHub.cs:440-497) already queries all of a customer's active requests and joins their socket connection to everyRequest-{id}group.SendCustomerInitialDataAsync(RealTimeHub.cs:499-571) already loops over all the customer's active requests and sends the full list back over the socket — this one was already correct.- Push/SMS/email notifications already embed
RequestId/RequestNumberso the customer's app can tell which vehicle/request an alert belongs to. - Admin assignment (
AssignServiceRequestToTechnician,ServiceRequestService.cs:2257) only checks the target request's own status and the technician's schedule — it never looks at the customer's other requests. Two different technicians can already be assigned to two different requests for the same customer at overlapping times.
The only real gaps: the creation-time block, and the REST initial-data endpoint still assuming a single live request.
5. Changes Required
5.1 Replace customer-level block with vehicle-level block
- File:
ServiceRequestService.cs:250-263(CreateServiceRequest). - Add a new DAL method
GetActiveVehicleRequestsAsync(vehicleId, statusList)(modeled on the existingGetActiveCustomerRequestsAsync,ServiceRequestDal.cs:2040, but filtering onvehicleidinstead ofcustomerid— the column already exists onservicerequests). - Block creation only when the same vehicle already has an active request; different vehicles for the same customer are allowed to be active at the same time.
5.2 Make customer "initial data" multi-request-aware
- DAL:
GetLiveCustomerInitialDataRequestId(ServiceRequestDal.cs:2368,LIMIT 1) → becomesGetLiveCustomerInitialDataRequestIds, dropping theLIMIT 1, returning all matches. - Service:
GetCustomerInitialData(ServiceRequestService.cs:4980) builds aLocationBroadcastModelfor each returned request (reusing the existingBuildLocationBroadcastModelAsynchelper) and returns the full list. - Controller:
GET /service-request/initial-data(ServiceManagementController.cs:2012) response updated — customers get an array, technicians keep getting a single object. - Cleanup: unused
GetFirstActiveCustomerRequestAsync(no remaining call sites) can be removed.
5.3 Response Model Structure
This section spells out the exact shape of the data before and after the change, so both backend and mobile teams can align on the contract.
LocationBroadcastModel itself is unchanged — this is the existing model built by BuildLocationBroadcastModelAsync, and it's what each entry in the new list will look like:
| Field | Type | Description |
|---|---|---|
ServiceRequestInfo | ServiceRequestBasicInfo | RequestId, RequestNumber, CustomerId, TechnicianId, RequestStatus, Location, Latitude/Longitude, RequestedAt, StartedAt, IsReached, etc. |
TechnicianInfo | TechnicianBasicInfo | TechnicianId, TechnicianCode, AvailabilityStatus, FirstName, LastName, PhoneNumber, ProfileImageUrl |
CustomerInfo | CustomerBasicInfo | CustomerId, CustomerCode, FirstName, LastName, Email, PhoneNumber |
VanInfo | VanBasicInfo | VanId, VanNumber, RegistrationNumber, VIN |
TechnicianLocation | TechnicianLocation | Latitude, Longitude, Heading, Speed, Accuracy, UpdatedAt |
DistanceInMeter | int? | Remaining distance to customer (only when EnRoute) |
EtaInSec | int? | ETA in seconds (only when EnRoute) |
EncodedPolyline / PolylineVersion | string? / int? | Mapbox route polyline (only when EnRoute) |
CompletedJobs / QueuedJobs | int? | Technician's job counters for the day |
What actually changes is only the wrapper around it — one object → one array, for customers only.
Before (current REST response for a customer):
{
"serviceRequestInfo": { "requestId": "b1f7...", "requestNumber": "SR-1001", "requestStatus": "EnRoute", ... },
"technicianInfo": { "technicianId": "t1...", "firstName": "Mike", ... },
"customerInfo": { "customerId": "c1...", "firstName": "Jane", ... },
"vanInfo": { "vanId": "v1...", "registrationNumber": "DL-01-AB-1234" },
"technicianLocation": { "latitude": 28.61, "longitude": 77.20, "updatedAt": "2026-07-10T10:15:00Z" },
"distanceInMeter": 850,
"etaInSec": 180
}
After (proposed REST response for a customer with 2 concurrent requests):
[
{
"serviceRequestInfo": { "requestId": "b1f7...", "requestNumber": "SR-1001", "requestStatus": "InProgress", ... },
"technicianInfo": { "technicianId": "tA...", "firstName": "Mike", ... },
"customerInfo": { "customerId": "c1...", "firstName": "Jane", ... },
"vanInfo": { "vanId": "vA...", "registrationNumber": "DL-01-AB-1234" },
"technicianLocation": null,
"distanceInMeter": null,
"etaInSec": null
},
{
"serviceRequestInfo": { "requestId": "d2e9...", "requestNumber": "SR-1002", "requestStatus": "EnRoute", ... },
"technicianInfo": { "technicianId": "tB...", "firstName": "Sara", ... },
"customerInfo": { "customerId": "c1...", "firstName": "Jane", ... },
"vanInfo": { "vanId": "vB...", "registrationNumber": "DL-02-CD-5678" },
"technicianLocation": { "latitude": 28.60, "longitude": 77.19, "updatedAt": "2026-07-10T10:16:00Z" },
"distanceInMeter": 1200,
"etaInSec": 240
}
]
Note technicianLocation/distanceInMeter/etaInSec are only populated for requests currently EnRoute (matching existing single-request behavior) — an InProgress/Servicing request doesn't need live location.
Method signature changes (C#):
| Layer | Before | After |
|---|---|---|
| DAL | Task<(int Status, Guid? RequestId, Guid? TechnicianId, string? Message)> GetLiveCustomerInitialDataRequestId(Guid customerId) | Task<(int Status, List<(Guid RequestId, Guid TechnicianId)> Requests, string? Message)> GetLiveCustomerInitialDataRequestIds(Guid customerId) |
| Service (customer branch) | Task<(int Status, LocationBroadcastModel? Data, string? Message)> GetCustomerInitialData(Guid customerId) | Task<(int Status, List<LocationBroadcastModel> Data, string? Message)> GetCustomerInitialData(Guid customerId) |
| Service (dispatcher) | Task<(int Status, LocationBroadcastModel? Data, string? Message)> GetInitialData(Guid userId) | Task<(int Status, object? Data, string? Message)> GetInitialData(Guid userId) — returns a single LocationBroadcastModel for technicians, List<LocationBroadcastModel> for customers |
| Controller | [SwaggerResponse(200, typeof(LocationBroadcastModel))] | Documented as returning LocationBroadcastModel (technician) or List<LocationBroadcastModel> (customer) |
New DAL method for the vehicle-level check:
Task<(int Status, List<Guid> RequestIds, string? Message)> GetActiveVehicleRequestsAsync(Guid vehicleId, List<RequestStatus> statusList);
Same shape as the existing GetActiveCustomerRequestsAsync, just filtered by vehicleid instead of customerid.
6. Example Workflow
Scenario: Customer "Jane" owns two vehicles — a Sedan and an SUV. Both need service on the same day.
| Step | Actor | Action | Resulting State |
|---|---|---|---|
| 1 | Customer (Jane) | Creates a service request for the Sedan | Sedan request → Pending |
| 2 | Customer (Jane) | Creates a second service request for the SUV | SUV request → Pending (allowed — different vehicle) |
| 3 | Admin | Assigns Technician A to the Sedan request | Sedan request → Assigned, Technician A notified |
| 4 | Admin | Assigns Technician B to the SUV request | SUV request → Assigned, Technician B notified (allowed even though it overlaps with Technician A's schedule — conflict checks are per-technician, not per-customer) |
| 5 | Technician A | Starts journey to Jane's location for the Sedan | Sedan request → EnRoute. Jane's app receives journey_started for the Sedan request only (payload includes Sedan's RequestId) |
| 6 | Technician B | Starts journey to Jane's location for the SUV | SUV request → EnRoute. Jane's app receives a separate journey_started event for the SUV request |
| 7 | Technician A | Arrives and starts work on the Sedan | Sedan request → InProgress, Technician A's availability → Servicing. Jane's app receives service_started for the Sedan request |
| 8 | Technician B | Still driving to Jane for the SUV | SUV request remains EnRoute, Technician B's availability → EnRoute |
| 9 | Jane's app | Calls GET /service-request/initial-data (or connects via SignalR) | Receives both requests: Sedan (InProgress, with Technician A's live location/servicing status) and SUV (EnRoute, with Technician B's live location/ETA) |
| 10 | Technician A | Completes the Sedan job | Sedan request → Completed. Jane's app receives service_completed for the Sedan request; the SUV request is unaffected and keeps broadcasting independently |
| 11 | Technician B | Arrives and starts work on the SUV | SUV request → InProgress. Jane's app receives service_started for the SUV request |
| 12 | Technician B | Completes the SUV job | SUV request → Completed. Both of Jane's requests are now done |
Throughout this flow, every socket event and push notification is tagged with its own RequestId, so Jane's app always knows which vehicle/technician a given update refers to — even while both jobs are in flight at once.
7. Verification Plan
- Create 2 requests for the same customer on 2 different vehicles — both should succeed. A 2nd request on the same vehicle while one is active should be blocked.
- Assign 2 different technicians to the 2 requests with overlapping schedules — both assignments should succeed.
- Drive both requests through
EnRoute→Servicing/InProgress→Completedindependently and confirm, via a connected SignalR client, that each status change broadcasts only for its ownRequest-{id}group with the correctRequestIdin the payload. - Call
GET /service-request/initial-dataas the customer while both requests are live — confirm the response is a JSON array with one correct entry per active request. - Confirm the admin dashboard (
AllAdminsgroup) shows both technicians' statuses independently. - Run/extend automated tests for
ServiceRequestService/ServiceRequestDalcovering the new vehicle-scoped check and the new plural DAL/service methods.
Review & Approval
Reviewer(s): Sanket
Approval Date: [To be completed after reviews]