Skip to main content
Version: RK Auto

Service Booking Management

Author(s)

  • Amarnath Garai

  • Sanket Mal

  • Ayan Ghosh

  • Ribhu Gautam

  • Faizal Khan

  • ...

Last Updated Date

[2026-02-20]


SRS References

  • 2.1.2
  • 2.1.3

Version History

VersionDateChangesAuthor
1.02026-01-13Initial draft (Booking Request)Amarnath Garai , Sanket Mal
1.12026-02-05Added Ring System SpecDevelopment Team
1.2TBDFCM integration for background notificationsTBD
1.32026-02-17Real Time events added, Models Updated, API addedFaizal Khan

Part 1: Service Booking & Request Lifecycle

Feature Overview

Objective The objective of this feature is to enable customers to book vehicle service requests and allow multiple technicians to receive, respond to, and be assigned to those requests in real time. The system ensures that only one technician can successfully accept a service request while maintaining a complete audit trail of all technician interactions, service execution, and billing.

This feature supports both on-demand and scheduled service workflows and enables location-based technician assignment for efficient dispatch.

Scope This feature covers the full lifecycle of a service request, including:

  • Service request creation by customers
  • Technician notification, acceptance, rejection, and cancellation
  • Technician assignment and tracking
  • Service execution and completion
  • Per-service pricing and billing

Requirements

Functional Requirements

  1. The system must allow customers to create a service request with vehicle, location, and service details.
  2. The system must support both ON_DEMAND and SCHEDULED service types.
  3. The system must allow customers to choose service location preference (Customer Location or Service Center).
  4. The system must notify multiple nearby technicians when a service request is created.
  5. The system must allow technicians to Accept, Reject, or Ignore a service request.
  6. The system must ensure that only one technician can successfully accept a service request.
  7. When a technician accepts a request, all other technicians must be blocked from accepting the same request.
  8. The system must record all technician actions (Notified, Accepted, Rejected, Canceled) for each request.
  9. The system must assign the accepting technician as the current technician for the service request.
  10. The system must allow the assigned technician to cancel the request if needed.
  11. The system must track the service status (Pending, In-Progress, Completed).
  12. The system must allow multiple services to be attached to a single service request.
  13. The system must track per-service status (Pending, Completed, Rejected).
  14. The system must calculate the total price of a request based on completed services.
  15. The system must allow customers to provide ratings and feedback after completion.

Non-Functional Requirements

  1. The system must handle concurrent technician acceptance attempts without data conflicts.
  2. The system must ensure data integrity through foreign keys and transactional updates.
  3. The system must maintain a complete audit trail of technician actions.
  4. The system must support location-based searching for technicians using latitude and longitude.
  5. The system must be optimized for high-volume real-time requests.
  6. The system must be resilient to partial failures (e.g., technician app crashes during acceptance).
  7. The system must support future scalability for thousands of technicians and service requests.

Design Specifications

SQL Data Models

CREATE TABLE IF NOT EXISTS servicerequests (
requestid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
requestnumber VARCHAR(50),
customerid UUID NOT NULL,
vehicleid UUID NOT NULL,
vanid UUID,
currenttechnicianid UUID,

servicescheduletype VARCHAR(20) NOT NULL, -- ('OnDemand', 'Scheduled')
locationpreference VARCHAR(30) NOT NULL, -- ('CustomerLocation', 'ServiceCenter')

estimatedprice NUMERIC(10,2),
paymentstatus VARCHAR(20),
paymentmethod VARCHAR(20),
description TEXT,

rating INT,
feedback TEXT,

scheduledat TIMESTAMP,
startedat TIMESTAMP NULL,
completedat TIMESTAMP NULL,

requeststatus VARCHAR(20) NOT NULL, -- ('Pending', 'Accepted', 'InProgress', 'Completed', 'Canceled', 'Ringing')

latitude NUMERIC(9,6),
longitude NUMERIC(9,6),
location TEXT,

requestedat TIMESTAMP NOT NULL DEFAULT NOW(),
updatedat TIMESTAMP NOT NULL DEFAULT NOW(),

-- Ring System Columns
ringstarttime TIMESTAMP,
ringendtime TIMESTAMP,
maxringradius INT DEFAULT 80,

CONSTRAINT fk_request_customer
FOREIGN KEY (customerid)
REFERENCES customermaster(customerid)
ON DELETE CASCADE,

CONSTRAINT fk_request_vehicle
FOREIGN KEY (vehicleid)
REFERENCES customervehicles(customervehicleid)
ON DELETE CASCADE,

CONSTRAINT fk_request_van
FOREIGN KEY (vanid)
REFERENCES vanmaster(vanid)
ON DELETE SET NULL,

CONSTRAINT fk_request_technician
FOREIGN KEY (currenttechnicianid)
REFERENCES technicianmaster(technicianid)
ON DELETE SET NULL
);

CREATE TABLE IF NOT EXISTS requestedservices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

requestid UUID NOT NULL,
serviceid UUID NOT NULL,

serviceprice NUMERIC(10,2),

servicestatus VARCHAR(20) NOT NULL, -- ('Pending', 'Completed', 'Rejected')
reasonofrejection TEXT,

CONSTRAINT fk_requested_request
FOREIGN KEY (requestid)
REFERENCES servicerequests(requestid)
ON DELETE CASCADE,

CONSTRAINT fk_requested_service
FOREIGN KEY (serviceid)
REFERENCES servicemaster(serviceid),

CONSTRAINT uq_request_service UNIQUE (requestid, serviceid)
);


CREATE TABLE IF NOT EXISTS technicianengagementdetails (
engagementid UUID PRIMARY KEY DEFAULT gen_random_uuid(),

requestid UUID NOT NULL,
technicianid UUID NOT NULL,

action VARCHAR(20) NOT NULL, -- ('NOTIFIED', 'ACCEPTED', 'REJECTED', 'CANCELED')
time TIMESTAMP NOT NULL DEFAULT NOW(),

CONSTRAINT fk_engagement_request
FOREIGN KEY (requestid)
REFERENCES servicerequests(requestid)
ON DELETE CASCADE,

CONSTRAINT fk_engagement_technician
FOREIGN KEY (technicianid)
REFERENCES technicianmaster(technicianid)
ON DELETE CASCADE
);

-- Ring event log table
CREATE TABLE technicianringlog (
ringlogid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
requestid UUID NOT NULL REFERENCES servicerequests(requestid),
technicianid UUID NOT NULL REFERENCES technicianmaster(technicianid),
ringminradius INT NOT NULL, -- Minimum radius of ring phase
ringmaxradius INT NOT NULL, -- Maximum radius of ring phase
ringstartedat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ringendedat TIMESTAMP,
createdat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(requestid, technicianid)
);

-- INDEXS
CREATE INDEX idx_servicerequests_status ON servicerequests(requeststatus);
CREATE INDEX idx_servicerequests_location ON servicerequests(latitude, longitude);
CREATE INDEX idx_servicerequests_customer_requestedat ON servicerequests(customerid, requestedat DESC);

CREATE INDEX idx_requestedservices_request ON requestedservices(requestid);
CREATE INDEX idx_requestedservices_service ON requestedservices(serviceid);

CREATE INDEX idx_technicianengagement_request ON technicianengagementdetails(requestid);
CREATE INDEX idx_technicianengagement_technician ON technicianengagementdetails(technicianid);

CREATE INDEX idx_ringlog_request ON technicianringlog(requestid, ringstartedat DESC);
CREATE INDEX idx_ringlog_technician ON technicianringlog(technicianid, ringstartedat DESC);

Enums

public enum ServiceScheduleType
{
OnDemand = 1,
Scheduled
}

public enum LocationPreference
{
CustomerLocation = 1,
ServiceCenter
}

public enum RequestStatus
{
Pending = 1,
Assigned,
EnRoute,
InProgress,
Stopped,
Completed,
Canceled,
Accepted,
Ringing
}

public enum ServiceAvailableIn
{
Van = 1,
ServiceCenter,
Both
}

public enum ServiceStatus
{
Pending = 1,
Completed,
Rejected
}

public enum TechnicianEngagementAction
{
Accepted = 1,
Rejected,
Canceled
}

public enum ServiceCategory
{
Maintenance = 1, // Oil change, filter replacement, routine service
Repair = 2, // Brake repair, engine repair, parts replacement
Inspection = 3, // Safety inspection, pre-purchase inspection
Diagnostic = 4, // Electrical diagnostic, engine diagnostic
Detailing = 5, // Cleaning, polishing, interior detailing
Customization = 6, // Modifications, upgrades, accessories
Emergency = 7 // Roadside assistance, towing, urgent repairs
}

public enum PaymentStatus
{
Pending = 1,
Paid,
PartiallyPaid,
Refunded,
PartiallyRefunded,
Failed
}

public enum TechnicianWorkAction
{
Start = 1,
Complete
}

public enum TechnicianAvailabilityStatus
{
Available = 1, // Roaming around city, available for requests
EnRoute = 2, // Driving to customer location
Servicing = 3, // Actively working on a car
Offline = 4, // Technician off-duty / logged out
OnBreak = 5 // Technician on lunch/break
}

public enum RouteCalculationStrategy
{
LocalCalculation,
FetchFromMapbox
}

C# Models

public record CreateServiceRequest
{
// Required
public Guid VehicleId { get; init; }
public ServiceScheduleType ServiceScheduleType { get; init; } = ServiceScheduleType.OnDemand; // OnDemand / Scheduled
public LocationPreference LocationPreference { get; init; } = LocationPreference.CustomerLocation; // CustomerLocation / ServiceCenter , Also OnDemand is Always CustomerLocation

// Only required when ServiceScheduleType == Scheduled
public DateTime? ScheduledAt { get; init; }

// Location
public required decimal Latitude { get; init; }
public required decimal Longitude { get; init; }
public string? Location { get; init; }

// Description
public string? Description { get; init; }
// List of service IDs (from servicemaster) that the customer is requesting
public required List<Guid> Services { get; init; }

// System controlled — NOT sent by client
[JsonIgnore]
public Guid CustomerId { get; init; }
[JsonIgnore]
public decimal? EstimatedPrice { get; init; }
[JsonIgnore]
public RequestStatus RequestStatus { get; init; } = RequestStatus.Pending;
}

public record ServiceRequestDetailedResponse
{
// --- Request Core ---
public Guid RequestId { get; init; }
public Guid CustomerId { get; init; }
public Guid VehicleId { get; init; }
public required string RequestNumber { get; set; }
public ServiceScheduleType ServiceScheduleType { get; init; } // OnDemand / Scheduled
public LocationPreference LocationPreference { get; init; } // CustomerLocation / ServiceCenter
public RequestStatus RequestStatus { get; init; } // Pending / Accepted / InProgress / Completed / Canceled
public DateTime RequestedAt { get; init; }
public DateTime? ScheduledAt { get; init; }
public DateTime? StartedAt { get; init; }
public DateTime? StoppedAt { get; init; }
public DateTime? CompletedAt { get; init; }
public TimeSpan? Duration { get; init; }

public DateTime? CancelledAt { get; init; }
public UserBasicInfo? CancelledBy { get; init; }
public string? CancellationReason { get; init; }

// --- Location ---
public required decimal Latitude { get; init; }
public required decimal Longitude { get; init; }
public string? Location { get; init; }

// --- Description ---
public string? Description { get; init; }

// --- Payment ---
public decimal? EstimatedPrice { get; init; }
public PaymentStatus PaymentStatus { get; init; }
public string? PaymentMethod { get; init; }
public string? TransactionNo { get; init; }
public decimal? Subtotal { get; init; }
public decimal? PaidAmount { get; init; }
public DateTime? PaymentCompletedAt { get; init; }
/// <summary>
/// Nullable — populated only when a completed service request has an invoice.
/// Retrieved via LEFT JOIN on invoices table using servicerequestid.
/// </summary>
public Guid? InvoiceId { get; init; }
public string? InvoiceNumber { get; init; }

// --- Feedback ---
public int? Rating { get; init; }
public string? Feedback { get; init; }

// --- Completion Details ---
public int? Mileage { get; init; }
public List<string>? Evidences { get; init; }

// --- Nested objects ---
public required CustomerBasicInfo Customer { get; init; }

public required VehicleBasicInfo Vehicle { get; init; }

public VanInfo? Van { get; init; }
public TechnicianBasicInfo? Technician { get; init; }

public List<RequestedServiceInfo> Services { get; init; } = new();
}


public record RequestedServiceInfo
{
public Guid ServiceId { get; init; }
public required string ServiceCode { get; init; }
public required string ServiceName { get; init; }
public string? ServiceDescription { get; init; }
public decimal? ServicePrice { get; init; }
public ServiceStatus ServiceStatus { get; init; } // Pending / Completed / Rejected
public string? ReasonOfRejection { get; init; }
}

public record GetServiceRequestsFilter
{
// Status filters
public RequestStatus? RequestStatus { get; init; } // Pending / InProgress / Completed / Canceled
public PaymentStatus? PaymentStatus { get; init; } // Pending / Paid
public TechnicianEngagementAction? Action { get; init; }

// Type filters
public ServiceScheduleType? ServiceScheduleType { get; init; } // OnDemand / Scheduled
public LocationPreference? LocationPreference { get; init; } // CustomerLocation / ServiceCenter

// Date filters
public DateTime? FromDate { get; init; }
public DateTime? ToDate { get; init; }

// Paging
public int RowsPerPage { get; init; } = 10;
public int PageNumber { get; init; } = 1;
}

public record GetServicesFilter
{
public ServiceAvailableIn? ServiceAvailableIn { get; init; }
public string? SearchKeyword { get; init; }
public ServiceCategory? ServiceCategory { get; init; }

// Paging
public int RowsPerPage { get; init; } = 10;
public int PageNumber { get; init; } = 1;
}

public record CustomerBasicInfo
{
public Guid UserId { get; init; }

// Customer identification
public Guid CustomerId { get; init; }
public string CustomerCode { get; init; } = string.Empty;
// User information
public string Email { get; init; } = string.Empty;
public string FirstName { get; init; } = string.Empty;
public string LastName { get; init; } = string.Empty;
public string PhoneNumber { get; init; } = string.Empty;
public string? ProfileImageUrl { get; set; }
public Gender? Gender { get; init; }
}

public record VehicleBasicInfo
{
public required string Vin { get; init; }
public string? Make { get; init; }
public string? Model { get; init; }
public int? Year { get; init; }
public string? Trim { get; init; }
public string? VehicleType { get; init; } // Passenger Car
public string? BodyType { get; init; } // Coupe, Sedan
}

public record TechnicianBasicInfo
{
public Guid? UserId { get; set; }
public Guid TechnicianId { get; init; }
public string TechnicianCode { get; init; } = string.Empty;
public TechnicianAvailabilityStatus? AvailabilityStatus { get; init; }
public string FirstName { get; init; } = string.Empty;
public string LastName { get; init; } = string.Empty;
public string? Email { get; init; }
public string? PhoneNumber { get; init; }
public string? ProfileImageUrl { get; set; }
}

public record VanInfo
{
public Guid VanId { get; init; }
public string VanNumber { get; init; } = string.Empty;
public string RegistrationNumber { get; init; } = string.Empty;
public string VIN { get; init; } = string.Empty;
public string Make { get; init; } = string.Empty;
public string Model { get; init; } = string.Empty;
public int Year { get; init; }
}

public record SubmitFeedback
{
public int Rating { get; init; } // 1-5 star rating
public string? Feedback { get; init; }
}

public record CreateRequestedServiceRequest
{
public Guid RequestId { get; init; }
public Guid ServiceId { get; init; }
public decimal ServicePrice { get; init; }
[JsonIgnore]
public ServiceStatus ServiceStatus { get; init; } = ServiceStatus.Pending;
}

public record TechnicianEngagementRequest
{
public Guid EngagementId { get; init; } = Guid.NewGuid();
public Guid RequestId { get; init; }
public Guid TechnicianId { get; init; }
public TechnicianEngagementAction Action { get; init; }
}

public record ServiceRequestActionRequest
{
public TechnicianEngagementAction Action { get; init; }
}

public record ServiceRequestBasicInfo
{
public Guid RequestId { get; init; }
public required string RequestNumber { get; set; }
public Guid CustomerId { get; init; }
public Guid? TechnicianId { get; init; }
public RequestStatus RequestStatus { get; init; }
public decimal Latitude { get; init; }
public decimal Longitude { get; init; }
public string? Location { get; init; }
public DateTime RequestedAt { get; init; }
}

public record CreateServiceRequestResponse
{
public Guid RequestId { get; init; }
public string RequestNumber { get; init; } = string.Empty;
}

public record LocationUpdateRequest
{
public decimal? Latitude { get; init; }
public decimal? Longitude { get; init; }
}

public record ServiceRequestRouteInformation
{
public CustomerCurrentLocation? CustomerLocation { get; init; }
public TechnicianCurrentLocation? TechnicianLocation { get; init; }
public MapboxRouteResult RouteInfo { get; init; } = new();
}

public record ServiceRequestSummary : ServiceRequestBasicInfo
{
public DateTime ScheduledAt { get; init; }
[JsonIgnore]
public DateTime UpdatedAt { get; init; }
public TimeSpan? EstimatedDuration { get; init; }
}

public record TechnicianSuggestionsResponse
{
public TechnicianBasicInfo TechnicianInfo { get; init; } = new();
public int Eta { get; init; }
public int JobCompletedToday { get; init; }
public int JobQueueToday { get; init; }
}

public record GetTechnicianSuggestionsFilter
{
public int PageNumber { get; init; } = 1;
public int RowsPerPage { get; init; } = 10;
public string? SearchKeyword { get; init; }
}

public record TechnicianSuggestionFilter
{
public string? SearchKeyword { get; init; } // Global search
public UserStatus Status { get; init; }
public bool HasVanAssigned { get; init; }
}

public record AssignServiceRequestModel
{
public Guid RequestId { get; init; }
public Guid TechnicianId { get; init; }
public DateTime ScheduledAt { get; init; }
}

public record RouteInfoWithCalculationStrategy
{
public double EtaSeconds { get; init; }
public double DistanceMeters { get; init; }
public string? Polyline { get; init; }
public string CalculationStrategy { get; init; } = string.Empty;
public DateTime? LastDirectionsCallAt { get; init; }
public DateTime? LastMapboxApiCallAt { get; init; }
public string? TechnicianLocation { get; init; }
public string? CustomerLocation { get; init; }
}

public record ResponseWithData<T> : CommonResponse
{
public T? Data { get; init; }
}

public record SubmitFeedback
{
public int Rating { get; init; } // 1-5 star rating
public string? Feedback { get; init; }
}


public record CompleteServiceRequest // when job start this request body is not required
{
public int? Mileage { get; init; } // mileage is required when job complete
public List<string>? Evidences { get; init; } // minimum one evidence is required when job complete
public List<Guid>? CompletedServices { get; init; } // minimum one completed service is required when job complete
public List<Guid>? AdditionalServices { get; init; } // optional
}


Request Lifecycle & State Transitions

The service request lifecycle involves interaction between RequestStatus (Service Request) and TechnicianAvailabilityStatus (Technician).

1. Creation & Assignment Phase

ActionRequest StatusTechnician StatusNotes
Customer Creates RequestPending (or Ringing)AvailableRequest enters the system. If On-Demand, Ring System may trigger Ringing.
Technician Accepts (Ring)RingingAssignedAvailableTechnician accepts the ring notification.
Admin Assigns (Manual)PendingAssignedAvailableAdmin manually assigns request to an available technician.

Note: When assigned, the technician remains Available until they explicitly start the journey. This allows them to finish a current job or task before moving.

2. Execution Phase

ActionRequest StatusTechnician StatusNotes
Start JourneyAssignedAvailableEnRouteTechnician indicates they are moving towards customer. Customer notified of "Journey Started".
Arrive at LocationAssignedEnRouteEnRouteTechnician arrives (detected via geofence or manual?). Status remains EnRoute.
Start Work (Action)AssignedInProgressEnRouteServicingTechnician begins the actual service work.
Complete Work (Action)InProgressCompletedServicingAvailableService finished. Technician becomes available for new rings/assignments.

3. Exception Flows

ActionRequest StatusTechnician StatusNotes
Technician Rejects (Ring)RingingPendingAvailableIf all techs reject/timeout, request goes to Pending.
Customer CancelsAny → CanceledReverts to AvailableIf assigned, technician is freed up.

API Endpoints

EndpointMethodParametersResponseResponse Status Codes
/service-requestPOSTCreateServiceRequestResponseWithData<CreateServiceRequestResponse>200, 204, 500
/service-request/{requestId}/{actionType}PUTrequestId, actionTypeCommonResponse200, 204, 500
/service-request/{requestId}?workAction={workAction}PUTCompleteServiceRequest, requestId, workActionCommonResponse200, 400, 401, 403, 404, 500
/service-request/feedback/{requestId}POSTSubmitFeedbackCommonResponse200, 400, 403, 500
/service-requestsGETGetServiceRequestsFilterServerPaginatedData<ServiceRequestDetailedResponse>200, 204, 500
/service-request/{requestId}GETrequestIdServiceRequestDetailedResponse200, 204, 500
/technician-suggestionsGETTechnicianSuggestionFilterList<TechnicianBasicInfo>200, 204, 500
/service-request/assignPOSTAssignServiceRequestModelCommonResponse200, 404, 500
/service-request/{requestId}/start-journeyPOSTrequestIdCommonResponse200, 500
/technician/current-requestGET-ServiceRequestDetailedResponse200, 404, 500
/service-request/{requestId}/route-infoGETrequestIdServiceRequestRouteInformation200, 403, 404, 500
/service-request/{requestId}/technician-suggestionsGETrequestId, filterServerPaginatedData<TechnicianSuggestionsResponse>200, 404, 500
/service-request/{technicianId}/scheduled-requestsGETtechnicianId, dateList<ServiceRequestSummary>200, 404, 500

1. Create Service Request

Endpoint: POST /service-request

Request Parameters:

{
"vehicleId": "550e8400-e29b-41d4-a716-446655440001",
"serviceScheduleType": "OnDemand",
"locationPreference": "CustomerLocation",
"scheduledAt": null,
"latitude": 28.6139,
"longitude": 77.2090,
"location": "Connaught Place, New Delhi",
"description": "Car making strange noise from engine",
"services": [
"550e8400-e29b-41d4-a716-446655440002",
"550e8400-e29b-41d4-a716-446655440003"
]
}

Response (Success - 200):

{
"status": 200,
"message": "Service request created successfully",
"data": {
"requestId": "550e8400-e29b-41d4-a716-446655440010",
"requestNumber": "REQ-2026-1001"
}
}

Response (Failure - 400):

{
"status": 400,
"message": "Invalid parameters: Location is required for OnDemand requests"
}

2. Handle Service Request Action

Endpoint: PUT /service-request/{requestId}/{actionType} ActionType: 1=Accept, 2=Reject, 3=Cancel

Request Parameters:

{
"requestId": "550e8400-e29b-41d4-a716-446655440010",
"actionType": 1,
"locationUpdate": {
"latitude": 28.6139,
"longitude": 77.2090
}
}

(Note: locationUpdate body is optional and only used for Accept action)

Response (Success - 200):

{
"status": 200,
"message": "Action completed successfully"
}

3. Start or Complete Work

Endpoint: PUT /service-request/{requestId}?workAction={workAction} WorkAction: 1=Start, 2=Complete

Note: Request body is only used when workAction=Complete. For workAction=Start, the request body is ignored.

Request Body (Complete action only - CompleteServiceRequest):

{
"mileage": 45,
"evidences": [
"https://example.com/photo1.jpg",
"https://example.com/photo2.jpg"
],
"completedServices": [
"550e8400-e29b-41d4-a716-446655440020",
"550e8400-e29b-41d4-a716-446655440021"
],
"additionalServices": [
"550e8400-e29b-41d4-a716-446655440022"
]
}

Response (Success - 200):

{
"status": 200,
"message": "Work started successfully"
}

4. Submit Feedback

Endpoint: POST /service-request/feedback/{requestId}

Request Body (SubmitFeedback):

{
"rating": 5,
"feedback": "Technician was very professional and quick."
}

Response (Success - 200):

{
"status": 200,
"message": "Feedback submitted successfully"
}

5. Get Service Requests (Filter/List)

Endpoint: GET /service-requests

Request Parameters:

{
"requestStatus": "Completed",
"paymentStatus": "Paid",
"action": null,
"serviceScheduleType": "OnDemand",
"locationPreference": null,
"fromDate": "2026-01-01T00:00:00Z",
"toDate": "2026-01-31T23:59:59Z",
"rowsPerPage": 10,
"pageNumber": 1
}

Response (Success - 200):

{
"data": [
{
"requestId": "550e8400-e29b-41d4-a716-446655440003",
"requestNumber": "REQ-2026-001",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"vehicleId": "550e8400-e29b-41d4-a716-446655440005",
"serviceScheduleType": "OnDemand",
"locationPreference": "CustomerLocation",
"requestStatus": "Completed",
"requestedAt": "2026-01-13T10:30:00Z",
"scheduledAt": null,
"startedAt": "2026-01-13T11:00:00Z",
"completedAt": "2026-01-13T12:15:00Z",
"latitude": 40.7128,
"longitude": -74.0060,
"location": "123 Main St, New York, NY",
"description": "Full vehicle service",
"estimatedPrice": 199.98,
"paymentStatus": "Paid",
"paymentMethod": "Credit Card",
"rating": 5,
"feedback": "Great service!",
"mileage": 45,
"evidences": [
"https://example.com/photo1.jpg",
"https://example.com/photo2.jpg"
],
"customer": {
"userId": "550e8400-e29b-41d4-a716-446655440006",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"customerCode": "CUST-001",
"email": "customer@example.com",
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890",
"gender": "Male"
},
"vehicle": {
"vin": "WBADT43452G915187",
"make": "BMW",
"model": "3 Series",
"year": 2022,
"trim": "M Sport",
"vehicleType": "Passenger Car",
"bodyType": "Sedan"
},
"van": {
"vanId": "550e8400-e29b-41d4-a716-446655440008",
"registrationNumber": "VAN-2022-001",
"vinNumber": "WV2ZZZ3CZ8E123456",
"make": "Mercedes",
"model": "Sprinter"
},
"technician": {
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"technicianCode": "TECH-001",
"firstName": "Mike",
"lastName": "Smith",
"email": "mike@example.com",
"phoneNumber": "+1987654321"
},
"services": [
{
"serviceId": "550e8400-e29b-41d4-a716-446655440001",
"serviceName": "Oil Change",
"serviceCode": "OIL-001",
"serviceDescription": "Premium oil change",
"servicePrice": 49.99,
"serviceStatus": "Completed",
"reasonOfRejection": null
}
]
}
],
"totalNumber": 5,
"hasPreviousPage": false,
"hasNextPage": true,
"totalPages": 2,
"pageNumber": 1,
"rowsPerPage": 10
}

6. Get Service Request Details

Endpoint: GET /service-request/{requestId}

Request Parameters:

{
"requestId": "550e8400-e29b-41d4-a716-446655440010"
}

Response (Success - 200): Same structure as 'Get Service Requests' Data Item (Detailed Response)


7. Get Technician Suggestions (General)

Endpoint: GET /technician-suggestions Description: Search for technicians based on criteria (Admin/Dispatcher use).

Request Parameters:

{
"searchKeyword": "Mike",
"status": "Active",
"hasVanAssigned": true,
"rowsPerPage": 10,
"pageNumber": 1
}

Response (Success - 200):

{
"data": [
{
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"technicianCode": "TECH-001",
"availabilityStatus": "Available",
"firstName": "Mike",
"lastName": "Smith",
"email": "mike@example.com",
"phoneNumber": "+1987654321"
}
],
"totalNumber": 1,
"hasPreviousPage": false,
"hasNextPage": false,
"totalPages": 1,
"pageNumber": 1,
"rowsPerPage": 10
}

8. Assign Service Request (Manual)

Endpoint: POST /service-request/assign

Request Parameters:

{
"requestId": "550e8400-e29b-41d4-a716-446655440010",
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"scheduledAt": "2026-02-20T14:30:00Z"
}

Response (Success - 200):

{
"status": 200,
"message": "Service request assigned successfully to technician"
}

9. Start Journey

Endpoint: POST /service-request/{requestId}/start-journey

Request Parameters:

{
"requestId": "550e8400-e29b-41d4-a716-446655440010"
}

Response (Success - 200):

{
"status": 200,
"message": "Journey started successfully"
}

10. Get Current Job (Technician)

Endpoint: GET /technician/current-request

Request Parameters:

{}

(No parameters required)

Response (Success - 200): Same structure as 'Get Service Request Details'


11. Get Route Info

Endpoint: GET /service-request/{requestId}/route-info

Request Parameters:

{
"requestId": "550e8400-e29b-41d4-a716-446655440010"
}

Response (Success - 200):

{
"serviceRequestInfo": {
"requestId": "550e8400-e29b-41d4-a716-446655440010",
"status": "EnRoute"
},
"technicianInfo": {
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"name": "Mike Smith"
},
"customerInfo": {
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"name": "John Doe"
},
"vanInfo": {
"vanId": "550e8400-e29b-41d4-a716-446655440008",
"registration": "VAN-001"
},
"technicianLocation": {
"latitude": 28.5,
"longitude": 77.2,
"updatedAt": "2026-02-20T14:45:00Z"
},
"distanceInMeter": 1500,
"etaInSec": 300,
"encodedPolyline": "u{~vNro|y@..."
}

12. Get Technician Suggestions (For Assignment)

Endpoint: GET /service-request/{requestId}/technician-suggestions Description: Get a list of available technicians specific to a request (includes ETA).

Request Parameters:

{
"requestId": "550e8400-e29b-41d4-a716-446655440010",
"searchKeyword": "Mike",
"rowsPerPage": 10,
"pageNumber": 1
}

Response (Success - 200):

{
"data": [
{
"technicianInfo": {
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"technicianCode": "TECH-001",
"availabilityStatus": "Available",
"firstName": "Mike",
"lastName": "Smith",
"email": "mike@example.com",
"phoneNumber": "+1234567890"
},
"eta": 15,
"jobCompletedToday": 2,
"jobQueueToday": 1
}
],
"totalNumber": 1,
"hasPreviousPage": false,
"hasNextPage": false,
"totalPages": 1,
"pageNumber": 1,
"rowsPerPage": 10
}

13. Get Technician Scheduled Requests

Endpoint: GET /service-request/{technicianId}/scheduled-requests

Request Parameters:

{
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"date": "2026-02-20"
}

Response (Success - 200):

[
{
"requestId": "550e8400-e29b-41d4-a716-446655440010",
"requestNumber": "REQ-1001",
"requestStatus": "Accepted",
"scheduledAt": "2026-02-20T10:00:00Z",
"location": "Connaught Place, New Delhi",
"estimatedDuration": 60
}
]

14. Get Initial Data

Endpoint: GET /service-request/initial-data

Summary: Get initial tracking data for authenticated user

Description: Retrieves initial tracking data for the authenticated user. Works for both technicians and customers. For technicians, returns the request at queue position 1. For customers, returns their active service request details.

Authentication Required: Yes (Technician or Customer)

Request Parameters: None (uses authenticated user's ID from token)

Response (Success - 200):

{
"status": 200,
"message": "Initial data retrieved successfully",
"data": {
// Single LocationBroadcastModel object
}
}

Response (Unauthorized - 401):

{
"status": 401,
"message": "User not authenticated"
}

Response (Forbidden - 403):

{
"status": 403,
"message": "Initial data is only available for technicians and customers"
}

Response (Not Found - 404):

{
"status": 404,
"message": "User not found"
}

Response (Server Error - 500):

{
"status": 500,
"message": "An unexpected error occurred"
}

Part 2: Automated Assignment (Ring System)

Feature Overview

Objective Implement an intelligent technician assignment system for "First Available" service requests using a progressive ring-based notification approach. When a customer requests immediate service, the system will notify nearby technicians in expanding geographic radiuses, calculate accurate ETAs based on queue positions, and manage pending requests when no technician accepts within the defined timeframe.

Scope

  • Initiating ring notifications to technicians based on distance radiuses
  • Managing technician responses (accept/reject) via SignalR
  • Queue management with maximum limits per technician
  • ETA calculation based on pending queue and service durations
  • Fallback to pending request queue when no assignment succeeds
  • Ring event tracking and audit logging
  • Real-time customer notifications about assignment status

Detailed Workflow

Complete Ring Assignment Flow:

[CUSTOMER CREATES REQUEST via POST /service-request]

System: status = 'Ringing', ringstarttime = NOW()

[RING PHASE 1: 0-20 MILES, 15 SECONDS]
Query: Technicians within 20mi, queue < 5, online (SignalR connected)
Send: ReceiveRingNotification via SignalR (RealTimeHub)

IF Accept (via PUT /service-request/{id}/1):
- Assign technician + update status
- Send RequestAccepted to the specific customer and the specific technician who accepted the request
- Send RequestStatusChanged to other ringed techs (If ringing then only those technicians who received ring call and if ringing completed then to all the technician)
- EXIT ring process
IF Timeout: Continue to next phase

[RING PHASE 2-4: 20-40, 40-60, 60-80 MILES - 15 sec each]
(Same as Phase 1)

IF Still No Accept after all phases:
status = 'Pending'
ringendtime = NOW()
Request visible to ALL technicians via GET /service-requests
Send RequestStatusChanged to customer (fallback message)

[TECHNICIAN MANUAL ACCEPT FROM PENDING]
Technician uses PUT /service-request/{id}/1 to accept
Same flow as ring accept

[ETA CALCULATION]
= Σ(service durations) + Σ(travel times) + 30min buffer
Result: "3:15-4:15 PM" (60-min window)
Send QueueUpdated to technician
Send QueuePositionChanged to customer

Part 3: Real-Time Communication

Overview

The system uses SignalR (RealTimeHub) to broadcast events to connected Clients (Customers, Technicians, Admins).

SignalR Models

public record RingNotificationModel
{
public ServiceRequestBasicInfo ServiceRequestInfo { get; init; }
public CustomerBasicInfo CustomerInfo { get; init; }
public CustomerLocation CustomerLocation { get; init; }
public int MaxDistanceInMiles { get; init; }
public int RingTimeoutSeconds { get; init; } // Always 15 for MVP
public DateTime RingStartedAt { get; init; }
}

public record LocationBroadcastModel
{
public ServiceRequestBasicInfo? ServiceRequestInfo { get; init; }
public TechnicianBasicInfo? TechnicianInfo { get; init; }
public CustomerBasicInfo? CustomerInfo { get; init; }
public VanBasicInfo? VanInfo { get; init; }
public TechnicianLocation? TechnicianLocation { get; init; }
public int? DistanceInMeter { get; init; }
public int? EtaInSec { get; init; }
public string? EncodedPolyline { get; init; }
public int? PolylineVersion { get; init; }
public int? CompletedJobs { get; init; }
public int? QueuedJobs { get; init; }
}


public record ServiceStartBroadcastModel
{
public ServiceRequestBasicInfo? ServiceRequestInfo { get; set; }
public TechnicianBasicInfo? TechnicianInfo { get; set; }
public CustomerBasicInfo? CustomerInfo { get; set; }
public DateTime ServiceStartAt { get; set; } = DateTime.UtcNow;
}

public record ServiceCompletedBroadcastModel
{
public ServiceRequestBasicInfo? ServiceRequestInfo { get; set; }
public TechnicianBasicInfo? TechnicianInfo { get; set; }
public CustomerBasicInfo? CustomerInfo { get; set; }
public DateTime ServiceCompletedAt { get; set; } = DateTime.UtcNow;
}

public record TechnicianLocation : CustomerLocation
{
public decimal Latitude { get; set; }
public decimal Longitude { get; set; }
public double? Heading { get; set; }
public double? Speed { get; set; }
public double? Accuracy { get; set; }
public DateTime UpdatedAt { get; set; }
}

public class CustomerLocation
{
public decimal Latitude { get; set; }
public decimal Longitude { get; set; }
}

public class LocationDataWithRequestId: TechnicianLocation
{
public Guid? RequestId { get; set; }
}

public record TechnicianArrivedBroadcastModel
{
public Guid RequestId { get; init; }
public string? TechnicianName { get; init; }
public string? TechnicianCode { get; init; }
public string? Message { get; init; }
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
}

Events List

Event NamePayloadDescription
ring_notification_receivedRingNotificationModelRing sent to technician with countdown timer.
request_acceptedLocationBroadcastModelNotification when request is accepted by technician.
request_assignedServiceRequestDetailedResponseNew request assigned to technician.
journey_started{ RequestId, TechnicianName, Message, Timestamp, RequestDetails }Notify that technician has started journey to customer location.
request_canceledServiceRequestDetailedResponseRequest was canceled by either customer or technician.
service_startedServiceStartBroadcastModelService has started at customer location.
service_completedServiceCompletedBroadcastModelService has been completed at customer location.
technician_arrivedTechnicianArrivedBroadcastModelNotifies customer when technician is within 30m.
new_service_requestServiceRequestDetailedResponseNotifies admins of a new service request.
request_status_changedServiceRequestBasicInfoCustomer notification of assignment/status change.
LocationUpdatedLocationBroadcastModelUpdates technician location, distance, ETA, and polyline route.

SignalR Event Examples

1. Request Accepted

Event: request_accepted (Server → Client) Broadcast Groups: Request-{requestId}, AllAdmins Sent when a technician accepts a service request.

{
"serviceRequestInfo": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"requestNumber": "RQ-2026-001234",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"requestStatus": "EnRoute",
"latitude": 40.7128,
"longitude": -74.0060,
"location": "123 Main St, New York, NY",
"requestedAt": "2026-01-13T10:30:00Z"
},
"technicianInfo": {
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"technicianCode": "TECH-001",
"availabilityStatus": "EnRoute",
"firstName": "Mike",
"lastName": "Smith",
"email": "mike@example.com",
"phoneNumber": "+1987654321"
},
"customerInfo": {
"userId": "550e8400-e29b-41d4-a716-446655440006",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"customerCode": "CUST-001",
"email": "customer@example.com",
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890",
"gender": "Male"
},
"vanInfo": {
"vanId": "550e8400-e29b-41d4-a716-446655440008",
"vanNumber": "VAN-001",
"registrationNumber": "VAN-2022-001",
"vin": "WV2ZZZ3CZ8E123456"
},
"technicianLocation": {
"latitude": 40.7500,
"longitude": -73.9800,
"heading": 125.5,
"speed": 45.3,
"accuracy": 8.5,
"updatedAt": "2026-01-13T10:45:00Z"
},
"distanceInMeter": 15000,
"etaInSec": 1200,
"encodedPolyline": "_p~iF~ps|U_ulLnnqC_mqNvxq`@",
"polylineVersion": 2,
"completedJobs": 2,
"queuedJobs": 1
}

2. Request Assigned

Event: request_assigned (Server → Technician) Broadcast Groups: Request-{requestId}, AllAdmins Sent when a new request is assigned to a technician.

{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"vehicleId": "550e8400-e29b-41d4-a716-446655440005",
"requestNumber": "REQ-20260217-001",
"serviceScheduleType": "OnDemand",
"locationPreference": "CustomerLocation",
"requestStatus": "Assigned",
"requestedAt": "2026-01-13T10:30:00Z",
"scheduledAt": null,
"latitude": 40.7128,
"longitude": -74.0060,
"location": "123 Main St, New York, NY",
"description": "Full vehicle service",
"estimatedPrice": 199.98,
"paymentStatus": "Paid",
"paymentMethod": "Credit Card",
"rating": null,
"feedback": null,
"mileage": null,
"evidences": null,
"customer": {
"userId": "550e8400-e29b-41d4-a716-446655440006",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"customerCode": "CUST-001",
"email": "customer@example.com",
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890",
"gender": "Male"
},
"vehicle": {
"vin": "WBADT43452G915187",
"make": "BMW",
"model": "3 Series",
"year": 2022,
"trim": "M Sport",
"vehicleType": "Passenger Car",
"bodyType": "Sedan"
},
"van": {
"vanId": "550e8400-e29b-41d4-a716-446655440008",
"registrationNumber": "VAN-2022-001",
"vinNumber": "WV2ZZZ3CZ8E123456",
"make": "Mercedes",
"model": "Sprinter"
},
"technician": {
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"technicianCode": "TECH-001",
"availabilityStatus": "Available",
"firstName": "Mike",
"lastName": "Smith",
"email": "mike@example.com",
"phoneNumber": "+1987654321"
},
"services": [
{
"serviceId": "550e8400-e29b-41d4-a716-446655440001",
"serviceCode": "OIL-001",
"serviceName": "Oil Change",
"serviceDescription": "Premium oil change",
"servicePrice": 49.99,
"serviceStatus": "Pending",
"reasonOfRejection": null
}
]
}

2.1 Journey Started

Event: journey_started (Server → Customer) Broadcast Groups: Request-{requestId} Sent when the technician starts the journey to the customer's location.

{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"technicianName": "John Doe",
"message": "Technician has started journey to your location",
"timestamp": "2026-02-21T14:30:00Z",
"requestDetails": {
"requestDetails": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"vehicleId": "550e8400-e29b-41d4-a716-446655440005",
"requestNumber": "REQ-20260217-001",
"serviceScheduleType": "OnDemand",
"locationPreference": "CustomerLocation",
"requestStatus": "EnRoute",
"requestedAt": "2026-01-13T10:30:00Z",
"scheduledAt": null,
"latitude": 40.7128,
"longitude": -74.0060,
"location": "123 Main St, New York, NY",
"description": "Full vehicle service",
"estimatedPrice": 199.98,
"paymentStatus": "Paid",
"paymentMethod": "Credit Card",
"rating": null,
"feedback": null,
"mileage": null,
"evidences": null,
"customer": {
"userId": "550e8400-e29b-41d4-a716-446655440006",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"customerCode": "CUST-001",
"email": "customer@example.com",
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890",
"gender": "Male"
},
"vehicle": {
"vin": "WBADT43452G915187",
"make": "BMW",
"model": "3 Series",
"year": 2022,
"trim": "M Sport",
"vehicleType": "Passenger Car",
"bodyType": "Sedan"
},
"technician": {
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"technicianCode": "TECH-001",
"availabilityStatus": "EnRoute",
"firstName": "Mike",
"lastName": "Smith",
"email": "mike@example.com",
"phoneNumber": "+1987654321"
},
"services": [
{
"serviceId": "550e8400-e29b-41d4-a716-446655440001",
"serviceCode": "OIL-001",
"serviceName": "Oil Change",
"serviceDescription": "Premium oil change",
"servicePrice": 49.99,
"serviceStatus": "Pending",
"reasonOfRejection": null
}
]
}
}
}

3. Request Canceled

Event: request_canceled (Server → Client) Broadcast Groups: Request-{requestId}, AllAdmins Sent when a request is canceled by either technician or customer.

{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"vehicleId": "550e8400-e29b-41d4-a716-446655440005",
"requestNumber": "REQ-20260217-001",
"serviceScheduleType": "OnDemand",
"locationPreference": "CustomerLocation",
"requestStatus": "Canceled",
"requestedAt": "2026-01-13T10:30:00Z",
"scheduledAt": null,
"latitude": 40.7128,
"longitude": -74.0060,
"location": "123 Main St, New York, NY",
"description": "Full vehicle service",
"estimatedPrice": 199.98,
"paymentStatus": "Paid",
"paymentMethod": "Credit Card",
"rating": null,
"feedback": null,
"mileage": null,
"evidences": null,
"customer": {
"userId": "550e8400-e29b-41d4-a716-446655440006",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"customerCode": "CUST-001",
"email": "customer@example.com",
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890",
"gender": "Male"
},
"vehicle": {
"vin": "WBADT43452G915187",
"make": "BMW",
"model": "3 Series",
"year": 2022,
"trim": "M Sport",
"vehicleType": "Passenger Car",
"bodyType": "Sedan"
},
"technician": {
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"technicianCode": "TECH-001",
"availabilityStatus": "Available",
"firstName": "Mike",
"lastName": "Smith",
"email": "mike@example.com",
"phoneNumber": "+1987654321"
},
"services": [
{
"serviceId": "550e8400-e29b-41d4-a716-446655440001",
"serviceCode": "OIL-001",
"serviceName": "Oil Change",
"serviceDescription": "Premium oil change",
"servicePrice": 49.99,
"serviceStatus": "Pending",
"reasonOfRejection": null
}
]
}

4. Service Started

Event: service_started (Server → Client) Broadcast Groups: Request-{requestId}, AllAdmins Sent when service has started.

{
"serviceRequestInfo": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"requestNumber": "RQ-2026-001234",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"requestStatus": "InProgress",
"latitude": 40.7128,
"longitude": -74.0060,
"location": "123 Main St, New York, NY",
"requestedAt": "2026-01-13T10:30:00Z"
},
"technicianInfo": {
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"technicianCode": "TECH-001",
"availabilityStatus": "Servicing",
"firstName": "Mike",
"lastName": "Smith",
"email": "mike@example.com",
"phoneNumber": "+1987654321"
},
"customerInfo": {
"userId": "550e8400-e29b-41d4-a716-446655440006",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"customerCode": "CUST-001",
"email": "customer@example.com",
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890",
"gender": "Male"
},
"serviceStartAt": "2026-01-21T14:30:00Z"
}

5. Service Completed

Event: service_completed (Server → Client) Broadcast Groups: Request-{requestId}, AllAdmins Sent when service has been completed.

{
"serviceRequestInfo": {
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"requestNumber": "RQ-2026-001234",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"requestStatus": "Completed",
"latitude": 40.7128,
"longitude": -74.0060,
"location": "123 Main St, New York, NY",
"requestedAt": "2026-01-13T10:30:00Z"
},
"technicianInfo": {
"technicianId": "550e8400-e29b-41d4-a716-446655440007",
"technicianCode": "TECH-001",
"availabilityStatus": "Available",
"firstName": "Mike",
"lastName": "Smith",
"email": "mike@example.com",
"phoneNumber": "+1987654321"
},
"customerInfo": {
"userId": "550e8400-e29b-41d4-a716-446655440006",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"customerCode": "CUST-001",
"email": "customer@example.com",
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890",
"gender": "Male"
},
"serviceCompletedAt": "2026-01-21T15:30:00Z"
}

6. Technician Arrived

Event: technician_arrived (Server → Admin/Technician/Customer) Broadcast Groups: Request-{requestId} Sent when technician arrives at customer location (distance < 30 meters).

{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"firstName": "John",
"lastName": "Doe",
"technicianCode": "TECH-001",
"message": "Technician has arrived at your location",
"timestamp": "2026-01-21T14:30:00Z"
}

7. New Service Request

Event: new_service_request (Server → Admin) Broadcast Groups: Request-{requestId}, AllAdmins Sent to admins when a new service request is created.

{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"vehicleId": "550e8400-e29b-41d4-a716-446655440005",
"requestNumber": "REQ-20260217-001",
"serviceScheduleType": "OnDemand",
"locationPreference": "CustomerLocation",
"requestStatus": "Pending",
"requestedAt": "2026-01-13T10:30:00Z",
"scheduledAt": null,
"latitude": 40.7128,
"longitude": -74.0060,
"location": "123 Main St, New York, NY",
"description": "Full vehicle service",
"estimatedPrice": 199.98,
"paymentStatus": "Pending",
"paymentMethod": null,
"rating": null,
"feedback": null,
"mileage": null,
"evidences": null,
"customer": {
"userId": "550e8400-e29b-41d4-a716-446655440006",
"customerId": "550e8400-e29b-41d4-a716-446655440004",
"customerCode": "CUST-001",
"email": "customer@example.com",
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+1234567890",
"gender": "Male"
},
"vehicle": {
"vin": "WBADT43452G915187",
"make": "BMW",
"model": "3 Series",
"year": 2022,
"trim": "M Sport",
"vehicleType": "Passenger Car",
"bodyType": "Sedan"
},
"services": [
{
"serviceId": "550e8400-e29b-41d4-a716-446655440001",
"serviceCode": "OIL-001",
"serviceName": "Oil Change",
"serviceDescription": "Premium oil change",
"servicePrice": 49.99,
"serviceStatus": "Pending",
"reasonOfRejection": null
}
]
}

Development Tasks & Estimates (Preserved)

NoTask NameEst. (Hrs)
1DB Migration: Add columns to servicerequests table0.5
2DB Migration: Create technicianringlog table with indexes1.5
3Create RingNotificationModel and related DTOs2
4Implement GetEligibleTechnicians for ringing5
5Implement StartRingProcess method4
6Implement ring phase progression logic (4 phases, 15 sec each)4
7Implement ring timeout and phase transition handling1.5
8Implement ring event logging to technicianringlog1.5
9Implement queue limit validation2
10Implement GetCustomerQueuePosition (FIFO ordering)4
11Implement CalculateETA (service duration + travel + buffer)7
12Cache technician locations in Redis for ETA calc2
13Add SignalR event: ring_notification_received3
14Add SignalR event: request_accepted3
15Add SignalR event: request_status_changed2
16Enhance POST /service-request for ring initiation3
17Enhance PUT /service-request/{id}/accept with optimistic lock5
18Enhance PUT /service-request/{id}/reject with ring logging1
19Enhance GET /service-request/{id} with ETA and queue position2
TOTAL54 hrs

Risks & Mitigations

RiskImpactMitigation
Race Condition: Multiple Simultaneous AcceptsHighOptimistic locking with version check; clear error to slower technician
SignalR Disconnect During RingHighAuto-reconnection; mark as timeout; missed notification recovery
MapBox API Unavailable/SlowMediumFallback estimation (25 mph avg); cache results; 10-sec timeout
Technician Queue All at LimitHighEmergency service priority; admin override; alert client
ETA Calculation InaccuracyMediumUse historical completion data; customer feedback loop

Review & Approval

  • Reviewer:

    • Product Manager / Client
    • Tech Lead / Architecture Review
    • QA Lead / Testing Strategy Review
  • Approval Date:

    • Pending client/product manager approval
    • Pending tech lead architecture review
    • Pending QA test strategy approval

Part 4: Additional Implementation Guidelines

Testing & Quality Assurance

Unit Tests

RingAssignmentService Tests:

  • Test_GetEligibleTechnicians_FiltersCorrectly() - Distance, queue, status filters
  • Test_CalculateDistance_ReturnsAccurateResults() - MapBox distance accuracy
  • Test_RingTimeout_TransitionsToNextPhase() - Phase progression
  • Test_RingTimeout_MarksPendingAfterAllPhases() - Fallback to pending

TechnicianQueueService Tests:

  • Test_AcceptRequest_OptimisticLocking() - Prevent double-assignment
  • Test_AcceptRequest_QueueLimitEnforced() - Reject if queue ≥ 5
  • Test_CalculateQueuePosition_FIFO() - Correct ordering
  • Test_RejectRequest_LogsReason() - Rejection tracking

ETACalculationService Tests:

  • Test_CalculateETA_SumServiceDurations() - Service time summation
  • Test_CalculateETA_IncludeTravelTime() - MapBox travel time
  • Test_CalculateETA_AddBuffer() - 15-minute buffer
  • Test_CalculateETA_Window45Minutes() - ETA precision
  • Test_UpdateETAInRealtime() - Dynamic updates

AdminRequestActionService Tests:

  • Test_ManualAssignment_UpdatesQueueCorrectly()
  • Test_UnassignRequest_ReturnsToPending()
  • Test_ReorderQueue_ValidatesOrder()
  • Test_AdminAction_LogsAuditTrail()

Integration Tests

  1. Complete Ring Flow

    • Setup: Customer request, 5+ technicians at various distances
    • Verify: Ring phases execute, correct techs notified, proper state transition
  2. Technician Acceptance During Ring

    • Setup: Request in ring phase 2, tech queue < 5
    • Verify: Assignment succeeds, others notified, ETA calculated, race condition prevented
  3. No-Acceptance Fallback

    • Setup: All technicians >75 miles away
    • Verify: Request → PendingAssignment, visible to all, customer notified, admin alerted
  4. Admin Manual Assignment

    • Setup: Pending request, technician available
    • Verify: Assignment succeeds, action logged, both parties notified, audit trail complete
  5. ETA Real-time Updates

    • Setup: Tech with 3 requests in queue
    • Verify: ETA updates when service completes, customers notified
  6. Queue Limit Enforcement

    • Setup: Technician with 5 requests, new request created
    • Verify: Technician not eligible for ring, cannot manually accept
  7. SignalR Disconnection Recovery

    • Setup: Technician receives ring, disconnects
    • Verify: Timeout detected, auto-marked, tech can reconnect and recover missed notifications

Acceptance Criteria

Ring Assignment:

  • ✅ Ring notifications sent within 2 seconds
  • ✅ Ring timeouts enforced (30/20/15/15 seconds)
  • ✅ No double-assignment (race condition prevented)
  • ✅ Rejection tracked with reason
  • ✅ All 4 phases execute properly

Queue Management:

  • ✅ Queue limit enforced (≤ 5)
  • ✅ Queue position visible to customers
  • ✅ FIFO ordering maintained
  • ✅ Real-time queue updates

ETA:

  • ✅ Calculated within 1 second
  • ✅ Includes service duration + travel + buffer
  • ✅ Window ≤ 45 minutes
  • ✅ Updates dynamically as queue progresses
  • ✅ Shown to customer within 5 seconds

Admin Override:

  • ✅ Can manually assign pending requests
  • ✅ Can unassign requests
  • ✅ Can reorder queue
  • ✅ All actions audited with oldstate/newstate
  • ✅ Both parties notified

Notifications:

  • ✅ Customer notified when created/assigned/position changes
  • ✅ Technician notified of rings/queue changes
  • ✅ Admin notified when pending queue grows

Performance:

  • ✅ Ring delivery: < 2 sec
  • ✅ Accept/reject: < 100ms
  • ✅ ETA calculation: < 1 sec
  • ✅ Queue updates: < 1 sec
  • ✅ Database queries: < 200ms

Testing Tools

  • xUnit.NET - Unit testing
  • Moq - Mocking dependencies
  • testcontainers-dotnet - Docker PostgreSQL for integration tests
  • Postman - API endpoint testing
  • Fiddler - SignalR message inspection
  • JetBrains Rider Debugger - Debugging

Deployment Considerations

Configuration Changes

Environment Variables (Secrets):

  • MAPBOX_API_KEY - Already configured
  • REDIS_CONNECTION_STRING - Ensure Redis running
  • SIGNALR_HUB_URL - Configure for your environment

Rollout Plan

Phase 1: Development & Testing (Days 1-7)

  • Full integration testing with test data
  • Load testing with concurrent requests
  • Client review and approval

Phase 2: Staging (Day 8)

  • Deploy to staging environment
  • End-to-end tests with staging techs
  • Verify SignalR connectivity
  • Performance baseline measurement

Phase 3: Production (Day 9-10)

  • Create database backup before migration
  • Run migration during low-traffic window (2 AM)
  • Deploy backend services (rolling deployment)
  • Verify APIs operational
  • Keep feature flag RingAssignment.Enabled = false initially

Phase 4: Gradual Rollout (Days 11-14)

  • Day 1: Enable for 10% of technicians (test group)
  • Day 2: Enable for 25%
  • Day 3: Enable for 50%
  • Day 4: Enable for 100%

Rollback Plan:

  • Set RingAssignment.Enabled = false in config
  • All new requests revert to manual assignment
  • No data loss (all ring logs preserved)
  • Rollback time: < 5 minutes

Health Checks:

  • /health/ring-assignment endpoint returns all system statuses
  • Monitor: SignalR, Redis, MapBox, Database connectivity

Monitoring & Alerts:

  • Ring failure rate > 5% → Alert
  • Pending queue size > 10 → Alert
  • SignalR failures > 2% → Alert
  • MapBox latency > 5 sec → Fallback to cache

Implementation Notes

Key Design Decisions

  1. SignalR-Only for MVP (FCM to follow in Phase 2)

    • Faster initial delivery (7-day timeline)
    • Requires technicians to keep app open during shifts
    • Clear path to FCM addition without major refactoring
  2. Progressive Ring with Expanding Radiuses

    • Prioritizes nearby technicians (faster service)
    • Automatic fallback when no local technicians available
    • Guarantees completion within 65 seconds
  3. FIFO Queue Ordering

    • Simple, fair, predictable for MVP
    • Customer satisfaction (no one jumps ahead)
    • Future: Can evolve to load-based or geographic optimization
  4. Optimistic Locking for Acceptance

    • Prevents double-assignment without distributed locks
    • Fast response time
    • Clear error message to slower technician
  5. Real-time ETA Updates

    • Calculated at assignment time
    • Updated as queue progresses
    • 45-minute max window for realistic expectations

Future Enhancements (Post-MVP)

  • FCM Background Notifications - Remove app-open requirement
  • Predictive Queue Matching - Assign based on remaining queue time
  • Geographic Optimization - Minimize total travel time
  • Service Type Specialization - Ring only certified technicians
  • Machine Learning - Predict acceptance rates
  • SMS/Email Fallback - Notify when push fails

Success Metrics

  • Ring acceptance rate: 80%+ on ring 1, 95%+ by ring 3
  • Ring-to-assignment time: < 60 seconds (goal: 30 sec)
  • Customer ETA satisfaction: > 85% within 30-min window
  • Average pending queue size: < 2 requests, max < 5
  • System uptime: 99.5% availability
  • Technician queue overload complaints: < 5%

Support & Maintenance

  • Daily monitoring of ring assignment metrics
  • Weekly client review meeting
  • Rapid hotfix deployment (< 4 hours SLA for critical bugs)
  • Monthly performance optimization
  • Quarterly feature planning