Skip to main content
Version: MarketPulse

Dealer Management

Authors

  • Sanket Mal

Last Updated Date

2026-02-24


SRS References


Version History

VersionDateChangesAuthor
1.02026-02-24Initial draft - Dealer onboarding with scraping configurationSanket Mal

Feature Overview

Objective:
Implement a comprehensive dealer management system that enables resellers to onboard, configure, and manage dealers on the Market Pulse platform. The system supports multi-step dealer setup including identity, geography targeting, vehicle filtering, scraping quality controls, and source scheduling — all within a single API call.

Scope:

  • Dealer CRUD operations with full scraping configuration
  • Multi-step onboarding wizard support (identity → geography → vehicle → keywords → quality → sources → delivery)
  • Group head hierarchy for dealer organizations
  • Server-side pagination and advanced filtering for dealer listings
  • Per-source scraping schedule configuration (hourly, daily, weekly, monthly)
  • Soft delete support for dealers

Dependencies:

  • ManagementService - Hosts the Dealer API endpoints
  • DatabaseService - PostgreSQL 14+ schema and migrations
  • CommonLibrary - Shared extensions and utilities
  • ContractLibrary - C# record models and enums
  • MassTransit/RabbitMQ - For event-driven scraping triggers

Requirements

Functional Requirements

  1. Dealer Onboarding Requirements

    • Resellers must be able to create a new dealer with all scraping configuration in a single API call
    • Dealer identity must include name, code, timezone, logo, and group head association
    • Each dealer must belong to exactly one reseller (FK to ResellerMaster)
    • Dealers may optionally be designated as a group head or linked to an existing group head
    • Dealer status must default to Active and support PendingActivation and Disable states
    • Default timezone must be US/Central
  2. Geography Setting Requirements

    • Each dealer must have one geography configuration (one-to-one with DealerMaster)
    • Geography mode must support: Radius, ZipList, DMA, States, Cities
    • When mode is Radius, centerZipCode and radiusMiles are required
    • For list-based modes, geoValues array contains the selected entries
  3. Vehicle Setting Requirements

    • Each dealer must have one vehicle filtering configuration
    • Vehicle filters include body types, makes, year range, and price range
    • Year range constraint: maxYear >= minYear
    • Price range constraint: maxPrice >= minPrice
    • Default currency is USD
  4. Scraping Configuration Requirements

    • Each dealer must have one scraping config for quality controls
    • Support include/exclude keyword lists for relevance filtering
    • Confidence preset must support Conservative, Balanced, Aggressive
    • Default confidence threshold is 0.70
    • Daily record cap and data retention days must be configurable
  5. Scraping Source Requirements

    • Dealers can be linked to multiple scraping sources (many-to-many via junction table)
    • Each dealer-source link has its own schedule (frequency type, value, day of week/month)
    • Scraping mode per source: Manual or Auto
    • Source-level status: Active, Paused, Suspended
    • Scheduling supports: Hourly, Daily, Weekly, Monthly intervals
  6. Dealer Listing & Detail Requirements

    • Paginated dealer listing with server-side filtering (search keyword, name, code, reseller, group head, status, timezone)
    • Listing returns minimal info: identity, status, active source count, reseller info, timestamps
    • Detail endpoint returns complete dealer data including all nested scraping objects
    • Support for searchKeyword that searches across multiple fields simultaneously
  7. Dealer Update Requirements

    • Full update of dealer identity and all scraping sub-objects via PUT
    • Only provided fields are updated (partial update semantics)
    • Nested objects (geography, vehicle, config, sources) are replaced when provided

Non-Functional Requirements

  1. Performance

    • API response time must be under 500ms for list endpoints
    • API response time must be under 300ms for create/update operations
    • Support pagination for large datasets (1000+ dealers)
    • Efficient database queries with proper indexing
  2. Security

    • All endpoints must require Bearer token authentication
    • Resellers must only manage dealers within their own reseller scope
    • Input validation and sanitization on all fields
    • Rate limiting on create/update endpoints
  3. Scalability

    • Database must handle thousands of dealers with millions of scraping records
    • Server-side pagination for efficient data retrieval
    • Indexed queries on all filter fields
  4. Reliability

    • 99.9% uptime for dealer management APIs
    • Transactional writes across dealer + sub-tables (all-or-nothing)
    • Soft delete to prevent accidental data loss
    • Data consistency across all related tables
  5. Usability

    • Single API call for full dealer onboarding (no multi-step API sequence required)
    • Sensible defaults for all optional fields
    • Clear validation error messages with field-level detail

Design Specifications

UI/UX Design

Dealer Onboarding Wizard (7 Steps):

  • Step 1: Dealer Identity (name, code, reseller, group head, timezone, logo)
  • Step 2: Geography Setting (mode selection, zip/radius/list configuration)
  • Step 3: Vehicle Setting (body types, makes, year range, price range)
  • Step 4: Include Keywords (prioritize specific terms in scraping)
  • Step 5: Exclude Keywords (filter out unwanted terms)
  • Step 6: Source Selection & Scheduling (pick sources, set frequency per source)
  • Step 7: Quality Controls & Delivery (confidence preset, threshold, daily cap, retention)

Dealer List View:

  • Server-paginated table with sortable columns
  • Filter bar: search keyword, reseller, status, group head
  • Each row shows: dealer name, code, status, active sources count, reseller, timezone
  • Click row to open full detail view

Dealer Detail View:

  • Tabbed layout showing all configuration sections
  • Edit mode toggles inline editing
  • Save triggers PUT with changed sections only

Data Models

Database Schema

-- DealerMaster Table
CREATE TABLE IF NOT EXISTS DealerMaster (
dealerId UUID PRIMARY KEY DEFAULT gen_random_uuid(),
dealerName VARCHAR(255) NOT NULL,
resellerId UUID NOT NULL,
isgrouphead BOOLEAN,
groupheadid UUID,
timezone VARCHAR(50) NOT NULL DEFAULT 'US/Central',
notes TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'Active',
DealerLogoUrl VARCHAR(200),
createdAt TIMESTAMPTZ NOT NULL DEFAULT now(),
createdBy UUID NOT NULL,
updatedAt TIMESTAMPTZ NOT NULL DEFAULT now(),
updatedBy UUID,
isDeleted BOOLEAN NOT NULL DEFAULT false,
deletedAt TIMESTAMPTZ,
deletedBy UUID,

CONSTRAINT chkStatus CHECK (status IN ('Active', 'Disable'))
);

CREATE INDEX IF NOT EXISTS idxDealerReseller ON DealerMaster(resellerId);
CREATE INDEX IF NOT EXISTS idxDealerStatus ON DealerMaster(status) WHERE isDeleted = false;

ALTER TABLE DealerMaster
ADD CONSTRAINT fkReseller FOREIGN KEY (resellerId) REFERENCES ResellerMaster(resellerId);

-- DealerAddress Table
CREATE TABLE IF NOT EXISTS DealerAddress (
addressId UUID PRIMARY KEY,
dealerId UUID NOT NULL,
addressType VARCHAR(50) NOT NULL,
addressLine VARCHAR(100),
state VARCHAR,
zipCode VARCHAR,
country VARCHAR,
logUserName VARCHAR(50),
logDts TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
radius DOUBLE PRECISION,
isPrimary BOOLEAN DEFAULT false,

CONSTRAINT fkDealerAddress FOREIGN KEY (dealerId) REFERENCES DealerMaster(dealerId) ON DELETE NO ACTION
);

CREATE INDEX IF NOT EXISTS idxDealerAddressDealer ON DealerAddress(dealerId);

-- GeographySettingForScraping Table
CREATE TABLE IF NOT EXISTS GeographySettingForScraping (
geographySettingId UUID PRIMARY KEY DEFAULT gen_random_uuid(),
dealerId UUID NOT NULL UNIQUE,
geoMode VARCHAR(20) NOT NULL,
centerZipCode VARCHAR(10),
radiusMiles INTEGER,
geoValues TEXT[],
createdAt TIMESTAMPTZ NOT NULL DEFAULT now(),
createdBy UUID NOT NULL,
updatedAt TIMESTAMPTZ NOT NULL DEFAULT now(),
updatedBy UUID,

CONSTRAINT fkDealerGeo FOREIGN KEY (dealerId) REFERENCES DealerMaster(dealerId) ON DELETE CASCADE,
CONSTRAINT chkGeoMode CHECK (geoMode IN ('Radius', 'ZipList', 'DMA', 'States', 'Cities')),
CONSTRAINT chkRadiusZip CHECK (
(geoMode = 'Radius' AND centerZipCode IS NOT NULL AND radiusMiles IS NOT NULL)
OR (geoMode != 'Radius')
)
);

CREATE INDEX IF NOT EXISTS idxGeographyDealer ON GeographySettingForScraping(dealerId);

-- VehicleSettingForScraping Table
CREATE TABLE IF NOT EXISTS VehicleSettingForScraping (
vehicleSettingId UUID PRIMARY KEY DEFAULT gen_random_uuid(),
dealerId UUID NOT NULL UNIQUE,
bodyTypes TEXT[],
makes TEXT[],
minYear INTEGER NOT NULL DEFAULT 2016,
maxYear INTEGER NOT NULL DEFAULT 2026,
minPrice DECIMAL(10, 2) NOT NULL DEFAULT 5000.00,
maxPrice DECIMAL(10, 2) NOT NULL DEFAULT 75000.00,
currency VARCHAR(10) NOT NULL DEFAULT 'USD',
createdAt TIMESTAMPTZ NOT NULL DEFAULT now(),
createdBy UUID NOT NULL,
updatedAt TIMESTAMPTZ NOT NULL DEFAULT now(),
updatedBy UUID,

CONSTRAINT fkDealerVehicle FOREIGN KEY (dealerId) REFERENCES DealerMaster(dealerId) ON DELETE CASCADE,
CONSTRAINT chkYearRange CHECK (maxYear >= minYear),
CONSTRAINT chkPriceRange CHECK (maxPrice >= minPrice)
);

CREATE INDEX IF NOT EXISTS idxVehicleDealer ON VehicleSettingForScraping(dealerId);

-- DealerScrapingConfig Table
CREATE TABLE IF NOT EXISTS DealerScrapingConfig (
scrapingConfigId UUID PRIMARY KEY DEFAULT gen_random_uuid(),
dealerId UUID NOT NULL UNIQUE,
includeKeywords TEXT[],
excludeKeywords TEXT[],
confidencePreset VARCHAR(30) NOT NULL DEFAULT 'Balanced',
confidenceThreshold DECIMAL(3, 2) NOT NULL DEFAULT 0.70,
dailyRecordCap INTEGER NOT NULL DEFAULT 200,
dataRetentionDays INTEGER NOT NULL DEFAULT 90,
createdAt TIMESTAMPTZ NOT NULL DEFAULT now(),
createdBy UUID NOT NULL,
updatedAt TIMESTAMPTZ NOT NULL DEFAULT now(),
updatedBy UUID,

CONSTRAINT fkDealerConfig FOREIGN KEY (dealerId) REFERENCES DealerMaster(dealerId) ON DELETE CASCADE,
CONSTRAINT chkConfidencePreset CHECK (confidencePreset IN ('Conservative', 'Balanced', 'Aggressive'))
);

CREATE INDEX IF NOT EXISTS idxScrapingConfigDealer ON DealerScrapingConfig(dealerId);

-- ScrapingSource Table (Master)
CREATE TABLE IF NOT EXISTS ScrapingSource (
sourceId UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sourceName VARCHAR(50) NOT NULL UNIQUE,
status VARCHAR(20) NOT NULL DEFAULT 'Active',
createdAt TIMESTAMPTZ NOT NULL DEFAULT now(),
createdBy UUID NOT NULL,
updatedAt TIMESTAMPTZ NOT NULL DEFAULT now(),
updatedBy UUID,
disabledAt TIMESTAMPTZ,
disabledBy UUID,

CONSTRAINT chkSourceStatus CHECK (status IN ('Active', 'Disable'))
);

CREATE INDEX IF NOT EXISTS idxSourceStatus ON ScrapingSource(status);

-- DealerScrapingSources Table (Junction)
CREATE TABLE IF NOT EXISTS DealerScrapingSources (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
dealerId UUID NOT NULL,
sourceId UUID NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'Active',
scrapeMode VARCHAR(20) NOT NULL DEFAULT 'Manual',
frequencymode VARCHAR(20),
frequencyValue INTEGER,
dayOfWeek INTEGER,
dayOfMonth INTEGER,
lastScrapedAt TIMESTAMPTZ,
createdAt TIMESTAMPTZ NOT NULL DEFAULT now(),
createdBy UUID NOT NULL,
updatedAt TIMESTAMPTZ NOT NULL DEFAULT now(),
updatedBy UUID NOT NULL,

CONSTRAINT fkDealerSources FOREIGN KEY (dealerId) REFERENCES DealerMaster(dealerId) ON DELETE CASCADE,
CONSTRAINT fkSourceSources FOREIGN KEY (sourceId) REFERENCES ScrapingSource(sourceId) ON DELETE CASCADE,
CONSTRAINT uqDealerSource UNIQUE (dealerId, sourceId)
);

CREATE INDEX IF NOT EXISTS idxDealerSourcesDealer ON DealerScrapingSources(dealerId);
CREATE INDEX IF NOT EXISTS idxDealerSourcesSource ON DealerScrapingSources(sourceId);
CREATE INDEX IF NOT EXISTS idxDealerSourcesEnabled ON DealerScrapingSources(dealerId, status);

C# Contract Models

namespace ContractLibrary.Dealer;

// ============================================================
// Enums
// ============================================================
public enum GeoMode
{
Radius = 1,
ZipList = 2,
DMA = 3,
States = 4,
Cities = 5
}

public enum ConfidencePreset
{
Conservative = 1,
Balanced = 2,
Aggressive = 3
}

public enum FrequencyMode
{
HourInterval = 1,
DayInterval = 2,
WeekInterval = 3,
MonthInterval = 4
}

public enum ScrapeMode
{
Manual = 1,
Auto = 2
}

public enum Status
{
PendingActivation = 1,
Active = 2,
Disable = 3,
}

public enum ScrapingConfigurationStatus
{
Active = 1,
Paused = 2,
Suspended = 3
}
public enum Day
{
Sunday = 0,
Monday = 1,
Tuesday = 2,
Wednesday = 3,
Thursday = 4,
Friday = 5,
Saturday = 6
}

// Request Models
public record Dealer
{
public Guid? DealerId { get; init; } = Guid.NewGuid();
public required string DealerName { get; init; }
public required Guid ResellerId { get; init; }
public bool? IsGroupHead { get; init; }
public Guid? GroupHeadId { get; init; }
public string Timezone { get; init; } = "US/Central";
public Status Status { get; init; } = Status.Active;
public string? DealerLogoUrl { get; init; }
public GeographySetting? GeographySetting { get; init; }
public VehicleSetting? VehicleSetting { get; init; }
public ScrapingConfig? ScrapingConfig { get; init; }
public List<DealerScrapingSource> ScrapingSources { get; init; } = [];
}
public record DealerDetailResponse : Dealer
{
public string DealerCode { get; init; } = string.Empty;
public string? GroupHeadName { get; init; }
public string? GroupHeadCode { get; init; }
public DateTime CreatedAt { get; init; }
public Guid CreatedBy { get; init; }
public DateTime UpdatedAt { get; init; }
public Guid? UpdatedBy { get; init; }
}
public record GeographySetting
{
public Guid? GeographySettingId { get; init; } = Guid.NewGuid();
public GeoMode? GeoMode { get; init; }
public string? CenterZipCode { get; init; }
public int? RadiusMiles { get; init; }
public List<string>? GeoValues { get; init; }
}

public record VehicleSetting
{
public Guid VehicleSettingId { get; init; } = Guid.NewGuid();
public List<string>? BodyTypes { get; init; }
public List<string>? Makes { get; init; }
public int? MinYear { get; init; }
public int? MaxYear { get; init; }
public decimal? MinPrice { get; init; }
public decimal? MaxPrice { get; init; }
public string? Currency { get; init; } = "USD";
}

public record ScrapingConfig
{
public Guid? ScrapingConfigId { get; init; } = Guid.NewGuid();
public List<string>? IncludeKeywords { get; init; }
public List<string>? ExcludeKeywords { get; init; }
public ConfidencePreset ConfidencePreset { get; init; } = ConfidencePreset.Balanced;
public decimal ConfidenceThreshold { get; init; } = 0.70m;
public int? DailyRecordCap { get; init; }
public int DataRetentionDays { get; init; } = 90;
}
public record ScrapingSource
{
public required Guid SourceId { get; init; }
public string? SourceName { get; init; }
public Status Status { get; init; } = Status.Active;

}

public record DealerScrapingSource : ScrapingSource
{
public Guid? DealerScrapingSourceId { get; init; } = Guid.NewGuid();
public ScrapingConfigurationStatus ScrapingConfigurationStatus { get; init; } = ScrapingConfigurationStatus.Active;
public ScrapeMode ScrapeMode { get; init; } = ScrapeMode.Manual;
public FrequencyMode? FrequencyMode { get; init; }
public int FrequencyValue { get; init; } = 1;
public Day? DayOfWeek { get; init; }
public int? DayOfMonth { get; init; }
public TimeOnly? StartTime { get; init; }
}
public record DealerMinimalInfo
{
public Guid DealerId { get; init; }
public string DealerName { get; init; } = string.Empty;
public string DealerCode { get; init; } = string.Empty;
public bool IsGroupHead { get; init; }
public Guid? GroupHeadId { get; init; }
public string? GroupHeadName { get; init; }
public string? GroupHeadCode { get; init; }
public Status Status { get; init; }
public int ActiveSourceCount { get; init; }
public Guid ResellerId { get; init; }
public string? ResellerName { get; init; } = string.Empty;
public string Timezone { get; init; } = string.Empty;
public DateTime CreatedAt { get; init; }
public DateTime UpdatedAt { get; init; }
}


public record DealerFilter
{
public string? SearchKeyword { get; init; }
public string? DealerName { get; init; }
public string? DealerCode { get; init; }
public Guid? ResellerId { get; init; }
public bool? IsGroupHead { get; init; }
public string? GroupHeadName { get; init; }
public string? GroupHeadCode { get; init; }
public string? Status { get; init; }
public string? Timezone { get; init; }
public int PageNumber { get; init; } = 1;
public int RowsPerPage { get; init; } = 10;
}

public record ServerPaginatedData<T>
{
public List<T> Data { get; init; } = [];
public int TotalNumber { get; init; }
public bool HasPreviousPage { get; init; }
public bool HasNextPage { get; init; }
public int TotalPages { get; init; }
public int PageNumber { get; init; }
public int RowsPerPage { get; init; }
}
public record CommonResponse
{
public int Status { get; init; }
public string? Message { get; init; }
}

API Interfaces

EndpointMethodParametersResponseResponse Status Codes
/api/v1/dealersPOSTDealerCommonResponse201, 400, 401, 500
/api/v1/dealers/{dealerId}PUTDealerId , DealerCommonResponse200, 400, 401, 404, 500
/api/v1/dealersGETDealerFilterCommonResponse with ServerPaginatedData<DealerMinimalInfo>200, 401, 500
/api/v1/dealers/{dealerId}GETdealerId (path)DealerDetailResponse200, 401, 404, 500
/api/v1/scraping-sourcesGETList<ScrapingSource>200, 401, 404, 500

API Request & Response Examples

1. Create Dealer API

Endpoint: POST /api/v1/dealers

Request Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

Request Body (Dealer):

{
"dealerName": "AutoMax Dallas",
"resellerId": "b2f7c1a0-3e5d-4f8a-9c1b-2d3e4f5a6b7c",
"isGroupHead": false,
"groupHeadId": null,
"timezone": "US/Central",
"status": "Active",
"dealerLogoUrl": "https://cdn.example.com/logos/automax.png",
"dealerCode": "AMX-001",
"geographySetting": {
"geoMode": "Radius",
"centerZipCode": "75201",
"radiusMiles": 50,
"geoValues": null
},
"vehicleSetting": {
"bodyTypes": ["sedan", "suv", "truck"],
"makes": ["Toyota", "Honda", "Ford"],
"minYear": 2018,
"maxYear": 2026,
"minPrice": 10000.00,
"maxPrice": 60000.00,
"currency": "USD"
},
"scrapingConfig": {
"includeKeywords": ["clean title", "low miles", "one owner"],
"excludeKeywords": ["salvage", "rebuilt", "flood"],
"confidencePreset": "Balanced",
"confidenceThreshold": 0.70,
"dailyRecordCap": 200,
"dataRetentionDays": 90
},
"scrapingSources": [
{
"sourceId": "c3d4e5f6-a7b8-9012-3456-789abcdef012",
"scrapingConfigurationStatus": "Active",
"scrapeMode": "Auto",
"FrequencyMode": "Daily",
"frequencyValue": 1,
"dayOfWeek": null,
"dayOfMonth": null
},
{
"sourceId": "d4e5f6a7-b8c9-0123-4567-890abcdef123",
"scrapingConfigurationStatus": "Active",
"scrapeMode": "Manual",
"FrequencyMode": "Weekly",
"frequencyValue": 1,
"dayOfWeek": 1,
"dayOfMonth": null
}
]
}

Success Response (201 Created - CommonResponse):

{
"status": 0,
"message": "Dealer created successfully.",
}

Error Response (400 Bad Request):

{
"status": -20000,
"message": "Validation failed."
}

2. Update Dealer API

Endpoint: PUT /api/v1/dealers/{dealerId}

Request Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

URL Parameters:

  • dealerId: a1b2c3d4-e5f6-7890-abcd-ef1234567890

Request Body (Dealer):

{
"dealerName": "AutoMax Dallas",
"resellerId": "b2f7c1a0-3e5d-4f8a-9c1b-2d3e4f5a6b7c",
"isGroupHead": false,
"groupHeadId": null,
"timezone": "US/Central",
"status": "Active",
"dealerLogoUrl": "https://cdn.example.com/logos/automax.png",
"dealerCode": "AMX-001",
"geographySetting": {
"geoMode": "Radius",
"centerZipCode": "75201",
"radiusMiles": 50,
"geoValues": null
},
"vehicleSetting": {
"bodyTypes": ["sedan", "suv", "truck"],
"makes": ["Toyota", "Honda", "Ford"],
"minYear": 2018,
"maxYear": 2026,
"minPrice": 10000.00,
"maxPrice": 60000.00,
"currency": "USD"
},
"scrapingConfig": {
"includeKeywords": ["clean title", "low miles", "one owner"],
"excludeKeywords": ["salvage", "rebuilt", "flood"],
"confidencePreset": "Balanced",
"confidenceThreshold": 0.70,
"dailyRecordCap": 200,
"dataRetentionDays": 90
},
"scrapingSources": [
{
"sourceId": "c3d4e5f6-a7b8-9012-3456-789abcdef012",
"scrapingConfigurationStatus": "Active",
"scrapeMode": "Auto",
"FrequencyMode": "Daily",
"frequencyValue": 1,
"dayOfWeek": null,
"dayOfMonth": null
},
{
"sourceId": "d4e5f6a7-b8c9-0123-4567-890abcdef123",
"scrapingConfigurationStatus": "Active",
"scrapeMode": "Manual",
"FrequencyMode": "Weekly",
"frequencyValue": 1,
"dayOfWeek": 1,
"dayOfMonth": null
}
]
}

Success Response (200 OK - CommonResponse):

{
"status": 0,
"message": "Dealer updated successfully."
}

Error Response (404 Not Found):

{
"status": -20000,
"message": "Dealer not found."
}

3. Get Dealers (Paginated List) API

Endpoint: GET /api/v1/dealers

Request Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Query Parameters (DealerFilter):

?pageNumber=1&rowsPerPage=10&status=Active&resellerId=b2f7c1a0-3e5d-4f8a-9c1b-2d3e4f5a6b7c

Success Response (200 OK - ServerPaginatedData<DealerMinimalInfo>):

{
"data": [
{
"dealerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"dealerName": "AutoMax Dallas",
"dealerCode": "AMX-001",
"isGroupHead": false,
"groupHeadId": null,
"groupHeadName": null,
"groupHeadCode": null,
"status": "Active",
"activeSourceCount": 2,
"resellerId": "b2f7c1a0-3e5d-4f8a-9c1b-2d3e4f5a6b7c",
"resellerName": "StockSource Reseller A",
"timezone": "US/Central",
"createdAt": "2026-02-24T10:30:00Z",
"updatedAt": "2026-02-24T14:15:00Z"
},
{
"dealerId": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
"dealerName": "QuickDrive Houston",
"dealerCode": "QDH-002",
"isGroupHead": true,
"groupHeadId": null,
"groupHeadName": null,
"groupHeadCode": null,
"status": "Active",
"activeSourceCount": 3,
"resellerId": "b2f7c1a0-3e5d-4f8a-9c1b-2d3e4f5a6b7c",
"resellerName": "StockSource Reseller A",
"timezone": "US/Central",
"createdAt": "2026-02-20T08:00:00Z",
"updatedAt": "2026-02-22T12:00:00Z"
}
],
"totalNumber": 25,
"hasPreviousPage": false,
"hasNextPage": true,
"totalPages": 3,
"pageNumber": 1,
"rowsPerPage": 2
}

4. Get Dealer Details API

Endpoint: GET /api/v1/dealers/{dealerId}

Request Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

URL Parameters:

  • dealerId: a1b2c3d4-e5f6-7890-abcd-ef1234567890

Success Response (200 OK - DealerDetailResponse):

{
"dealerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"dealerName": "AutoMax Dallas",
"dealerCode": "AMX-001",
"resellerId": "b2f7c1a0-3e5d-4f8a-9c1b-2d3e4f5a6b7c",
"isGroupHead": false,
"groupHeadId": null,
"groupHeadName": null,
"groupHeadCode": null,
"timezone": "US/Central",
"status": "Active",
"dealerLogoUrl": "https://cdn.example.com/logos/automax.png",
"createdAt": "2026-02-24T10:30:00Z",
"createdBy": "d4e5f6a7-b8c9-0123-4567-890abcdef012",
"updatedAt": "2026-02-24T14:15:00Z",
"updatedBy": "d4e5f6a7-b8c9-0123-4567-890abcdef012",
"geographySetting": {
"geographySettingId": "e5f6a7b8-c9d0-1234-5678-90abcdef0123",
"geoMode": "Radius",
"centerZipCode": "75201",
"radiusMiles": 50,
"geoValues": null
},
"vehicleSetting": {
"vehicleSettingId": "f6a7b8c9-d0e1-2345-6789-0abcdef01234",
"bodyTypes": ["sedan", "suv", "truck"],
"makes": ["Toyota", "Honda", "Ford"],
"minYear": 2018,
"maxYear": 2026,
"minPrice": 10000.00,
"maxPrice": 60000.00,
"currency": "USD"
},
"scrapingConfig": {
"scrapingConfigId": "a7b8c9d0-e1f2-3456-7890-abcdef012345",
"includeKeywords": ["clean title", "low miles", "one owner"],
"excludeKeywords": ["salvage", "rebuilt", "flood"],
"confidencePreset": "Balanced",
"confidenceThreshold": 0.70,
"dailyRecordCap": 200,
"dataRetentionDays": 90
},
"scrapingSources": [
{
"dealerScrapingSourceId": "b8c9d0e1-f2a3-4567-8901-bcdef0123456",
"sourceId": "c3d4e5f6-a7b8-9012-3456-789abcdef012",
"scrapingConfigurationStatus": "Active",
"scrapeMode": "Auto",
"FrequencyMode": "Daily",
"frequencyValue": 1,
"dayOfWeek": null,
"dayOfMonth": null
},
{
"dealerScrapingSourceId": "c9d0e1f2-a3b4-5678-9012-cdef01234567",
"sourceId": "d4e5f6a7-b8c9-0123-4567-890abcdef123",
"scrapingConfigurationStatus": "Active",
"scrapeMode": "Manual",
"FrequencyMode": "Weekly",
"frequencyValue": 1,
"dayOfWeek": 1,
"dayOfMonth": null
}
]
}

Third-Party Integrations

  • None required for dealer management APIs
  • Scraping sources (e.g., third-party marketplaces) are configured via ScrapingSource master table

Workflow

1. Reseller Onboards a New Dealer

1. Reseller logs into the Management Portal
2. Reseller navigates to Dealer Management → Create Dealer
3. Step 1: Reseller fills dealer identity (name, code, timezone, logo)
4. Step 2: Reseller configures geography targeting (mode, zip/radius/list)
5. Step 3: Reseller sets vehicle filters (body types, makes, year/price range)
6. Step 4: Reseller adds include keywords for scraping relevance
7. Step 5: Reseller adds exclude keywords to filter out unwanted results
8. Step 6: Reseller selects scraping sources and sets schedule per source
9. Step 7: Reseller configures quality controls (confidence, daily cap, retention)
10. Frontend collects all steps into a single DealerCreateRequest
11. POST /api/v1/dealer with full payload
12. System validates:
- ResellerId exists and is active
- GroupHeadId (if provided) exists and belongs to same reseller
- GeoMode constraints (Radius requires centerZipCode + radiusMiles)
- Year/price range constraints
- SourceIds exist and are active
13. System creates DealerMaster + all sub-tables in a single transaction
14. System returns 201 with full DealerDetailResponse
15. Dealer appears in the dealer list with status "Active"

2. Reseller Updates Dealer Configuration

1. Reseller clicks on a dealer from the paginated list
2. GET /api/v1/dealer/{dealerId} (load full dealer detail)
3. System returns DealerDetailResponse with all nested objects
4. Reseller modifies desired fields (e.g., change radius, add sources, update keywords)
5. Frontend sends only changed sections in the PUT request
6. PUT /api/v1/dealer/{dealerId} with partial payload
7. System validates changes and updates in a transaction
8. System returns 200 with updated DealerDetailResponse
9. Updated values appear immediately on detail reload

3. Reseller Searches and Filters Dealers

1. Reseller navigates to Dealer Management → Dealer List
2. GET /api/v1/dealer?pageNumber=1&rowsPerPage=10 (load first page)
3. System returns ServerPaginatedData<DealerMinimalInfo>
4. Reseller types in search bar → searchKeyword searches across:
- Dealer name, dealer code
- Group head name, group head code
- Reseller name
5. Reseller applies filters (status, reseller, group head, timezone)
6. Frontend re-fetches: GET /api/v1/dealer?searchKeyword=...&status=...&pageNumber=1
7. System returns filtered, paginated results
8. Reseller clicks next page → pageNumber=2
9. Reseller clicks a dealer row → navigates to detail view

4. Dealer Status Lifecycle

1. New dealer is created with status = "Active" (default)
2. Reseller can change status to "Disable" via PUT update
3. When dealer is disabled:
- Dealer no longer appears in active dealer listings (when filtered by Active)
- All scraping for this dealer is paused
- Dealer data is retained for historical reporting
4. Reseller can re-enable by setting status back to "Active"
5. Soft delete: sets isDeleted=true, retains all data
- Soft-deleted dealers are excluded from all queries (WHERE isDeleted = false)
- Can be restored by admin if needed

Development Tasks & Estimates

NoTask NameEstimate (Hours)DependenciesNotes
1Database Schema Design & Migration Script4 hoursNoneCreate all 9 tables with indexes, constraints, and FK relationships
2Feature Documentation4 hoursTask 1Document all request/response DTOs, enums, and setup ManagementService project structure
3Feature Planning & Technical Design6 hoursNoneArchitecture decisions, data flow, validation rules
4C# Contract Models (ContractLibrary)3 hoursTask 1Create all records, enums, and pagination wrapper in ContractLibrary/Dealer
5API: Create Dealer (POST /api/v1/dealer)8 hoursTask 4Multi-table transactional insert, validation, error handling, unit tests
6API: Update Dealer (PUT /api/v1/dealer/{dealerId})7 hoursTask 5Partial update logic, nested object replacement, validation, tests
7API: Get Dealers - Paginated List (GET /api/v1/dealer)6 hoursTask 4Server-side pagination, multi-field search, filtering, sorting, tests
8API: Get Dealer Details (GET /api/v1/dealer/{dealerId})4 hoursTask 4Join across all sub-tables, nested object mapping, authorization, tests
Grand Total42 hours

Testing & Quality Assurance

Integration Test Scenarios:

  • All API endpoints with valid and invalid requests
  • Authorization checks (resellers can only access their own dealers)
  • Pagination edge cases (first page, last page, empty results, single page)
  • Transactional integrity (partial failure during create rolls back all sub-tables)
  • Constraint validation (geo mode, year range, price range)
  • Search keyword across multiple fields
  • Group head hierarchy validation
  • Soft delete and status transitions

Testing Tools

  • xUnit - Unit and integration testing framework
  • FluentAssertions - Readable test assertions
  • Moq - Mocking framework for unit tests
  • WebApplicationFactory - Integration testing for APIs
  • Testcontainers - PostgreSQL container for integration tests
  • Bogus - Fake data generation for tests
  • Postman/Swagger - Manual API testing
  • SonarQube - Code quality and coverage analysis

Acceptance Criteria

Feature Complete When:

  1. ✅ Resellers can create a dealer with full scraping configuration in one API call
  2. ✅ Resellers can update any combination of dealer fields via PUT
  3. ✅ Dealer list supports server-side pagination with configurable page size
  4. ✅ Search keyword filters across dealer name, code, group head, and reseller
  5. ✅ All filter combinations work correctly (status, reseller, group head, timezone)
  6. ✅ Dealer detail returns complete nested data (geography, vehicle, config, sources)
  7. ✅ Group head hierarchy is correctly maintained and queryable
  8. ✅ Geo mode constraints are enforced (Radius requires zip + miles)
  9. ✅ Year and price range constraints are validated
  10. ✅ Transactional writes ensure all-or-nothing for create/update
  11. ✅ Soft delete excludes dealers from all queries
  12. ✅ Authorization ensures reseller-scoped access only
  13. ✅ All APIs respond within required time limits
  14. ✅ Unit test coverage > 85%
  15. ✅ All integration tests pass

Deployment Considerations

Configuration Changes

Database Migration:

  • Run migration script 001_CreateAllTables.sql
  • Verify all indexes are created for performance
  • Verify foreign key constraints are valid
  • Seed ScrapingSource master table with available marketplace sources

Risks & Mitigations

RiskImpactLikelihoodMitigation Strategy
Large payload size for create request (all sub-objects)MediumLowValidate payload size limits; implement request size middleware; consider chunked onboarding if needed
Complex multi-table transaction failuresHighLowUse database transactions with proper rollback; implement retry logic for transient failures
Search keyword performance on large datasetsMediumMediumAdd full-text search indexes; implement caching for frequent queries; monitor query plans
Group head circular referencesMediumLowValidate group head hierarchy on create/update; prevent self-referencing; limit hierarchy depth
Scraping source schedule conflictsLowMediumValidate schedule parameters (dayOfWeek 0-6, dayOfMonth 1-31); warn on overlapping schedules

Review & Approval

(Include a section for review and approval by stakeholders.)

  • Reviewer:
    (pending)

  • Approval Date:
    (pending)


Notes

Future Enhancements

  1. Dealer Address management APIs (CRUD for DealerAddress table)
  2. Bulk dealer import via CSV/Excel upload
  3. Dealer cloning (duplicate existing dealer with modified settings)
  4. Scraping schedule calendar view
  5. Dealer activity audit log
  6. Real-time scraping status dashboard per dealer
  7. Dealer group management APIs (manage group head hierarchies)
  8. Dealer comparison view (side-by-side configuration comparison)

Technical Debt to Address

  • DealerCreateRequest is reused for both POST and PUT — consider separate UpdateDealerRequest record
  • Validation rules are implicit — implement FluentValidation for all request models
  • No pagination sorting support yet — add sortBy and sortDirection parameters

Dependencies on Other Services

  • ManagementService: Hosts the Dealer API controllers and business logic
  • DatabaseService: Provides PostgreSQL schema migrations and seed data
  • CommonLibrary: Shared extensions (MassTransit, OpenTelemetry, etc.)
  • ContractLibrary: C# record models shared across services
  • Crawler Service: Consumes dealer scraping configuration to execute scheduled scrapes