Dealer Management System
Author(s)
- Faizal Khan
- Pritam Dutta
- Sanket Mal
- Ayan Ghosh
- Ashik Ikbal
Last Updated Date
2025-12-19
SRS References
Version History
| Version | Date | Changes | Author |
|---|---|---|---|
| 1.0 | 2025-11-16 | Initial draft - Dealer API design | Ashik |
| 1.1 | 2025-11-17 | Added latitude/longitude to DealerAddress, auto-populate from zipcodedetails | Ashik |
| 1.2 | 2025-12-19 | Feature Listing | Faizal Khan |
Dealer Management
Feature Overview
Objective
Provide backend APIs for dealer lifecycle management (create dealer, list dealers with filters, fetch dealer details, update dealer profile and addresses, fetch address suggestions, and update dealer radius) used by admin and dealer users.
Scope
- Dealer creation and onboarding with detailed information (including addresses, managers, business hours).
- Dealer listing with pagination and advanced filtering.
- Dealer detail retrieval with addresses, Business Hours, Contact Persons, etc.
- Dealer profile and address updates with validation rules.
- Address suggestion retrieval for a dealer.
- Dealer radius update.
- Geo-coordinates (latitude, longitude) for dealer addresses auto-populated from zipcodedetails
Dependencies
-
Authentication & Authorization
- JWT-based authentication.
- Scope-based authorization.
-
Contract Libraries & Models
ContractLibrary.Dealer:DealerDealerAddressDealerFilterUpdateDealerRadiusRequestDealerCreationResponseGetDealersRequestDealerMinimalInfo
ContractLibrary.Common:CommonResponseServerPaginatedData<T>- Enums:
Status,UserType,DealershipType,Brand,AddressType,DealerSortBy,SortOrder.
-
Database
- DAL uses the following tables:
dealermasterdealeraddresszipcodedetailsadminmaster
- DAL uses the following tables:
-
Messaging & Notifications
MassTransit(IBus, request clients).- Requests to Auth services:
UserCreateRequest/UserCreateResponsePrimaryUserUpdateRequest
- Notifications via
ContractLibrary.Notification:NotificationRequestBatchNotificationRequestNotificationType.NewDealerCreate
-
Geolocation
IGeolocationServiceandGeolocationService(fromCommonLibrary.Services), used to resolve zipcode to latitude/longitude, with fallback to Nominatim API.
Request & Response Models
Request Models
Dealer (ContractLibrary.Dealer.Dealer)
public record Dealer
{
public Guid DealerId { get; set; }
public string? DealerCode { get; set; }
public required string PrimaryEmail { get; init; }
public required string CompanyName { get; init; }
public string? ContactPersonName { get; init; }
public required string Phone { get; init; }
public string? Website { get; init; }
public List<DealershipType>? DealershipType { get; init; }
public List<Brand>? Brand { get; init; }
public Status Status { get; set; } = Status.Active;
public string? DealerLogoUrl { get; set; }
public List<DealerAddress>? Addresses { get; init; } = new List<DealerAddress>();
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
public string? LogUsername { get; set; }
public DateTimeOffset LogDts { get; set; } = DateTimeOffset.UtcNow;
}
DealerAddress (ContractLibrary.Dealer.DealerAddress)
public record DealerAddress
{
public AddressType AddressType { get; init; }
public string? AddressLine1 { get; init; }
public string? City { get; init; }
public string? State { get; init; }
public string? ZipCode { get; init; }
public double? Latitude { get; set; } // auto-populated from zipcodedetails
public double? Longitude { get; set; } // auto-populated from zipcodedetails
public bool IsPrimary { get; set; } = false; // indicates primary address for dealer
public double? Radius { get; set; } // optional radius for this address
}
DealerFilter (ContractLibrary.Dealer.DealerFilter)
public record DealerFilter
{
public string? DealerCode { get; set; }
public string? CompanyName { get; set; }
public string? ContactPersonName { get; set; }
public string? PrimaryEmail { get; set; }
public string? Phone { get; set; }
public List<Status>? Status { get; set; }
public List<DealershipType>? DealershipTypes { get; set; }
public List<Brand>? Brands { get; set; }
public string? Website { get; set; }
public DateTimeOffset? CreatedAtFrom { get; set; }
public DateTimeOffset? CreatedAtTo { get; set; }
public DateTimeOffset? UpdatedAtFrom { get; set; }
public DateTimeOffset? UpdatedAtTo { get; set; }
public string? SearchKeyword { get; set; }
public DealerSortBy SortBy { get; set; }
public SortOrder SortDirection { get; set; }
[Range(1, int.MaxValue, ErrorMessage = "Page number must be greater than 0")]
public int PageNumber { get; set; }
[Range(1,100, ErrorMessage = "Page size must be between 1 and 100")]
public int RowsPerPage { get; set; }
}
UpdateDealerRadiusRequest (ContractLibrary.Dealer.UpdateDealerRadiusRequest)
public record UpdateDealerRadiusRequest
{
[Range(1, 1000, ErrorMessage = "Radius must be between 1 and 1000 KM")]
public required double Radius { get; init; }
}
Response Models
CommonResponse (ContractLibrary.Common.CommonResponse)
public record CommonResponse
{
public int Status { get; init; }
public string? Message { get; init; }
}
ServerPaginatedData<T>
From ContractLibrary.Common.ServerPaginatedData<T>
public class ServerPaginatedData<T>
{
public List<T> Data { get; set; }
public int TotalNumber { get; set; }
public bool HasPreviousPage { get; set; }
public bool HasNextPage { get; set; }
public int TotalPages { get; set; }
public int PageNumber { get; set; }
public int RowsPerPage { get; set; }
}
Dealer (Response)
- Same as request model, with all properties populated.
List<DealerAddress> (Response)
- List of
DealerAddressrecords as defined above, includingLatitudeandLongitude.
Geo-coordinates Handling
- On create and update dealer, for each address:
- The system will fetch
latitudeandlongitudefrom thezipcodedetailstable using the providedZipCode. - These values are stored in the
dealeraddresstable and returned in API responses. - If the zip code is not found,
LatitudeandLongitudewill benull.
- The system will fetch
Primary Address Management
- Each dealer must have exactly one primary address (
isprimary = true) - When updating dealer addresses:
- If a new address is marked as primary, all other addresses for that dealer are automatically set to
isprimary = false - If no address is marked as primary, the system will default the first address as primary
- If a new address is marked as primary, all other addresses for that dealer are automatically set to
- The primary address is used for:
- Geo-radius filtering calculations
- Default location for auction notifications
- Dealer's main service location
Requirements
Functional Requirements
1. Dealer Creation
- Only users with:
- Valid JWT
user_typeclaim =Admin- Scopes
Right.admin_userandRight.dealer_createcan create dealers.
- Request body is a
Dealerobject with at least:primaryEmail(required by validation)companyNamephone
- System responsibilities:
- Validate that
Dealeris non-null andPrimaryEmailis present. - Check duplicates:
- Email (
GetDealerByEmail) - Phone (
GetDealerByPhone) if provided.
- Email (
- Generate unique dealer code using sequence from DB and pattern:
DL-{sequence:D5}(e.g.,DL-00001).
- Assign:
- New
dealerId(Guid.NewGuid()). Status = Status.Active.- Logging fields
LogUsername,LogDts.
- New
- Persist dealer into
dealermaster. - Persist dealer addresses into
dealeraddresswith:- Exactly one
AddressType.Legal. - Exactly one
AddressType.Billing.
- Exactly one
- For each address:
- If zipcode known in
zipcodedetails, populatelatitudeandlongitude. - If zipcode missing, fetch from Nominatim via
IGeolocationServiceand insert intozipcodedetails.
- If zipcode known in
- Create a primary user for the dealer (UserType = Dealer) via
UserCreateRequest. - Publish a welcome notification (
NotificationType.NewDealerCreate) viaMassTransit.
- Validate that
If address collection is missing, empty, or does not contain exactly one legal and one billing address, creation fails with MbdnStatusCode.INVALID_REQUEST.
2. Dealer Listing
- Only Admin users with:
- Scope
Right.admin_userandRight.dealer_view_all. user_type=Admin. can access this endpoint.
- Scope
- Client sends query parameters mapped to
DealerFilter:- Basic filters:
dealerCode,companyName,ownerName,primaryEmail,phone,website. - Status list (
Statusenum). - Dealership types (
DealershipTypeenum list). - Brands (
Brandenum list). - Created/Updated date ranges.
searchKeyword(global search across multiple columns).- Sorting via
DealerSortByandSortOrder. - Pagination via
pageNumberandrowsPerPage.
- Basic filters:
- DAL builds dynamic SQL over
dealermasterwith:- Filtering based on provided parameters.
- Global search using
unnest(@Keywords)and multipleILIKEconditions. - Sorting by chosen column.
- Pagination using
LIMITandOFFSET.
- Addresses are joined by separate query on
dealeraddressand attached to eachDealer. - Response shape is
ServerPaginatedData<Dealer>:data(list ofDealer)- Pagination metadata (
totalNumber,totalPages,hasPreviousPage,hasNextPage,pageNumber,rowsPerPage).
3. Dealer Details
- Requires
Right.dealer_viewscope. - Accepts
dealerIdas route parameter (Guid). - DAL:
- Fetches dealer core data from
dealermaster. - Fetches addresses from
dealeraddressordered byaddressType. - Populates
DealershipTypeandBrandfrom text arrays.
- Fetches dealer core data from
- Returns
Dealerwith:- Core profile fields.
- Status.
- Full list of addresses with type, geolocation, and
isPrimary.
- If dealer does not exist:
- Returns
MbdnStatusCode.RECORD_NOT_FOUNDand 404.
- Returns
4. Dealer Updates
- Requires:
Right.dealer_updatescope.- Valid
dealerIdin route. - Dealer body (
Dealer) in request.
- Authorization rules:
user_typemust beAdminorDealer.- If user is
Dealer(non-admin):entity_idclaim must be present and equal todealerId(cannot update other dealers).
- Validation (
ValidateDealerUpdateData):- Dealer cannot be null.
DealerIdmust be non-empty.- Reuse of basic
Dealervalidation (e.g.,PrimaryEmail). - Duplicate checks:
- Email must not belong to another dealer.
- Phone must not belong to another dealer.
- Address validation and update:
- Negative
Radiusis rejected. - If no address is marked
IsPrimary, first address is made primary. - Only one primary address allowed.
- There must be exactly one
Legaland oneBillingaddress (EnsureSingleLegalAndBillingAddress). - It is not allowed to:
- Delete existing legal or billing address.
- Add a second legal or billing address (
ValidateLegalAndBillingModification).
- New addresses:
- Cannot be of type
LegalorBilling. - Get new
AddressId(Guid.NewGuid()).
- Cannot be of type
- Non-admin users cannot update legal address (
AddressType.Legal). - All address insert/update/delete operations are classified and applied via DAL:
SaveDealerAddressUpdateDealerAddressesDeleteDealerAddresses
- Zipcode geolocation is refreshed (same as in creation) using
zipcodedetailsand Nominatim for any missing zipcodes.
- Negative
- If primary email or phone changed:
PrimaryUserUpdateRequestis sent to update dealer's primary user details.
5. Address Suggestions
- Requires
Right.dealer_viewscope. - Behavior depends on
user_type:- Admin:
- Must provide
dealerIdas query parameter.
- Must provide
- Dealer:
- Uses
entity_idclaim as dealer ID. - If
dealerIdquery parameter is also provided and does not matchentity_id, request is rejected.
- Uses
- Admin:
- Helper:
- Validates non-empty dealer ID.
- Fetches addresses from
dealeraddress. - Returns:
- 200 with addresses if found.
RECORD_NOT_FOUNDif dealer not found.
6. Radius Update
- Requires
Right.dealer_updatescope. - Uses
UpdateDealerRadiusRequestbody:radius(decimal, required).
- Authorization:
- User must be
AdminorDealer. entity_idclaim is parsed asdealerId.
- User must be
- Validation:
radiusmust be between 1 and 1000 KM.
- DAL:
- Confirms dealer existence via
GetDealerMinimalInfo. - Updates
radiusindealermasterand audit fields (updatedAt,logUsername,logDts).
- Confirms dealer existence via
- Returns:
OKon success.RECORD_NOT_FOUNDif dealer not found.
Non-Functional Requirements
1. Performance
- All database access for dealer listing uses paginated queries with
LIMIT/OFFSETand server-side filtering to avoid loading all dealers at once. - Queries are built with parameterized SQL and leverage indexes implicitly on key columns (e.g.,
dealerCode,companyName,status) based on their usage.
2. Security
- JWT validation is configured.
- All
DealerControlleractions are protected by:[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)].- Scope attributes (
CustomRequiredScope,RequiredScope).
- Fine-grained authorization rules based on:
user_typeandentity_idclaims.- Specific rights such as
Right.dealer_create,Right.dealer_view_all,Right.dealer_view,Right.dealer_update.
3. Scalability
- Pagination and filterable listing are implemented to support large dealer datasets.
4. Usability
- Easy-to-use interface for both buyers and dealers.
- Mobile-friendly design to support access from any device.
5. Reliability
- Consistent error responses.
- Logging for all critical operations and errors.
Workflow Summary
-
Dealer Creation
- Admin calls
POST /createwithDealerpayload. - Helper validates dealer data, checks for duplicates, generates dealer code, assigns IDs and status.
- Dealer record is inserted into
dealermaster. - Addresses are validated and stored in
dealeraddresswith geolocation populated fromzipcodedetailsor Nominatim. - Contact Persons details are stored in
Contacttable. - Business Hours details are stored in
DepartmentBusinessHoursafter proper validation. - A primary user is created for the dealer via Auth service.
- A welcome and activation email notification is queued via notification service. Dealers and Contact Persons can activate their profile from the mail.
- Admin calls
-
Dealer Listing
- Admin calls
GET /listwithDealerFilterquery parameters. - Helper validates filter ranges, normalizes strings, and delegates to DAL.
- DAL builds dynamic SQL for filters, global search, sorting, and pagination.
- Dealer list and total count are returned; addresses are attached per dealer.
- Admin calls
-
Dealer Details
- Authorized user calls
GET /details/{dealerId}. - DAL reads dealer row and associated addresses, mapping enums from text arrays.
- Helper returns a full
Dealerobject to the controller, which returns 200 or appropriate error.
- Authorized user calls
-
Dealer Updates
- Authorized Admin or Dealer calls
PUT /update/{dealerId}with updatedDealerpayload. - Controller enforces that dealers can update only their own records using
entity_id. - Helper validates data, checks duplicates, and loads current dealer and addresses.
- Address operations (add/update/delete) are classified and validated (especially legal/billing constraints).
- DAL updates
dealermasteranddealeraddressin a transaction. - If primary email/phone changed, helper triggers primary user update.
- Authorized Admin or Dealer calls
-
Address Suggestions
- Admin or Dealer calls
GET /address-suggestion:- Admin passes
dealerId. - Dealer uses
entity_idclaim.
- Admin passes
- Helper fetches addresses and returns them as suggestions.
- Admin or Dealer calls
-
Radius Update
- Admin or Dealer calls
PUT /update-radiuswithUpdateDealerRadiusRequest. - Controller resolves dealer ID from
entity_idclaim. - Helper validates radius and dealer existence, then DAL updates radius in
dealermaster.
- Admin or Dealer calls
Example Use Case: Dealer Profile Update
- Admin updates company name, website, and brand list for an existing dealer via
PUT /update/{dealerId}. - Dealer updates their billing address radius while keeping legal address unchanged.
- Admin later uses
GET /listwith filters onstatusandsearchKeywordto confirm updated dealer data appears correctly.
Development Tasks & Estimates
| No | Task Name | Estimate (Hours) | Dependencies | Notes |
|---|---|---|---|---|
| 1 | DealerController API implementation | 10 | None | All endpoints, validation, error handling (includes UpdateDealerRadius) |
| 2 | DealerHelper business logic and integration | 7 | Task1 | Includes radius update logic |
| 3 | Unit and integration tests for DealerController | 10 | Task1,2 | Includes tests for radius update API |
| 4 | Swagger/OpenAPI documentation | 2 | Task1 | |
| Grand Total | 29 hours |
Design Specifications
UI/UX Design
The dealer management feature includes several key user interfaces:
-
Dealer Onboarding Page
- Multi-step form for registering a new dealership
- Basic company details (company name, owner name, email, phone, website)
- Logo upload section
- Contact person information (role, name, phone, email)
- Business Hours for individual department
- Dealership type selection (owned, pre-owned)
- Brand association selection
- Address entry with minimum two required
- Final confirmation and submission
-
Dealer List Page
- List of all registered dealers
- Search, sorting, and filtering options
- Dealer table displaying essential dealership info
- Quick actions such as edit/view
-
Dealer Details View
- Detailed profile of selected dealership
- Contact person list
- Registered brands overview
- All registered addresses
- Status indicators (active / disable)
-
Dealer Profile Update Page
- Edit basic and contact details
- Add/remove brands
- Add/update addresses
- Update logo, website, etc.
Database Schema
1. dealermaster Table
- Core columns used:
dealerId(primary identifier,uuid).dealerCode(unique text code, patternDL-00001, ...).primaryemailcompanyNameownerNamephonewebsitedealershipType(text array; mapped toDealershipTypeenum).brand(text array; mapped toBrandenum).status(text; mapped toStatusenum).dealerLogoUrlcreatedAtupdatedAtlogUsernamelogDtsradius(used byUpdateDealerRadius).
2. dealeraddress Table
- Columns:
addressIddealerIdaddressType(text; mapped toAddressTypeenum).addresslinestatezipcodecountrylatitudelongituderadiusisprimarylogUsernamelogDts
3. zipcodedetails Table
-
Columns:
zipcodelatlng
-
Data Models:
- Dealer Related Models
public record Dealer
{
public Guid DealerId { get; set; }
public string? DealerCode { get; set; }
public required string PrimaryEmail { get; init; }
public required string CompanyName { get; init; }
public string? OwnerName { get; init; } // Changed from "ContactPersonName" to "OwnerName"
public required string Phone { get; init; }
public string? Website { get; init; }
public List<DealershipType>? DealershipType { get; init; }
public List<Brand>? Brand { get; init; }
public Status Status { get; set; } = Status.Active;
public string? DealerLogoUrl { get; set; }
public List<DealerAddress>? Addresses { get; set; } = new List<DealerAddress>();
public List<ContactPerson>? ContactPersons { get; set; } = new List<ContactPerson>(); // Newly added for Adding Managers Details
public List<DepartmentBusinessHours>? DepartmentBusinessHours { get; set; } = new List<DepartmentBusinessHours>(); // Newly added for Adding Department wise Business Hours
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
public string? LogUsername { get; set; }
public DateTimeOffset LogDts { get; set; } = DateTimeOffset.UtcNow;
}
public record DealerAddress
{
public Guid AddressId { get; set; }
public Guid DealerId { get; set; }
public AddressType AddressType { get; init; }
public string? AddressLine { get; init; }
public string? State { get; init; }
public string? Zipcode { get; init; }
public string? Country { get; init; }
public double? Latitude { get; set; }
public double? Longitude { get; set; }
public double? Radius { get; set; }
public bool IsPrimary { get; set; }
}
public record DealerFilter
{
public string? DealerCode { get; set; }
public string? CompanyName { get; set; }
public string? OwnerName { get; set; }
public string? PrimaryEmail { get; set; }
public string? Phone { get; set; }
public List<Status>? Status { get; set; }
public List<DealershipType>? DealershipTypes { get; set; }
public List<Brand>? Brands { get; set; }
public string? Website { get; set; }
public DateTimeOffset? CreatedAtFrom { get; set; }
public DateTimeOffset? CreatedAtTo { get; set; }
public DateTimeOffset? UpdatedAtFrom { get; set; }
public DateTimeOffset? UpdatedAtTo { get; set; }
public string? SearchKeyword { get; set; }
public DealerSortBy SortBy { get; set; } = DealerSortBy.DealerCode;
public SortOrder SortDirection { get; set; } = SortOrder.Descending;
public int PageNumber { get; set; } = 1;
public int RowsPerPage { get; set; } = 10;
}
public class UpdateDealerRadiusRequest
{
public required decimal Radius { get; set; }
}
public record DealerCreationResponse : CommonResponse
{
public string? DealerCode { get; init; }
public Guid? DealerId { get; init; }
} -
API Interfaces:
(Dealer endpoints implemented in
DealerControllerwith methods, parameters, and response types.)
| Endpoint | Method | Parameters | Response | Response Status Codes |
|---|---|---|---|---|
/create | POST | Body: Dealer | CommonResponse / DealerCreationResponse | 201, 400, 401, 403, 404, 500 |
/list | GET | Query: DealerFilter | ServerPaginatedData<Dealer> | 200, 400, 401, 403, 500 |
/details/{dealerId} | GET | Route: Guid dealerId | Dealer | 200, 401, 404, 500 |
/update/{dealerId} | PUT | Route: Guid dealerId, Body: Dealer | CommonResponse | 200, 400, 401, 403, 404, 409, 500 |
/address-suggestion | GET | Query: Guid? dealerId (admin only) | List<DealerAddress> | 200, 400, 401, 403, 404, 500 |
/update-radius | PUT | Body: UpdateDealerRadiusRequest | CommonResponse | 200, 400, 401, 403, 404, 500 |
API Request & Response Examples
Below are JSON examples derived directly from the C# models and controller behavior.
1. Create Dealer
Endpoint: POST /create
Request Body (Dealer):
{
"dealerId": "00000000-0000-0000-0000-000000000000",
"dealerCode": null,
"primaryEmail": "contact@dealer.com",
"companyName": "Best Dealer Ltd",
"ownerName": "John Doe",
"phone": "+1-555-0100",
"website": "https://dealer.example.com",
"dealershipType": [ "New" ],
"brand": [ "Toyota", "Honda" ],
"status": "Active",
"dealerLogoUrl": "https://example.com/logo.png",
"addresses": [
{
"addressId": "00000000-0000-0000-0000-000000000001",
"dealerId": "00000000-0000-0000-0000-000000000000",
"addressType": "Legal",
"addressLine": "123 Legal St",
"state": "CA",
"zipcode": "90001",
"country": "USA",
"latitude": null,
"longitude": null,
"radius": 50.0,
"isPrimary": true
},
{
"addressId": "00000000-0000-0000-0000-000000000002",
"dealerId": "00000000-0000-0000-0000-000000000000",
"addressType": "Billing",
"addressLine": "456 Billing Ave",
"state": "CA",
"zipcode": "90002",
"country": "USA",
"latitude": null,
"longitude": null,
"radius": 25.0,
"isPrimary": false
}
],
"ContactPersons": [
{
"ContactId": "00000000-0000-0000-0000-000000002222",
"DealerId": "00000000-0000-0000-0000-0000000033333",
"FirstName": "ALex",
"LastName": "Net",
"Phone": "9988776651",
"Email": "gm@gmail.com",
"IsUser": false,
"Designation": "General Manager"
},
{
"ContactId": "00000000-0000-0000-0000-000000002222",
"DealerId": "00000000-0000-0000-0000-0000000033333",
"FirstName": "ALex",
"LastName": "Net",
"Phone": "9988776651",
"Email": "gm@gmail.com",
"IsUser": true,
"Designation": "Billing Manager"
}
],
"DepartmentBusinessHours": [
{
"DeptName": "Sales",
"DealerId": "00000000-0000-0000-0000-000000000000",
"MonFrom": 09:30:00,
"MonTo": 09:30:00,
"TueFrom": 09:30:00,
"TueTo": 09:30:00,
"WedFrom": 09:30:00,
"WedTo": 09:30:00,
"ThuFrom": 09:30:00,
"ThuTo": 09:30:00,
"FriFrom": 09:30:00,
"FriTo": 09:30:00,
"SatFrom": 09:30:00,
"SatTo": 09:30:00,
"SunFrom": 09:30:00,
"SunTo": 09:30:00,
},
{
"DeptName": "Parts",
"DealerId": "00000000-0000-0000-0000-000000000000",
"MonFrom": 09:30:00,
"MonTo": 09:30:00,
"TueFrom": 09:30:00,
"TueTo": 09:30:00,
"WedFrom": 09:30:00,
"WedTo": 09:30:00,
"ThuFrom": 09:30:00,
"ThuTo": 09:30:00,
"FriFrom": 09:30:00,
"FriTo": 09:30:00,
"SatFrom": 09:30:00,
"SatTo": 09:30:00,
"SunFrom": 09:30:00,
"SunTo": 09:30:00,
}
]
}
Success Response Body (DealerCreationResponse):
{
"status": 200,
"message": "Dealer created successfully",
"dealerCode": "DL-00001",
"dealerId": "11111111-1111-1111-1111-111111111111"
}
Error Response Body (CommonResponse example):
{
"status": 400,
"message": "Exactly one legal address and one billing address are required."
}
2. List Dealers
Endpoint:
GET /list?pageNumber=1&rowsPerPage=10&dealerCode=DL-00001&status=Active
Query Parameters: Map to DealerFilter.
Response Body (ServerPaginatedData<Dealer>):
{
"data": [
{
"dealerId": "11111111-1111-1111-1111-111111111111",
"dealerCode": "DL-00001",
"primaryEmail": "contact@dealer.com",
"companyName": "Best Dealer Ltd",
"ownerName": "John Doe",
"phone": "+1-555-0100",
"website": "https://dealer.example.com",
"dealershipType": [ "New" ],
"brand": [ "Toyota" ],
"status": "Active",
"dealerLogoUrl": "https://example.com/logo.png",
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-02T12:00:00Z",
"addresses": [
{
"addressId": "22222222-2222-2222-2222-222222222222",
"dealerId": "11111111-1111-1111-1111-111111111111",
"addressType": "Legal",
"addressLine": "123 Legal St",
"state": "CA",
"zipcode": "90001",
"country": "USA",
"latitude": 34.0522,
"longitude": -118.2437,
"radius": 50.0,
"isPrimary": true
}
],
"ContactPersons": [
{
"ContactId": "00000000-0000-0000-0000-000000002222",
"DealerId": "00000000-0000-0000-0000-0000000033333",
"FirstName": "Jhon",
"LastName": "Doe",
"Phone": "9988776651",
"Email": "gm@gmail.com",
"IsUser": false,
"Designation": "General Manager"
},
{
"ContactId": "00000000-0000-0000-0000-000000002222",
"DealerId": "00000000-0000-0000-0000-0000000033333",
"FirstName": "Alison",
"LastName": "Drew",
"Phone": "9988274651",
"Email": "gm2@gmail.com",
"IsUser": true,
"Designation": "Billing Manager"
}
],
"DepartmentBusinessHours": [
{
"DeptName": "Sales",
"DealerId": "00000000-0000-0000-0000-000000000000",
"MonFrom": 09:30:00,
"MonTo": 09:30:00,
"TueFrom": 09:30:00,
"TueTo": 09:30:00,
"WedFrom": 09:30:00,
"WedTo": 09:30:00,
"ThuFrom": 09:30:00,
"ThuTo": 09:30:00,
"FriFrom": 09:30:00,
"FriTo": 09:30:00,
"SatFrom": 09:30:00,
"SatTo": 09:30:00,
"SunFrom": 09:30:00,
"SunTo": 09:30:00,
},
{
"DeptName": "Parts",
"DealerId": "00000000-0000-0000-0000-000000000000",
"MonFrom": 09:30:00,
"MonTo": 09:30:00,
"TueFrom": 09:30:00,
"TueTo": 09:30:00,
"WedFrom": 09:30:00,
"WedTo": 09:30:00,
"ThuFrom": 09:30:00,
"ThuTo": 09:30:00,
"FriFrom": 09:30:00,
"FriTo": 09:30:00,
"SatFrom": 09:30:00,
"SatTo": 09:30:00,
"SunFrom": 09:30:00,
"SunTo": 09:30:00,
}
]
}
],
"totalNumber": 1,
"hasPreviousPage": false,
"hasNextPage": false,
"totalPages": 1,
"pageNumber": 1,
"rowsPerPage": 10
}
3. Get Dealer Details
Endpoint: GET /details/{dealerId}
Response Body (Dealer):
{
"dealerId": "11111111-1111-1111-1111-111111111111",
"dealerCode": "DL-00001",
"primaryEmail": "contact@dealer.com",
"companyName": "Best Dealer Ltd",
"ownerName": "John Doe",
"phone": "+1-555-0100",
"website": "https://dealer.example.com",
"dealershipType": [ "New" ],
"brand": [ "Toyota" ],
"status": "Active",
"dealerLogoUrl": "https://example.com/logo.png",
"createdAt": "2025-01-01T10:00:00Z",
"updatedAt": "2025-01-02T12:00:00Z",
"addresses": [
{
"addressId": "22222222-2222-2222-2222-222222222222",
"dealerId": "11111111-1111-1111-1111-111111111111",
"addressType": "Legal",
"addressLine": "123 Legal St",
"state": "CA",
"zipcode": "90001",
"country": "USA",
"latitude": 34.0522,
"longitude": -118.2437,
"radius": 50.0,
"isPrimary": true
}
]
}
4. Update Dealer
Endpoint: PUT /update/{dealerId}
Request Body (Dealer – partial update example):
{
"dealerId": "11111111-1111-1111-1111-111111111111",
"primaryEmail": "new-contact@dealer.com",
"companyName": "Best Dealer Ltd",
"phone": "+1-555-0200",
"website": "https://dealer-updated.example.com",
"addresses": [
{
"addressId": "22222222-2222-2222-2222-222222222222",
"dealerId": "11111111-1111-1111-1111-111111111111",
"addressType": "Legal",
"addressLine": "123 Legal St, Suite 200",
"state": "CA",
"zipcode": "90001",
"country": "USA",
"latitude": null,
"longitude": null,
"radius": 50.0,
"isPrimary": true
}
]
}
Success Response Body (CommonResponse):
{
"status": 200,
"message": "Dealer updated successfully"
}
5. Get Address Suggestions
Endpoint:
GET /address-suggestion?dealerId=11111111-1111-1111-1111-111111111111
Response Body (List<DealerAddress>):
[
{
"addressId": "22222222-2222-2222-2222-222222222222",
"dealerId": "11111111-1111-1111-1111-111111111111",
"addressType": "Legal",
"addressLine": "123 Legal St",
"state": "CA",
"zipcode": "90001",
"country": "USA",
"latitude": 34.0522,
"longitude": -118.2437,
"radius": 50.0,
"isPrimary": true
}
]
6. Update Dealer Radius
Endpoint: PUT /update-radius
Request Body (UpdateDealerRadiusRequest):
{
"radius": 150.5
}
Success Response Body (CommonResponse):
{
"status": 200,
"message": "Dealer radius updated successfully"
}
Important Notes for Frontend Developers
-
Models: All request and response bodies are based directly on the C# models in
ContractLibrary.DealerandContractLibrary.Common. -
GUID Format: All IDs are
Guidin C# and serialized as standard GUID strings like"11111111-1111-1111-1111-111111111111". -
Pagination Structure: List endpoints use
ServerPaginatedData<T>with the following JSON shape:{
"data": [],
"totalNumber": 0,
"hasPreviousPage": false,
"hasNextPage": false,
"totalPages": 0,
"pageNumber": 1,
"rowsPerPage": 10
} -
Address Constraints: The backend enforces:
- Exactly one
Legaland oneBillingaddress per dealer. - Legal and billing addresses cannot be deleted or duplicated.
- Exactly one
Data Relationships
dealermaster(1) ← (M)dealeraddress: One dealer can have multiple addresses.
Business Logic
Dealer Lifecycle
- Creation: Admin creates dealer with required data (Addresses, Contact Person details, Business Hours).
- Onboarding: Primary user is created and welcome notification is sent.
- Dealers and Managers can activate their profile from mail.
- Listing & Search: Admin lists dealers using filters and pagination.
- Update: Admin or dealer updates dealer profile and addresses with validation.
- Radius Management: Dealer radius is updated and stored on dealer record.
Address Rules
- Exactly one legal and one billing address must exist for each dealer.
- Optional additional addresses (e.g.,
Shipping,Branch) can be added or removed.
Authorization Rules
- Admin-only operations:
- Dealer creation.
- Viewing full dealer list.
- Dealer-specific restrictions:
- Dealers can only update their own dealer record, enforced via
entity_idclaim.
- Dealers can only update their own dealer record, enforced via
Third-Party Integrations
- RabbitMQ / MassTransit: Used for:
- User creation/update requests to Auth services.
- Batch notification publishing (
BatchNotificationRequest).
- Notification Service: Sends welcome and activation emails for new dealers.
- Geolocation: Used by
IGeolocationServiceto enrich addresses with latitude/longitude.
Workflow
Dealer Creation Workflow
- Admin calls
POST /createwith dealer, contact persons, business hours and address data. - Service validates input and checks for duplicate email/phone.
- Unique dealer code is generated and dealer is saved.
- Addresses are validated and saved with geolocation.
- Contact Person Details are saved.
- Contact Person are also saved as user if
IsUserfield istrue. - Business Hours for each department is saved.
- Primary user is created.
- Welcome notification is published.
Dealer Update Workflow
- Admin or dealer calls
PUT /update/{dealerId}. - Service validates authorization (including
entity_idfor dealers). - Dealer core data and addresses are validated.
- Dealer and address changes are applied in a transaction.
- If primary email/phone changed, user is updated.
Radius Update Workflow
- Dealer calls
PUT /update-radiuswith new radius. - Service validates user type and entity ID, and radius range.
- Dealer radius is updated in
dealermaster. - Audit fields are updated.
Testing & Quality Assurance
Unit Tests Exists for:
- Dealer Controllers
- Dealer Helpers
Integration Test Scenarios:
- Dealer creation with valid and invalid data
- Dealer listing with various filters and pagination
- Dealer update by admin and dealer roles
- Dealer radius update with valid and invalid values (1-1000 KM)
- Dealer radius update only by dealer role
- Address suggestion for admin and dealer
- Unauthorized and forbidden access attempts
- Error handling for missing/invalid claims
Testing Tools
- xUnit - Unit and integration testing
- FluentAssertions - Test assertions
- Moq - Mocking dependencies
- WebApplicationFactory - API integration tests
- Postman/Swagger - Manual API testing
Acceptance Criteria
- Dealer creation fails when:
- Duplicate email/phone exists.
- Exactly one legal and billing address are not provided.
- Dealer listing returns correct pagination metadata and honors all filters.
- Dealer update enforces address and permission rules.
- Radius update is allowed only for valid users and within valid range.
Feature Complete When:
- ✅ Admins can create, update, and list dealers
- ✅ Dealers can update their own information
- ✅ Dealers can update their service radius (validated 1-1000 KM, dealer-only access)
- ✅ All endpoints enforce correct access control
- ✅ Pagination and filtering work as expected
- ✅ Address suggestions are accurate and secure
- ✅ All error cases return correct status and message
- ✅ Unit test coverage >85%
- ✅ All integration tests pass
Deployment Considerations
Database Migration
- Tables
dealermaster,dealeraddress, andzipcodedetailsmust exist with columns used byDealerDAL. - Any new columns used by this service (e.g.,
radiusondealermaster) must be added before deploying updated code.
Monitoring & Alerts
- Monitor:
- Dealer creation and update error logs (
DealerHelper,DealerDAL). - Failed geolocation lookups and zipcode insert failures.
- MassTransit publish failures for notifications.
- Dealer creation and update error logs (
Configuration Changes
- Ensure database schema for dealers and addresses is up to date
- Configure JWT authentication and required claims in IAMService
- Update Swagger documentation
Security Considerations
Authorization
- All
DealerControllerendpoints require authenticated JWT tokens. - Scopes (
Right.dealer_create,Right.dealer_view_all,Right.dealer_view,Right.dealer_update) are enforced via attributes and middleware. - Additional checks based on:
user_typeclaim (Admin vs Dealer).entity_idclaim (dealer-specific access control).
Data Protection
- Dealer email and phone are central identifiers; duplicates are explicitly checked to avoid conflicts.
- Audit fields (
logUsername,logDts) are maintained on dealer and address records for traceability. - Zipcode geolocation is cached and reused, minimizing calls to external services.
Risks & Mitigations
| Risk | Impact | Likelihood | Mitigation Strategy |
|---|---|---|---|
| Incorrect address configuration (missing legal/billing) | High | Medium | Strict validation in helper methods and helpful error messages |
| Duplicate dealer email or phone | High | Medium | Duplicate checks before insert/update in DealerHelper |
| Geolocation failures for zipcodes | Medium | Medium | Warning logs and graceful degradation without blocking create/update |
| Auth or notification service failures | Medium | Low | Logging errors and isolating them from core DB transactions |
| Misconfigured JWT or scopes | High | Low | Centralized auth configuration in Program.cs and middleware |
| Unauthorized access to endpoints | High | Medium | Strict JWT and claim validation, logging, and auditing |
| Data inconsistency on update | Medium | Low | Use transactions and validation in business logic |
Review & Approval
-
Reviewer: Sanket
-
Approval Date: 2025-11-17
Notes
Future Enhancements
- Bulk dealer import/export
- Dealer status change notifications
- Dealer audit log endpoints
- Advanced address validation and enrichment
Dependencies on Other Services
- IAMService: For authentication and user claims
- CommonLibrary/ContractLibrary: For shared models and enums