Billing System
Author(s)
- Ashik
Last Updated Date
2026-04-16
SRS References
- 3.3.1 Billing & Invoice Management
- 3.3.2 Subscription Management
- 3.3.3 Usage Tracking & Metering
- 3.3.4 Payment Status Tracking
Version History
| Version | Date | Changes | Author |
|---|---|---|---|
| 1.0 | 2026-04-06 | Initial draft for comprehensive Billing System | Ashik |
| 1.1 | 2026-04-16 | Updated OAuth scopes (billing.settings.manage, billing.settings.view.all); Unified update endpoint to use BillingConfigurationRequest model; Removed separate UpdateBillingConfigurationRequest DTO | Ashik |
Feature Overview
Objective:
Implement a production-grade billing system that tracks setup charges, recurring subscriptions, usage-based billing, and generates reseller-level invoices with payment status tracking. The system supports prorated billing for mid-cycle onboarding and deactivation, configurable pricing tiers, scheduled invoice generation, and comprehensive export capabilities.
Scope:
- Track all billing events as immutable records: Setup (one-time fee), MonthlySubscription (monthly recurring), UsageBasedCharges (pay-per-record).
- Support prorated subscription calculations for mid-cycle dealer onboarding and deactivation using actual days in month (28/29/30/31).
- Generate reseller-level invoices with each reseller receiving their own consolidated invoice per billing period for all their dealers.
- Implement invoice numbering in date-based format:
INV-YYYY-MM-NNNN(sequential per billing period). - Track payment status lifecycle: Unpaid → PartiallyPaid/Paid with payment date tracking.
- Schedule automated monthly billing jobs: Subscription invoices on 1st (advance), Usage invoices on 1st (postpaid for previous month).
- Provide XLSX invoice export using existing ExcelService infrastructure.
- Store pricing configuration in database table with separate configurations for Setup, MonthlySubscription (with ScrapingSourceId), and PricePerRecord types for runtime updates without deployment.
- Publish MassTransit events for dealer lifecycle (onboarding, deactivation) to trigger billing actions.
- Support flexible billing cycles (e.g., 1st-15th, 16th-end of month) with billing_cycle_start and billing_cycle_end fields.
Requirements
Functional Requirements
-
Billing Event Capture
- Automatically create billing records when dealers are onboarded, subscriptions renew, or usage is recorded.
- Record types: Setup, MonthlySubscription, UsageBasedCharges.
- Capture metadata: Dealer ID, Amount, Quantity, Billing Config ID, Billing Period (YYYY-MM), Description.
- Generate unique
BillingId(UUID) for each entry. - Store timestamps in UTC with TIMESTAMPTZ.
- Mark events as invoiced once included in an invoice (
isInvoicedflag).
-
Pricing Configuration
- Store pricing tiers in
billingconfigurationdatabase table per reseller with separate rows per configuration type: Setup, MonthlySubscription (with ScrapingSourceId), PricePerRecord. - Each reseller can have their own pricing configuration (reseller-specific pricing).
- For MonthlySubscription configuration type, ScrapingSourceId must be specified to define pricing for each specific scraping source.
- Allow runtime updates to pricing without code deployment via API.
- Track pricing history with
effectivefromandeffectivetodate ranges. - Active configurations have
status = 'Active'andeffectiveto = NULL. - When updating pricing, set
effectivetoon the old record and create a new record witheffectivefromset to the new effective date. - Support future extensions: volume discounts, tiered pricing, time-based pricing rules.
- All configurations retained for complete audit trail and historical reporting.
- Store pricing tiers in
-
Prorated Subscription Calculation
- Calculate prorated subscription charges for mid-cycle dealer onboarding and scraping source additions.
- Formula:
(MonthlyRate / DaysInMonth) * DaysActive - Use actual days in month:
DateTime.DaysInMonth(year, month)(28/29/30/31). - No mid-cycle deactivation adjustments: Dealer and scraping source deactivations are scheduled for month-end, so full monthly charges apply (no prorated credits).
- Example: Dealer onboarded March 10 (22 days active):
($50 / 31) * 22 = $35.48
-
Invoice Generation
- Aggregate uninvoiced billing records by reseller (across all their dealers).
- Generate invoice with header: Invoice Number, Reseller ID, Invoice Date, Billing Cycle Start, Billing Cycle End, Total Amount, Payment Status.
- Create invoice items linking to billing records.
- Invoice numbering: Date-based format
INV-YYYY-MM-NNNNwith sequential counter per billing period. - Billing cycle dates support flexible periods (e.g., 1st-15th, 16th-end of month).
-
Scheduled Billing Jobs
- Monthly Subscription Billing (1st of month): Generate invoices for all active resellers with full monthly subscription charges aggregated from all their dealers (advance billing).
- Monthly Usage Billing (1st of month): Aggregate
dealerdatatransactionrecords from previous month for all dealers under each reseller, calculate usage charges, generate invoices (postpaid billing). - Scraping Source Mid-Cycle Billing (15th of month): Generate prorated invoices for scraping sources added between 1st-14th of current month.
- Scraping Source End-of-Month Billing (1st of month): Generate prorated invoices for scraping sources added between 15th-31st of previous month (runs together with monthly subscription billing).
- Setup + Prorated Billing (on-demand or scheduled): Generate invoices for newly onboarded dealers including setup fee and prorated subscription for partial month.
- Use Hangfire recurring jobs with cron expressions:
0 0 1 * *(subscription, usage, end-of-month source billing),0 0 15 * *(mid-cycle source billing). - Prevent duplicate billing with unique constraint on billing events per dealer per period.
-
Payment Status Tracking
- Track invoice payment lifecycle: Unpaid (default), PartiallyPaid (partial payment received), Paid (full payment received).
- Record payment date when status changes to Paid.
-
Dealer Lifecycle Event Handling
- Onboarding: When dealer is activated with required scraping sources (minimum 1), publish
DealerOnboardedEvent→ BillingService consumer creates Setup + Prorated MonthlySubscription charges (base + scraping sources).- Dealer must select at least 1 scraping source during onboarding (validation enforced at API level).
- Initial invoice includes: Setup fee + Prorated base subscription + Prorated charges for each selected scraping source.
- All charges prorated based on days remaining in current month.
- Invoice is generated at reseller level, consolidating billing for all their dealers.
- Deactivation: When dealer is disabled, deactivation is scheduled for month-end (no immediate billing adjustment).
- Unique constraint on billing events prevents duplicate processing of lifecycle events.
- Onboarding: When dealer is activated with required scraping sources (minimum 1), publish
-
Usage Tracking & Aggregation
- Query
dealerdatatransactiontable to count all Insert/Update records per dealer per billing period. - Formula:
UsageCharge = TotalRecordCount * PerRecordPrice - Example: Dealer D1 had 1,000 total records (inserts + updates) @ $0.10 in March → 1,000 * $0.10 = $100.00
- Create Usage billing events (UsageBasedCharges type) during monthly usage billing job.
- Query
-
Scraping Source Lifecycle Billing
- Mid-Cycle Addition: When a scraping source is added after monthly subscription payment, generate prorated invoice.
- Example: Reseller's dealer paid MonthlySubscription on April 1 ($50), adds Craigslist on April 5 ($30/month source fee).
- Invoice Scheduling Rules:
- If source added before 15th of month (days 1-14): Invoice generates on 15th of same month
- If source added on/after 15th of month (days 15-31): Invoice generates on 1st of next month
- Example calculation (added April 5, invoiced April 15): Days remaining = 30 - 5 + 1 = 26 days
- Prorated charge:
(SourceMonthlyFee / DaysInMonth) * RemainingDays= ($30 / 30) * 26 = $26.00 - Invoice sent to reseller showing: "Dealer XYZ added Craigslist on April 5 - Prorated charge for 26 days: $26.00"
- Next Month Billing: On May 1, monthly subscription invoice includes full charges for all active sources.
- May invoice: Base subscription ($50) + Craigslist full month ($30) = $80.00
- Source Removal Policy: Scraping sources are not removed immediately; removal is scheduled for month-end.
- When dealer/admin disables a scraping source, system sets
scheduledremovaldateto last day of current month. - Source remains active and billable until scheduled removal date.
- No refund/credit issued for partial month when source is disabled.
- Display warning: "This source will be removed on [month-end date]. You will continue to be billed until that date."
- When dealer/admin disables a scraping source, system sets
- Dealer Disable Policy: Same month-end removal policy applies to dealer deactivation.
- When dealer is disabled mid-month, system schedules deactivation for month-end.
- Full monthly subscription charge applies (no prorated credit).
- All active scraping sources continue billing until month-end.
- Display warning: "Dealer deactivation scheduled for [month-end date]. Full monthly charges apply."
- Mid-Cycle Addition: When a scraping source is added after monthly subscription payment, generate prorated invoice.
-
Billing Configuration Management
- Fetch current pricing via
GET /billing/configuration(requiresbilling.settings.view.allscope). - Returns paginated list of pricing configurations for all billing configuration types (Setup, MonthlySubscription, PricePerRecord).
- For MonthlySubscription configurations, ScrapingSourceId must be provided to specify which scraping source the pricing applies to.
- Supports search by keyword (searches reseller name, billing configuration type), filtering by configuration type, and pagination.
- Pagination parameters:
pagenumber(default: 1),rowsperpage(default: 10). - Create pricing (admin only) via
POST /billing/configurationwithbilling.settings.managescope. - Update scheduled pricing (admin only) via
PUT /billing/configuration/{billingconfigid}withbilling.settings.managescope.- Only configurations with
status = 'Scheduled'can be updated. - Can update: price, effectiveFrom, effectiveTo.
- Active configurations cannot be updated directly (must create new config and set effectiveTo on old one for audit trail).
- Only configurations with
- Return pricing per configuration type with status and effective date range.
- Validate pricing values (must be positive, non-zero).
- Track effective date range for pricing changes (
effectivefromandeffectiveto). - When changing active pricing, set
effectivetoon the old record and create new active config witheffectivefrom(maintains history). - Historical configurations (with
effectivetoset) are included in paginated results for audit trail.
- Fetch current pricing via
-
Reseller Invoice History API
- Fetch reseller invoices: List all invoices for a specific reseller with filters (billing period, payment status).
- Used for reseller portal to display billing history and invoices.
- Invoices aggregate billing records from all dealers under the reseller.
Non-Functional Requirements
-
Data Integrity
- Event-sourcing pattern: All charges stored as immutable
billingrecords (append-only, no updates/deletes). - Invoices are aggregations of billing records (supports audit trail, re-invoicing, dispute resolution).
- Transactional consistency: Billing event creation and invoice generation must be atomic.
- Foreign key constraints enforce referential integrity (dealer → reseller, event → invoice, line item → invoice).
- Event-sourcing pattern: All charges stored as immutable
-
Performance
- Invoice generation for a reseller (with all their dealers) completes in less than 5 seconds.
- Scheduled billing jobs execute without blocking API requests (Hangfire background workers).
- Database indexes on composite keys:
(dealerid, billingperiod),(resellerid, paymentstatus),(isinvoiced)for efficient queries. - Pagination for invoice lists with default 50 rows per page, max 500.
-
Scalability
- Support 1,000+ active dealers across 50+ resellers.
- Handle 10,000+ billing events per month.
- Invoices and billing events retained for 7+ years (compliance requirement).
- Database table partitioning strategy for historical data (future optimization).
-
Reliability
- Hangfire job retries on transient failures (database deadlocks, network timeouts).
- Idempotent billing event creation (prevent duplicate charges from retry logic).
- Comprehensive error logging with OpenTelemetry tracing for debugging billing discrepancies.
- Fallback mechanism: Manual invoice generation endpoint if scheduled job fails.
Workflow
Dealer Onboarding Billing Workflow:
- User creates new dealer via ManagementService API (
POST /dealers) with required scraping sources (minimum 1).- API validates that at least 1 scraping source is selected.
- Scraping sources are persisted with the dealer during onboarding.
DealerServicepersists dealer record to database (status = Active) along with selected scraping sources.- On success,
DealerServicepublishesDealerOnboardedEventvia MassTransit:- Message payload:
{ DealerId, ResellerId, OnboardedDate, ActiveSourceCount, ScrapingSourceIds[] }
- Message payload:
DealerOnboardedConsumer(in BillingService) receives event.- Consumer calls
IBillingEngineService.ProcessDealerOnboardingAsync(...). - Billing engine:
- Fetches active pricing configurations for the dealer's reseller (setup fee, monthly subscription).
- Fetches scraping source prices from
billingconfigurationtable wherebillingconfigurationtype = 'MonthlySubscription'andscrapingsourceidmatches. - Calculates days remaining in current month.
- Calculates prorated charges for base subscription and all scraping sources.
- Creates billing records with
isinvoiced = false:- Setup billing record (full amount, no proration)
- MonthlySubscription billing records (prorated for base and each scraping source)
- Billing records are now available for invoice generation (on-demand or scheduled).
Dealer Deactivation Billing Workflow:
- User disables dealer via ManagementService API (
PUT /dealers/{id}/status) changing status to Disable. DealerServicedoes not immediately disable the dealer.- Instead, system sets
scheduleddeactivationdateto last day of current month on the dealer record. - On success,
DealerServicepublishesDealerDeactivationScheduledEvent:- Message payload:
{ DealerId, ScheduledDeactivationDate }
- Message payload:
- Dealer remains active and continues billing until scheduled deactivation date.
- All scraping sources continue operating until scheduled deactivation.
- Display warning: "Dealer deactivation scheduled for [month-end date]. Full monthly charges apply."
- On 1st of next month, cleanup job executes dealer deactivations:
- Query dealers with
scheduleddeactivationdate <= yesterday's date. - Update dealer status to Disable.
- Publish
DealerDeactivatedEventfor audit trail.
- Query dealers with
- No prorated credit issued for partial month when dealer is disabled.
- Dealer excluded from future subscription invoices after scheduled deactivation (queried from
dealermastertable).
Scraping Source Addition Billing Workflow (Mid-Cycle):
- User adds scraping source to dealer via ManagementService API (
POST /dealers/{id}/sources). ScrapingSourceServicepersists scraping source record to database with status = Active.- On success,
ScrapingSourceServicepublishesScrapingSourceAddedEventvia MassTransit:- Message payload:
{ DealerId, SourceType, AddedDate, SourceMonthlyFee }
- Message payload:
ScrapingSourceAddedConsumer(in BillingService) receives event.- Consumer calls
IBillingEngineService.ProcessScrapingSourceAdditionAsync(...). - Billing engine:
- Checks if addition occurred after 1st of month (mid-cycle addition).
- If mid-cycle:
- Fetches source monthly fee from
billingconfigurationtable wherebillingconfigurationtype = 'MonthlySubscription'andscrapingsourceidmatches. - Calculates prorated charge for remaining days in current month.
- Creates
MonthlySubscriptionbilling record with prorated amount andisinvoiced = false. - Schedules invoice generation based on addition date:
- If added before 15th (days 1-14): Schedule for 15th of current month
- If added on/after 15th (days 15-31): Schedule for 1st of next month
- Example: Added April 5 → invoice April 15 | Added April 20 → invoice May 1
- Fetches source monthly fee from
- Scraping source included in next month's full subscription invoice at full monthly rate.
- Invoice notification sent to reseller: "Dealer XYZ added [SourceType] on [AddedDate] - Prorated charge for [X] days: $[Amount]"
Scraping Source Mid-Cycle Invoice Generation Workflow (Automated):
-
Mid-Month Job (15th of month):
- Hangfire recurring job triggers on 15th at midnight UTC (
0 0 15 * *cron). ScrapingSourceBillingJob.RunMidCycleBillingAsync()executes.- Job queries all uninvoiced
MonthlySubscriptionbilling records (for scraping sources) where:AddedDateis between 1st-14th of current monthisinvoiced = false
- For each reseller with uninvoiced source additions:
- Calls
IBillingEngineService.GenerateScrapingSourceInvoiceAsync(resellerId, billingPeriod). - Generates invoice number in format "INV-2026-04-0015" (using current billing period).
- Creates invoice record with prorated source addition charges.
- Marks source addition billing records as invoiced.
- Calls
- Job logs success/failure per reseller with OpenTelemetry tracing.
- Hangfire recurring job triggers on 15th at midnight UTC (
-
Month-End Job (1st of month):
- Runs as part of monthly subscription billing job.
- Queries all uninvoiced
MonthlySubscriptionbilling records (for scraping sources) where:AddedDateis between 15th-31st of previous monthisinvoiced = false
- Generates separate invoices for these prorated charges (or includes in monthly subscription invoice).
- Marks source addition billing records as invoiced.
Scraping Source Removal Policy Workflow:
- User disables scraping source via ManagementService API (
DELETE /dealers/{id}/sources/{sourceid}orPUT /dealers/{id}/sources/{sourceid}/disable). ScrapingSourceServicedoes not immediately delete or disable the source.- Instead, system sets
scheduledremovaldateto last day of current month on the scraping source record. - System publishes
ScrapingSourceRemovalScheduledEvent:- Message payload:
{ DealerId, SourceType, ScheduledRemovalDate }
- Message payload:
- Source remains active and continues operating until scheduled removal date.
- Source continues to be billed in monthly subscription until scheduled removal.
- Display warning to user: "This source will be removed on [month-end date]. You will continue to be billed until that date."
- On 1st of next month, cleanup job executes scraping source removals:
- Query sources with
scheduledremovaldate <= yesterday's date. - Update source status to Disabled or soft-delete.
- Publish
ScrapingSourceRemovedEventfor audit trail.
- Query sources with
- No prorated credit issued for partial month when source is disabled.
- Next month's subscription invoice excludes the removed source.
Monthly Subscription Invoice Generation Workflow (Automated):
- Hangfire recurring job triggers on 1st of month at midnight UTC (
0 0 1 * *cron). MonthlyBillingJob.RunMonthlySubscriptionBillingAsync()executes.- Job queries all active resellers from
resellermastertable (status = Active). - For each reseller:
- Calls
IBillingEngineService.GenerateMonthlySubscriptionInvoiceAsync(resellerId, billingPeriod). - Billing period = current month (e.g., "2026-04" for April billing).
- Calls
- Billing engine:
- Queries all active dealers under the reseller.
- For each dealer, queries all active scraping sources (excluding those with
scheduledremovaldatein the past). - Calculates monthly subscription charge including:
- Base monthly subscription fee (from
billingconfigurationwherebillingconfigurationtype = 'MonthlySubscription'andscrapingsourceid IS NULL) - Per-source monthly fees for all active scraping sources (from
billingconfigurationwherebillingconfigurationtype = 'MonthlySubscription'andscrapingsourceidmatches)
- Base monthly subscription fee (from
- Queries all uninvoiced Subscription billing records for the reseller in billing period.
- Generates next invoice number in format "INV-2026-04-0001".
- Creates invoice record (resellerid, invoicenumber, invoicedate, billing_cycle_start, billing_cycle_end, totalamount, paymentstatus).
- Creates invoice item records linking to each billing record.
- Marks all aggregated billing records as invoiced (
isinvoiced = true).
- Job logs success/failure per reseller with OpenTelemetry tracing.
- Optional: Publish
InvoiceGeneratedEventfor downstream notifications (email, reseller portal alert).
Monthly Usage Invoice Generation Workflow (Automated):
- Hangfire recurring job triggers on 1st of month at midnight UTC (
0 0 1 * *cron). MonthlyBillingJob.RunMonthlyUsageBillingAsync()executes.- Job calculates previous billing period: If today is April 1, billing period = "2026-03" (March usage).
- Job queries all active resellers from
resellermastertable. - For each reseller:
- Calls
IBillingEngineService.GenerateUsageInvoiceAsync(resellerId, billingPeriod).
- Calls
- Billing engine:
- For each dealer under the reseller, counts all records (inserts + updates) from dealerdatatransaction table for the dealer in billing period.
- Fetches pricing from
billingconfigurationfor dealer's reseller:- Queries usage price:
SELECT price FROM billingconfiguration WHERE resellerid = ? AND billingconfigurationtype = 'PricePerRecord' AND status = 'Active' AND effectiveto IS NULL
- Queries usage price:
- Calculates usage charges: TotalRecordCount * PerRecordPrice.
- Creates Usage billing record (UsageBasedCharges) with quantity and calculated amount.
- Creates invoice record with billing_cycle_start and billing_cycle_end for the previous month.
- Creates invoice items linking to the usage billing record.
- Marks billing records as invoiced.
- Usage invoice spans previous month's data (postpaid billing model).
Manual Invoice Generation Workflow (On-Demand):
- Admin user calls invoice generation endpoint with reseller ID and billing period.
- Controller validates JWT and billing.write scope.
- Controller calls service method to generate invoice.
- Service executes same invoice generation logic as scheduled jobs.
- Returns invoice ID and details in response.
- Used for: Re-generating failed invoices, generating setup invoices immediately after onboarding.
Data Models
Entity: BillingConfiguration
public class BillingConfiguration
{
public Guid BillingConfigId { get; set; }
public Guid ResellerId { get; set; }
public BillingConfigurationType BillingConfigurationType { get; set; }
public Guid? ScrapingSourceId { get; set; } // Nullable, only applicable for MonthlySubscription type
public decimal Price { get; set; }
public BillingConfigurationStatus Status { get; set; }
public DateTime EffectiveFrom { get; set; }
public DateTime? EffectiveTo { get; set; }
public DateTime? CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
Enums:
public enum BillingConfigurationType
{
Setup = 1,
MonthlySubscription,
PricePerRecord
}
public enum BillingType
{
Setup = 1,
MonthlySubscription,
UsageBasedCharges
}
public enum PaymentStatus
{
UnPaid = 1,
PartiallyPaid,
Paid
}
public enum BillingConfigurationStatus
{
Active = 1,
Disabled,
Scheduled
}
public enum PaymentMethod
{
OnlineTransfer = 1,
BankTransfer,
Check,
Cash,
CreditCard
}
Note:
- BillingConfigurationType is used in the
billingconfigurationtable and stored as VARCHAR strings: "Setup", "MonthlySubscription", "PricePerRecord". When using MonthlySubscription configuration type, you must provide a ScrapingSourceId. - BillingType is used in the
billingtable for actual billing records and stored as VARCHAR strings: "Setup", "MonthlySubscription", "UsageBasedCharges". The C# enums provide type safety in code while the database stores human-readable string values.
Important: The invoice table now uses resellerid instead of dealerid, as dealer information is already captured in the billing table. Each invoice also includes billing_cycle_start and billing_cycle_end to support flexible billing periods (e.g., 1st-15th, 16th-end of month).
Payment Status Values (stored as VARCHAR, not enum):
"UnPaid"- Payment pending"PartiallyPaid"- Payment partially received"Paid"- Received full payment
Payment Method Values (stored as VARCHAR from enum):
"OnlineTransfer"- Online/digital transfer"BankTransfer"- Bank wire transfer"Check"- Check payment"Cash"- Cash payment"CreditCard"- Credit card payment
Note: Billing records are created automatically by internal services (DealerOnboardedConsumer, DealerDeactivatedConsumer, MonthlyBillingJob). No public API endpoints exist for manual billing record creation.
DTO: BillingRequest (internal use only)
public record BillingRequest
{
public Guid DealerId { get; init; }
public BillingType BillingType { get; init; } // "Setup", "MonthlySubscription", "UsageBasedCharges"
public decimal Amount { get; init; }
public int? Quantity { get; init; }
public Guid BillingConfigId { get; init; }
public Guid? ScrapingSourceId { get; init; }
public string BillingPeriod { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
}
DTO: InvoiceDetails
public record InvoiceDetails
{
public Guid InvoiceId { get; init; }
public Guid ResellerId { get; init; }
public string ResellerName { get; init; } = string.Empty;
public string InvoiceNumber { get; init; } = string.Empty;
public DateTime InvoiceDate { get; init; }
public decimal TotalAmount { get; init; }
public PaymentStatus PaymentStatus { get; init; } = string.Empty;
public DateTime BillingCycleStart { get; init; }
public DateTime BillingCycleEnd { get; init; }
public List<InvoiceItemDetails> InvoiceItems { get; init; } = [];
public DateTime CreatedAt { get; init; }
}
DTO: InvoiceItemDetails
public record InvoiceItemDetails
{
public Guid InvoiceItemId { get; init; }
public Guid? BillingId { get; init; }
public string BillingType { get; init; } = string.Empty;
public Guid? SourceId { get; init; }
public string? SourceName { get; init; }
public string Description { get; init; } = string.Empty;
public int? Quantity { get; init; }
public decimal Amount { get; init; }
}
DTO: InvoiceRequest
public record InvoiceRequest
{
public Guid ResellerId { get; init; }
public DateTime StartDate { get; init; }
public DateTime EndDate { get; init; }
}
DTO: InvoiceFilter
public record InvoiceFilter
{
public Guid? ResellerId { get; init; }
public List<PaymentStatus>? PaymentStatuses { get; init; } // ["Pending", "Paid", "Overdue", "Cancelled"]
public DateTime? StartDate { get; init; }
public DateTime? EndDate { get; init; }
public int PageNumber { get; init; } = 1;
public int RowsPerPage { get; init; } = 50;
}
DTO: CommonResponse
public record CommonResponse
{
public int Status { get; init; }
public string? Message { get; init; }
}
DTO: ServerPaginatedData
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; }
}
Note: CommonResponse is used for POST and PUT operations that don't need to return detailed data, providing status code and optional message. GET operations use ResponseWithData<T>, ServerPaginatedData<T>, or PaginatedResponse<T> wrappers depending on the endpoint.
DTO: BillingConfigurationRequest
public record BillingConfigurationRequest
{
public Guid ResellerId { get; init; }
public BillingConfigurationType BillingConfigurationType { get; init; }
public Guid? ScrapingSourceId { get; init; } // Nullable, only applicable for MonthlySubscription type
public decimal Price { get; init; }
public BillingConfigurationStatus Status { get; init; }
public DateTime EffectiveFrom { get; init; }
public DateTime? EffectiveTo { get; init; }
}
DTO: BillingConfiguration Filter
public record BillingConfigurationFilter
{
public Guid ResellerId { get; init; }
public BillingConfigurationType? BillingConfigurationType { get; init; }
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
public BillingConfigurationStatus? Status { get; init; }
public DateTime? EffectiveFrom { get; init; }
public DateTime? EffectiveTo { get; init; }
public string? SearchKeyword { get; set; } // Search by price, status, billing type and date
public int RowsPerPage { get; set; } = 10;
public int PageNumber { get; set; } = 1;
}
DTO: PaymentRequest
public record PaymentRequest
{
public Guid ResellerId { get; init; }
public decimal TotalAmount { get; init; }
public PaymentMethod PaymentMethod { get; init; }
public List<InvoicePaymentItem> InvoicePayments { get; init; } = [];
public string? TransactionReference { get; init; }
public string? ReceiptNo { get; init; }
public string? Notes { get; init; }
}
DTO: InvoicePaymentItem
public record InvoicePaymentItem
{
public Guid InvoiceId { get; init; }
public decimal AmountPaid { get; init; }
}
DTO: PaymentFilter
public record PaymentFilter
{
public Guid? ResellerId { get; init; }
public List<string>? PaymentStatuses { get; init; } // ["Pending", "PartiallyPaid", "Paid"]
public PaymentMethod? PaymentMethod { get; init; }
public DateTime? StartDate { get; init; }
public DateTime? EndDate { get; init; }
public int PageNumber { get; init; } = 1;
public int RowsPerPage { get; init; } = 50;
}
DTO: PaymentDetails
public record PaymentDetails
{
public Guid PaymentId { get; init; }
public Guid ResellerId { get; init; }
public string ResellerName { get; init; } = string.Empty;
public decimal TotalAmount { get; init; }
public DateTime PaymentDate { get; init; }
public PaymentMethod PaymentMethod { get; init; }
public string PaymentStatus { get; init; } = string.Empty;
public string? TransactionReference { get; init; }
public string? ReceiptNo { get; init; }
public string? Notes { get; init; }
public List<InvoicePaymentDetails> InvoicePayments { get; init; } = [];
public DateTime CreatedAt { get; init; }
public DateTime UpdatedAt { get; init; }
}
DTO: InvoicePaymentDetails
public record InvoicePaymentDetails
{
public Guid PaymentDetailsId { get; init; }
public Guid InvoiceId { get; init; }
public string InvoiceNumber { get; init; } = string.Empty;
public decimal PaidAmount { get; init; }
}
MassTransit Message Contracts:
- DealerOnboardedEvent: DealerId, ResellerId, OnboardedDate, ActiveSourceCount, ScrapingSourceIds[] (array of selected source IDs)
- DealerDeactivationScheduledEvent: DealerId, ScheduledDeactivationDate
- DealerDeactivatedEvent: DealerId, DeactivatedDate (published by cleanup job after scheduled deactivation)
- ScrapingSourceAddedEvent: DealerId, SourceType, AddedDate, SourceMonthlyFee
- ScrapingSourceRemovedEvent: DealerId, SourceType, RemovedDate (note: actual removal scheduled for month-end)
- BillingPeriodClosedEvent: BillingPeriod, ResellerId (nullable), ClosedDate
Database Schema:
-- billingconfiguration table
CREATE TABLE IF NOT EXISTS billingconfiguration (
billingconfigid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resellerid UUID NOT NULL,
billingconfigurationtype VARCHAR(30) NOT NULL, -- "Setup", "MonthlySubscription", "PricePerRecord"
scrapingsourceid UUID, -- Nullable, only applicable for MonthlySubscription type
price NUMERIC(12,4) NOT NULL, -- Price for this billing type
status VARCHAR(20) NOT NULL DEFAULT 'Active', -- "Active", "Disabled", "Scheduled"
effectivefrom TIMESTAMPTZ NOT NULL,
effectiveto TIMESTAMPTZ, -- NULL means currently active
createdat TIMESTAMPTZ NOT NULL DEFAULT now(),
updatedat TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT fkbillingconfig_reseller FOREIGN KEY (resellerid)
REFERENCES resellermaster(resellerid) ON DELETE RESTRICT,
CONSTRAINT fkbillingconfig_scrapingsource FOREIGN KEY (scrapingsourceid)
REFERENCES scrapingsource(sourceid) ON DELETE RESTRICT,
CONSTRAINT chk_billingconfig_status CHECK (status IN ('Active', 'Disabled', 'Scheduled')),
CONSTRAINT chk_billingconfig_billingtype CHECK (billingconfigurationtype IN ('Setup', 'MonthlySubscription', 'PricePerRecord'))
);
-- billing table (formerly billingevent)
CREATE TABLE IF NOT EXISTS billing (
billingid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
dealerid UUID NOT NULL,
billingtype VARCHAR(30) NOT NULL, -- "Setup", "MonthlySubscription", "UsageBasedCharges"
quantity INTEGER,
billingconfigid UUID,
sourceid UUID,
amount NUMERIC(12,2) NOT NULL,
billingperiod VARCHAR(7) NOT NULL, -- YYYY-MM format
description TEXT,
isinvoiced BOOLEAN NOT NULL DEFAULT false,
createdat TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT fkbilling_dealer FOREIGN KEY (dealerid)
REFERENCES dealermaster(dealerid) ON DELETE RESTRICT,
CONSTRAINT fkbilling_config FOREIGN KEY (billingconfigid)
REFERENCES billingconfiguration(billingconfigid) ON DELETE SET NULL
);
-- invoice table
CREATE TABLE IF NOT EXISTS invoice (
invoiceid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resellerid UUID NOT NULL,
invoicenumber VARCHAR(20) NOT NULL UNIQUE,
invoicedate TIMESTAMPTZ NOT NULL,
totalamount NUMERIC(12,2) NOT NULL,
paymentamount NUMERIC(12,2) NOT NULL,
remainingamount NUMERIC(12,2) NOT NULL,
paymentstatus VARCHAR(20) NOT NULL DEFAULT 'Pending', -- "Unpaid", "PartiallyPaid", "Paid"
billing_cycle_start TIMESTAMPTZ NOT NULL,
billing_cycle_end TIMESTAMPTZ NOT NULL,
createdat TIMESTAMPTZ NOT NULL DEFAULT now(),
updatedat TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT fkinvoice_reseller FOREIGN KEY (resellerid)
REFERENCES resellermaster(resellerid) ON DELETE RESTRICT
);
-- invoiceitem table (formerly invoicelineitem)
CREATE TABLE IF NOT EXISTS invoiceitem (
invoiceitemid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
invoiceid UUID NOT NULL,
billingid UUID,
createdat TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT fkinvoiceitem_invoice FOREIGN KEY (invoiceid)
REFERENCES invoice(invoiceid) ON DELETE CASCADE,
CONSTRAINT fkinvoiceitem_billing FOREIGN KEY (billingid)
REFERENCES billing(billingid) ON DELETE SET NULL
);
-- payment table (for managing partial payments across multiple invoices)
CREATE TABLE IF NOT EXISTS payment (
paymentid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resellerid UUID NOT NULL,
totalamount NUMERIC(12,2) NOT NULL,
paymentdate TIMESTAMPTZ NOT NULL,
paymentmethod VARCHAR(30) NOT NULL, -- Enum: "OnlineTransfer", "BankTransfer", "Check", "Cash", "CreditCard"
paymentstatus VARCHAR(20) NOT NULL DEFAULT 'Pending', -- "Unpaid", "PartiallyPaid", "Paid"
transactionreference VARCHAR(100),
receiptno VARCHAR(100),
notes TEXT,
createdat TIMESTAMPTZ NOT NULL DEFAULT now(),
updatedat TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT fkpayment_reseller FOREIGN KEY (resellerid)
REFERENCES resellermaster(resellerid) ON DELETE RESTRICT
);
-- paymentdetails table (links payments to invoices for partial payment tracking)
CREATE TABLE IF NOT EXISTS paymentdetails (
paymentdetailsid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
paymentid UUID NOT NULL,
invoiceid UUID NOT NULL,
paidamount NUMERIC(12,2) NOT NULL, -- Partial payment amount for this invoice
createdat TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT fkpaymentdetails_payment FOREIGN KEY (paymentid)
REFERENCES payment(paymentid) ON DELETE CASCADE,
CONSTRAINT fkpaymentdetails_invoice FOREIGN KEY (invoiceid)
REFERENCES invoice(invoiceid) ON DELETE CASCADE
);
-- Indexes for efficient querying
CREATE INDEX IF NOT EXISTS idxbilling_dealer
ON billing(dealerid);
CREATE INDEX IF NOT EXISTS idxbilling_invoiced
ON billing(isinvoiced) WHERE isinvoiced = false;
CREATE INDEX IF NOT EXISTS idxbilling_period
ON billing(billingperiod, billingtype);
CREATE INDEX IF NOT EXISTS idxbilling_composite
ON billing(dealerid, billingperiod, isinvoiced);
CREATE INDEX IF NOT EXISTS idxbilling_config
ON billing(billingconfigid);
CREATE INDEX IF NOT EXISTS idxinvoice_reseller
ON invoice(resellerid);
CREATE INDEX IF NOT EXISTS idxinvoice_paymentstatus
ON invoice(paymentstatus);
CREATE INDEX IF NOT EXISTS idxinvoice_composite
ON invoice(resellerid, paymentstatus, invoicedate DESC);
CREATE INDEX IF NOT EXISTS idxinvoice_cycle
ON invoice(billing_cycle_start, billing_cycle_end);
CREATE INDEX IF NOT EXISTS idxinvoiceitem_invoice
ON invoiceitem(invoiceid);
CREATE INDEX IF NOT EXISTS idxinvoiceitem_billing
ON invoiceitem(billingid);
CREATE INDEX IF NOT EXISTS idxbillingconfig_reseller
ON billingconfiguration(resellerid);
CREATE INDEX IF NOT EXISTS idxbillingconfig_active
ON billingconfiguration(resellerid, billingconfigurationtype, status, effectivefrom) WHERE status = 'Active' AND effectiveto IS NULL;
CREATE INDEX IF NOT EXISTS idxbillingconfig_effectiverange
ON billingconfiguration(resellerid, billingconfigurationtype, effectivefrom, effectiveto);
CREATE INDEX IF NOT EXISTS idxpayment_reseller
ON payment(resellerid);
CREATE INDEX IF NOT EXISTS idxpayment_status
ON payment(paymentstatus);
CREATE INDEX IF NOT EXISTS idxpayment_date
ON payment(paymentdate DESC);
CREATE INDEX IF NOT EXISTS idxpaymentdetails_payment
ON paymentdetails(paymentid);
CREATE INDEX IF NOT EXISTS idxpaymentdetails_invoice
ON paymentdetails(invoiceid);
API Interfaces
| Endpoint | Method | Payload | Response | Status Codes |
|---|---|---|---|---|
/billing/configuration | POST | BillingConfigurationRequest | CommonResponse | 200, 400, 403, 500 |
/billing/configuration/{billingconfigid} | PUT | BillingConfigurationRequest | CommonResponse | 200, 400, 403, 404, 500 |
/billing/configuration | GET | BillingConfigurationFilter | ServerPaginatedData<BillingConfiguration> | 200, 403, 404, 500 |
/billing/invoices | POST | InvoiceRequest | CommonResponse | 201, 400, 403, 500 |
/billing/invoices | GET | InvoiceFilter (query params) | ServerPaginatedData<InvoiceDetails> | 200, 400, 403, 500 |
/billing/invoices/{invoiceid} | GET | Path: invoiceid (Guid) | InvoiceDetails | 200, 403, 404, 500 |
/billing/invoices/{invoiceid}/export | GET | Path: invoiceid (Guid) | File (PDF) | 200, 403, 404, 500 |
/billing/payments | POST | PaymentRequest | CommonResponse | 201, 400, 403, 500 |
/billing/payments | GET | PaymentFilter (query params) | ServerPaginatedData<PaymentDetails> | 200, 400, 403, 500 |
/billing/payments/{paymentid} | GET | Path: paymentid (Guid) | PaymentDetails | 200, 403, 404, 500 |
API Request/Response Examples
Create Billing Configuration
Request: POST /billing/configuration with ResellerId, BillingConfigurationType, ScrapingSourceId, Price, Status, EffectiveFrom, EffectiveTo (admin only, requires billing.settings.manage scope)
Payload Example (Single):
{
"resellerId": "uuid",
"billingConfigurationType": "MonthlySubscription",
"scrapingSourceId": "uuid",
"price": 50.00,
"status": "Active",
"effectiveFrom": "2026-04-01T00:00:00Z",
"effectiveTo": null
}
Payload Example (Setup - no ScrapingSourceId):
{
"resellerId": "uuid",
"billingConfigurationType": "Setup",
"scrapingSourceId": null,
"price": 100.00,
"status": "Active",
"effectiveFrom": "2026-04-01T00:00:00Z",
"effectiveTo": null
}
Response: Returns CommonResponse with success confirmation (status 200).
Response Example:
{
"status": 200,
"message": "Billing configuration created successfully"
}
Get Billing Configuration
Request: GET /billing/configuration?&searchkeyword={keyword}&pagenumber={page}&rowsperpage={rows} (requires billing.settings.view.all scope)
Query Parameters:
BillingConfigurationFilter
Response: Returns paginated list of pricing configurations with pagination metadata
Response Example:
{
"data": [
{
"billingConfigId": "uuid",
"resellerId": "uuid",
"resellerName": "Acme Corp",
"billingConfigurationType": "Setup",
"scrapingSourceId": null,
"price": 100.00,
"status": "Active",
"effectiveFrom": "2026-04-01T00:00:00Z",
"effectiveTo": null
},
{
"billingConfigId": "uuid",
"resellerId": "uuid",
"resellerName": "Acme Corp",
"billingConfigurationType": "MonthlySubscription",
"scrapingSourceId": "uuid",
"price": 50.00,
"status": "Active",
"effectiveFrom": "2026-04-01T00:00:00Z",
"effectiveTo": null
}
],
"totalNumber": 25,
"hasPreviousPage": false,
"hasNextPage": true,
"totalPages": 3,
"pageNumber": 1,
"rowsPerPage": 10
}
Update Billing Configuration
Request: PUT /billing/configuration/{billingconfigid} (admin only, requires billing.settings.manage scope)
Description: Update scheduled status billing configuration records. Only Scheduled status configurations can be updated. Uses BillingConfigurationRequest as payload.
Payload Example:
{
"resellerId": "uuid",
"billingConfigurationType": 2,
"scrapingSourceId": "uuid",
"price": 55.00,
"status": 3,
"effectiveFrom": "2026-05-01T00:00:00Z",
"effectiveTo": "2026-12-31T23:59:59Z"
}
Validation Rules:
- Only configurations with
status = 'Scheduled'can be updated - Active configurations cannot be updated directly (must create new config and set effectiveTo on old one)
- ResellerId must be a valid Guid
- BillingConfigurationType must be a valid enum value (1=Setup, 2=MonthlySubscription, 3=PricePerRecord)
- ScrapingSourceId is required for MonthlySubscription type, must be null for other types
- Price must be positive and non-zero
- Status must be BillingConfigurationStatus.Scheduled (3) for updates
- EffectiveFrom is required and must be in the future
- EffectiveTo must be after EffectiveFrom if provided
Response: Returns CommonResponse with success confirmation (status 200).
Response Example:
{
"status": 200,
"message": "Billing configuration updated successfully"
}
Generate Invoice (Manual)
Request: POST /billing/invoices with InvoiceRequest payload
Payload Example:
{
"resellerId": "uuid",
"startDate": "2026-04-01T00:00:00Z",
"endDate": "2026-04-30T23:59:59Z"
}
Response: Returns CommonResponse with success confirmation (status 201)
Response Example:
{
"status": 201,
"message": "Invoice generated successfully"
}
Get Paginated Invoices
Request: GET /billing/invoices with filters (resellerid, paymentStatuses, pagination)
Response: Returns paginated list of invoices with full details including invoice items (status 200)
Response Example:
{
"data": [
{
"invoiceId": "123e4567-e89b-12d3-a456-426614174000",
"resellerId": "223e4567-e89b-12d3-a456-426614174000",
"resellerName": "ABC Reseller",
"invoiceNumber": "INV-2026-04-0001",
"invoiceDate": "2026-04-01T00:00:00Z",
"totalAmount": 180.00,
"paymentStatus": "Paid",
"billingCycleStart": "2026-04-01T00:00:00Z",
"billingCycleEnd": "2026-04-30T23:59:59Z",
"invoiceItems": [
{
"invoiceItemId": "323e4567-e89b-12d3-a456-426614174000",
"billingId": "423e4567-e89b-12d3-a456-426614174000",
"billingType": "MonthlySubscription",
"description": "Monthly subscription for April 2026",
"quantity": 1,
"amount": 50.00
},
{
"invoiceItemId": "523e4567-e89b-12d3-a456-426614174000",
"billingId": "623e4567-e89b-12d3-a456-426614174000",
"billingType": "MonthlySubscription",
"description": "Craigslist scraping source",
"quantity": 1,
"amount": 30.00
},
{
"invoiceItemId": "723e4567-e89b-12d3-a456-426614174000",
"billingId": "823e4567-e89b-12d3-a456-426614174000",
"billingType": "UsageBasedCharges",
"description": "Usage charges for March 2026",
"quantity": 1000,
"amount": 100.00
}
],
"createdAt": "2026-04-01T00:05:00Z"
}
],
"totalNumber": 145,
"hasPreviousPage": false,
"hasNextPage": true,
"totalPages": 15,
"pageNumber": 1,
"rowsPerPage": 10
}
Get Invoice Details
Request: GET /billing/invoices/{invoiceid}
Response: Returns complete invoice with reseller info, all invoice items with linked billing details (InvoiceDetails model, status 200)
Response Example:
{
"invoiceId": "123e4567-e89b-12d3-a456-426614174000",
"resellerId": "223e4567-e89b-12d3-a456-426614174000",
"resellerName": "ABC Reseller",
"invoiceNumber": "INV-2026-04-0001",
"invoiceDate": "2026-04-01T00:00:00Z",
"totalAmount": 180.00,
"paymentStatus": "Paid",
"billingCycleStart": "2026-04-01T00:00:00Z",
"billingCycleEnd": "2026-04-30T23:59:59Z",
"invoiceItems": [
{
"invoiceItemId": "323e4567-e89b-12d3-a456-426614174000",
"billingId": "423e4567-e89b-12d3-a456-426614174000",
"billingType": "MonthlySubscription",
"description": "Monthly subscription for April 2026",
"quantity": 1,
"amount": 50.00
},
{
"invoiceItemId": "523e4567-e89b-12d3-a456-426614174000",
"billingId": "623e4567-e89b-12d3-a456-426614174000",
"billingType": "MonthlySubscription",
"description": "Craigslist scraping source - prorated (26 days)",
"quantity": 1,
"amount": 30.00
},
{
"invoiceItemId": "723e4567-e89b-12d3-a456-426614174000",
"billingId": "823e4567-e89b-12d3-a456-426614174000",
"billingType": "UsageBasedCharges",
"description": "Usage charges for March 2026 (1000 records processed)",
"quantity": 1000,
"amount": 100.00
}
],
"createdAt": "2026-04-01T00:05:00Z"
}
Export Invoice as PDF
Request: GET /billing/invoices/{invoiceid}/export
Response: PDF file download with invoice header, invoice items, and total formatted for printing
Record Payment (Partial Payment Support)
Request: POST /billing/payments with PaymentRequest (requires billing.write scope)
Payload Example:
{
"resellerId": "123e4567-e89b-12d3-a456-426614174000",
"totalAmount": 250.00,
"paymentMethod": "BankTransfer",
"invoicePayments": [
{
"invoiceId": "223e4567-e89b-12d3-a456-426614174000",
"amountPaid": 150.00
},
{
"invoiceId": "323e4567-e89b-12d3-a456-426614174000",
"amountPaid": 100.00
}
],
"transactionReference": "TXN-2026-04-001",
"receiptNo": "RCP-2026-04-001",
"notes": "Payment received via bank transfer for April invoices"
}
Response: Returns CommonResponse with success confirmation (status 201)
Response Example:
{
"status": 201,
"message": "Payment recorded successfully"
}
Get Paginated Payments
Request: GET /billing/payments with filters (resellerId, paymentStatuses, pagination)
Response: Returns paginated list of payments with full details including invoice payment details (status 200)
Response Example:
{
"data": [
{
"paymentId": "323e4567-e89b-12d3-a456-426614174000",
"resellerId": "123e4567-e89b-12d3-a456-426614174000",
"resellerName": "Premium Auto Group",
"totalAmount": 250.00,
"paymentDate": "2026-04-05T10:30:00Z",
"paymentMethod": "BankTransfer",
"paymentStatus": "Paid",
"transactionReference": "TXN-2026-04-001",
"receiptNo": "RCP-2026-04-001",
"notes": "Payment received via bank transfer for April invoices",
"invoicePayments": [
{
"paymentDetailsId": "423e4567-e89b-12d3-a456-426614174000",
"invoiceId": "223e4567-e89b-12d3-a456-426614174000",
"invoiceNumber": "INV-2026-04-0012",
"paidAmount": 150.00
},
{
"paymentDetailsId": "523e4567-e89b-12d3-a456-426614174000",
"invoiceId": "323e4567-e89b-12d3-a456-426614174000",
"invoiceNumber": "INV-2026-04-0013",
"paidAmount": 100.00
}
],
"createdAt": "2026-04-05T10:30:00Z",
"updatedAt": "2026-04-05T10:30:00Z"
},
{
"paymentId": "623e4567-e89b-12d3-a456-426614174000",
"resellerId": "123e4567-e89b-12d3-a456-426614174000",
"resellerName": "Premium Auto Group",
"totalAmount": 500.00,
"paymentDate": "2026-04-10T14:15:00Z",
"paymentMethod": "OnlineTransfer",
"paymentStatus": "Paid",
"transactionReference": "TXN-2026-04-002",
"receiptNo": "RCP-2026-04-002",
"notes": "Online payment for outstanding invoices",
"invoicePayments": [
{
"paymentDetailsId": "723e4567-e89b-12d3-a456-426614174000",
"invoiceId": "823e4567-e89b-12d3-a456-426614174000",
"invoiceNumber": "INV-2026-04-0020",
"paidAmount": 300.00
},
{
"paymentDetailsId": "923e4567-e89b-12d3-a456-426614174000",
"invoiceId": "a23e4567-e89b-12d3-a456-426614174000",
"invoiceNumber": "INV-2026-04-0025",
"paidAmount": 200.00
}
],
"createdAt": "2026-04-10T14:15:00Z",
"updatedAt": "2026-04-10T14:15:00Z"
}
],
"totalNumber": 87,
"hasPreviousPage": false,
"hasNextPage": true,
"totalPages": 9,
"pageNumber": 1,
"rowsPerPage": 10
}
Get Payment Details
Request: GET /billing/payments/{paymentid}
Response: Returns complete payment details with reseller info, all invoice payments with invoice numbers and amounts (PaymentDetails model, status 200)
Response Example:
{
"paymentId": "323e4567-e89b-12d3-a456-426614174000",
"resellerId": "123e4567-e89b-12d3-a456-426614174000",
"resellerName": "Premium Auto Group",
"totalAmount": 250.00,
"paymentDate": "2026-04-05T10:30:00Z",
"paymentMethod": "BankTransfer",
"paymentStatus": "Paid",
"transactionReference": "TXN-2026-04-001",
"receiptNo": "RCP-2026-04-001",
"notes": "Payment received via bank transfer for April invoices",
"invoicePayments": [
{
"paymentDetailsId": "423e4567-e89b-12d3-a456-426614174000",
"invoiceId": "223e4567-e89b-12d3-a456-426614174000",
"invoiceNumber": "INV-2026-04-0012",
"paidAmount": 150.00
},
{
"paymentDetailsId": "523e4567-e89b-12d3-a456-426614174000",
"invoiceId": "323e4567-e89b-12d3-a456-426614174000",
"invoiceNumber": "INV-2026-04-0013",
"paidAmount": 100.00
}
],
"createdAt": "2026-04-05T10:30:00Z",
"updatedAt": "2026-04-05T10:30:00Z"
}
Acceptance Criteria
- ✅ Dealer onboarding requires minimum 1 scraping source (validation enforced at API level).
- ✅ Dealer onboarding automatically creates Setup + Prorated Base Subscription + Prorated Scraping Source charges via MassTransit consumer (no manual API calls).
- ✅ Setup fee is retrieved from billingconfiguration table (SetupFee billing type) per reseller.
- ✅ Dealer deactivation is scheduled for month-end (not immediate); full monthly charges apply with no prorated credit.
- ✅ Scraping source mid-cycle addition generates prorated invoice on 15th of month (if added before 15th) or 1st of next month (if added on/after 15th).
- ✅ Scraping source removal is scheduled for month-end (not immediate); full monthly charges apply with no prorated credit.
- ✅ No adjustment or credit billing events generated for deactivations; month-end scheduling eliminates need for prorated refunds.
- ✅ Monthly subscription invoices include base subscription fee plus all active scraping source fees.
- ✅ Monthly subscription invoices auto-generate on 1st of month via Hangfire job for all active dealers.
- ✅ Monthly usage invoices auto-generate on 1st of month via Hangfire job aggregating previous month's
dealerdatatransactionrecords (inserts + updates combined). - ✅ Invoice numbering follows date-based format
INV-YYYY-MM-NNNNwith sequential counter per billing period. - ✅ Prorated subscription calculation uses actual days in month (28/29/30/31) for accurate billing.
- ✅ Invoice export generates PDF with professional formatting, company branding, and print-ready layout.
- ✅ Pricing configuration stored per reseller in database table and can be created via API (admin only,
billing.settings.managescope). - ✅ Scheduled pricing configurations can be updated via PUT endpoint to modify price, effectiveFrom, or effectiveTo fields (admin only,
billing.settings.managescope). - ✅ Active pricing configurations cannot be updated directly; must create new config with new effectiveFrom and set effectiveTo on old config (maintains audit trail).
- ✅ Users with
billing.settings.view.allscope can view billing configurations (403 otherwise). - ✅ Only users with
billing.readscope can view invoices (403 otherwise). - ✅ Only users with
billing.writescope can generate invoices and record payments (403 otherwise). - ✅ Only users with
billing.settings.managescope can create and update billing configurations (403 otherwise). - ✅ Billing records are immutable (append-only, no updates/deletes after creation) and created automatically by services.
- ✅ Each reseller receives their own separate invoice (reseller-level invoicing).
- ✅ Invoice generation marks aggregated billing records as invoiced (
isinvoiced = true). - ✅ Pagination handles 1,000+ invoices efficiently (less than 500ms per page).
- ✅ Manual invoice generation endpoint allows admins to trigger on-demand invoice creation (fallback for failed jobs).
- ✅ Hangfire dashboard accessible at
/hangfire(admin only) for monitoring scheduled jobs. - ✅ Payment system supports partial payments across multiple invoices via payment and paymentdetails tables.
- ✅ System displays warnings when dealer/source deactivation is scheduled: "Removal scheduled for [month-end date]. Full monthly charges apply."
Complete Workflow Example: Dealer Onboarding with Scraping Sources
This section demonstrates a complete end-to-end billing workflow including dealer onboarding, scraping source management, invoice generation, and payment processing with actual table record examples.
Initial Setup - Master Data Tables
Table: scrapingsource (Master scraping source catalog)
| sourceid | sourcename | status | createdat |
|---|---|---|---|
| 550e8400-e29b-41d4-a716-446655440001 | Craigslist | Active | 2026-01-01 |
| 550e8400-e29b-41d4-a716-446655440002 | Facebook Marketplace | Active | 2026-01-01 |
| 550e8400-e29b-41d4-a716-446655440003 | CarGurus | Active | 2026-01-01 |
| 550e8400-e29b-41d4-a716-446655440004 | AutoTrader | Active | 2026-01-01 |
Table: billingconfiguration (Reseller pricing for all billing types including scraping sources)
| billingconfigid | resellerid | billingconfigurationtype | scrapingsourceid | price | status | effectivefrom | effectiveto |
|---|---|---|---|---|---|---|---|
| 650e8400-e29b-41d4-a716-446655440001 | 750e8400-e29b-41d4-a716-446655440001 | Setup | NULL | 100.00 | Active | 2026-04-01 | NULL |
| 650e8400-e29b-41d4-a716-446655440002 | 750e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | NULL | 50.00 | Active | 2026-04-01 | NULL |
| 650e8400-e29b-41d4-a716-446655440003 | 750e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 550e8400-e29b-41d4-a716-446655440001 | 30.00 | Active | 2026-04-01 | NULL |
| 650e8400-e29b-41d4-a716-446655440004 | 750e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 550e8400-e29b-41d4-a716-446655440002 | 25.00 | Active | 2026-04-01 | NULL |
| 650e8400-e29b-41d4-a716-446655440005 | 750e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 550e8400-e29b-41d4-a716-446655440003 | 35.00 | Active | 2026-04-01 | NULL |
| 650e8400-e29b-41d4-a716-446655440006 | 750e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 550e8400-e29b-41d4-a716-446655440004 | 40.00 | Active | 2026-04-01 | NULL |
| 650e8400-e29b-41d4-a716-446655440007 | 750e8400-e29b-41d4-a716-446655440001 | PricePerRecord | NULL | 0.10 | Active | 2026-04-01 | NULL |
Key Changes:
- BillingConfigurationType enum values: Setup, MonthlySubscription, PricePerRecord
- MonthlySubscription with ScrapingSourceId: For scraping source pricing, set
billingconfigurationtype = 'MonthlySubscription'and specify thescrapingsourceid - MonthlySubscription without ScrapingSourceId: For base subscription, set
scrapingsourceid = NULL - All pricing is now centralized in this table for easier management and audit trail
Step 1: Dealer Onboarding (April 8, 2026)
Action: Admin creates dealer "ABC Auto Sales" and selects 2 scraping sources during onboarding
Table: dealermaster (after dealer creation)
| dealerid | dealername | resellerid | status | createdat |
|---|---|---|---|---|
| 850e8400-e29b-41d4-a716-446655440001 | ABC Auto Sales | 750e8400-e29b-41d4-a716-446655440001 | Active | 2026-04-08 |
Table: dealerscrapingsources (scraping sources assigned to dealer)
| id | dealerid | sourceid | status | createdat |
|---|---|---|---|---|
| 950e8400-e29b-41d4-a716-446655440001 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440001 | Active | 2026-04-08 |
| 950e8400-e29b-41d4-a716-446655440002 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440002 | Active | 2026-04-08 |
Step 2: BillingService Consumer Creates Billing Records
Calculation Context:
- Days in April: 30
- Onboarded: April 8
- Days remaining: 30 - 8 + 1 = 23 days
Billing Calculations:
- Setup Fee: $100.00 (no proration) - from billingconfiguration where
billingconfigurationtype = 'Setup' - Base Subscription (Prorated): ($50.00 / 30) * 23 = $38.33 - from billingconfiguration where
billingconfigurationtype = 'MonthlySubscription'andscrapingsourceid IS NULL - Craigslist (Prorated): ($30.00 / 30) * 23 = $23.00 - from billingconfiguration where
billingconfigurationtype = 'MonthlySubscription'andscrapingsourceid = '550e8400-e29b-41d4-a716-446655440001' - Facebook Marketplace (Prorated): ($25.00 / 30) * 23 = $19.17 - from billingconfiguration where
billingconfigurationtype = 'MonthlySubscription'andscrapingsourceid = '550e8400-e29b-41d4-a716-446655440002'
Table: billing (billing records created with BillingType enum values)
| billingid | dealerid | billingtype | billingconfigid | sourceid | amount | quantity | billingperiod | description | isinvoiced | createdat |
|---|---|---|---|---|---|---|---|---|---|---|
| a50e8400-e29b-41d4-a716-446655440001 | 850e8400-e29b-41d4-a716-446655440001 | Setup | 650e8400-e29b-41d4-a716-446655440001 | NULL | 100.00 | 1 | 2026-04 | One-time setup fee | false | 2026-04-08 |
| a50e8400-e29b-41d4-a716-446655440002 | 850e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 650e8400-e29b-41d4-a716-446655440002 | NULL | 38.33 | 23 | 2026-04 | Base subscription - Prorated for 23 days | false | 2026-04-08 |
| a50e8400-e29b-41d4-a716-446655440003 | 850e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 650e8400-e29b-41d4-a716-446655440003 | 550e8400-e29b-41d4-a716-446655440001 | 23.00 | 23 | 2026-04 | Craigslist - Prorated for 23 days | false | 2026-04-08 |
| a50e8400-e29b-41d4-a716-446655440004 | 850e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 650e8400-e29b-41d4-a716-446655440004 | 550e8400-e29b-41d4-a716-446655440002 | 19.17 | 23 | 2026-04 | Facebook Marketplace - Prorated for 23 days | false | 2026-04-08 |
Key Changes:
- BillingType enum values: Setup, MonthlySubscription, UsageBasedCharges (no more FirstMonthSubscription or ScrapingSourceAddition)
- Both base subscription and scraping source charges use
billingtype = 'MonthlySubscription' - Pricing fetched from
billingconfigurationtable usingbillingconfigidFK sourceidfield populated for scraping source charges to maintain referential integrity
Step 3: Mid-Cycle Scraping Source Addition (April 12)
Action: Dealer adds CarGurus scraping source mid-cycle
Table: dealerscrapingsources (new source added)
| id | dealerid | sourceid | status | createdat |
|---|---|---|---|---|
| 950e8400-e29b-41d4-a716-446655440001 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440001 | Active | 2026-04-08 |
| 950e8400-e29b-41d4-a716-446655440002 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440002 | Active | 2026-04-08 |
| 950e8400-e29b-41d4-a716-446655440003 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440003 | Active | 2026-04-12 |
BillingService Consumer Creates Prorated Charge:
- Days in April: 30
- Added: April 12
- Days remaining: 30 - 12 + 1 = 19 days
- CarGurus pricing: $35.00 (from billingconfiguration where
billingconfigurationtype = 'MonthlySubscription'andscrapingsourceid = '550e8400-e29b-41d4-a716-446655440003') - Prorated charge: ($35.00 / 30) * 19 = $22.17
Table: billing (new billing record created with MonthlySubscription type)
| billingid | dealerid | billingtype | billingconfigid | sourceid | amount | quantity | billingperiod | description | isinvoiced | createdat |
|---|---|---|---|---|---|---|---|---|---|---|
| a50e8400-e29b-41d4-a716-446655440005 | 850e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 650e8400-e29b-41d4-a716-446655440005 | 550e8400-e29b-41d4-a716-446655440003 | 22.17 | 19 | 2026-04 | CarGurus - Prorated for 19 days | false | 2026-04-12 |
Key Change: Scraping source additions now use billingtype = 'MonthlySubscription' (no more separate ScrapingSourceAddition type). Pricing is fetched from billingconfiguration based on the scrapingsourceid.
Step 4: Invoice Generation (Scheduled - April 15)
Scheduling Logic:
- Dealer onboarded April 8 (before 15th) → Invoice scheduled for April 15
- CarGurus source added April 12 (before 15th) → Invoice scheduled for April 15
- Both sets of charges combined into single invoice on April 15
Total Amount Calculation: $100.00 + $38.33 + $23.00 + $19.17 + $22.17 = $202.67
Table: invoice (invoice created at reseller level with billing cycle dates)
| invoiceid | resellerid | invoicenumber | invoicedate | totalamount | paymentamount | remainingamount | paymentstatus | billing_cycle_start | billing_cycle_end | createdat |
|---|---|---|---|---|---|---|---|---|---|---|
| b50e8400-e29b-41d4-a716-446655440001 | 750e8400-e29b-41d4-a716-446655440001 | INV-2026-04-0001 | 2026-04-15 | 202.67 | 0.00 | 202.67 | UnPaid | 2026-04-08 | 2026-04-30 | 2026-04-15 |
Key Changes:
- Invoice now uses
reselleridinstead ofdealerid(reseller-level invoicing) - Added
billing_cycle_startandbilling_cycle_endfields to support flexible billing periods - In this example, the billing cycle is from April 8 (dealer onboarding date) to April 30 (month end)
- For regular monthly invoices, cycle would typically be 1st to last day of the month
Table: invoiceitem (invoice items linking to ALL billing records)
| invoiceitemid | invoiceid | billingid | createdat |
|---|---|---|---|
| c50e8400-e29b-41d4-a716-446655440001 | b50e8400-e29b-41d4-a716-446655440001 | a50e8400-e29b-41d4-a716-446655440001 | 2026-04-15 |
| c50e8400-e29b-41d4-a716-446655440002 | b50e8400-e29b-41d4-a716-446655440001 | a50e8400-e29b-41d4-a716-446655440002 | 2026-04-15 |
| c50e8400-e29b-41d4-a716-446655440003 | b50e8400-e29b-41d4-a716-446655440001 | a50e8400-e29b-41d4-a716-446655440003 | 2026-04-15 |
| c50e8400-e29b-41d4-a716-446655440004 | b50e8400-e29b-41d4-a716-446655440001 | a50e8400-e29b-41d4-a716-446655440004 | 2026-04-15 |
| c50e8400-e29b-41d4-a716-446655440005 | b50e8400-e29b-41d4-a716-446655440001 | a50e8400-e29b-41d4-a716-446655440005 | 2026-04-15 |
Table: billing (updated - ALL billing records marked as invoiced)
| billingid | dealerid | billingtype | sourceid | amount | isinvoiced | createdat |
|---|---|---|---|---|---|---|
| a50e8400-e29b-41d4-a716-446655440001 | 850e8400-e29b-41d4-a716-446655440001 | Setup | NULL | 100.00 | true | 2026-04-08 |
| a50e8400-e29b-41d4-a716-446655440002 | 850e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | NULL | 38.33 | true | 2026-04-08 |
| a50e8400-e29b-41d4-a716-446655440003 | 850e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 550e8400-e29b-41d4-a716-446655440001 | 23.00 | true | 2026-04-08 |
| a50e8400-e29b-41d4-a716-446655440004 | 850e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 550e8400-e29b-41d4-a716-446655440002 | 19.17 | true | 2026-04-08 |
| a50e8400-e29b-41d4-a716-446655440005 | 850e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 550e8400-e29b-41d4-a716-446655440003 | 22.17 | true | 2026-04-12 |
Step 5: Another Source Addition (April 20)
Action: Dealer adds AutoTrader scraping source after the 15th
Table: dealerscrapingsources (AutoTrader source added)
| id | dealerid | sourceid | status | createdat |
|---|---|---|---|---|
| 950e8400-e29b-41d4-a716-446655440001 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440001 | Active | 2026-04-08 |
| 950e8400-e29b-41d4-a716-446655440002 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440002 | Active | 2026-04-08 |
| 950e8400-e29b-41d4-a716-446655440003 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440003 | Active | 2026-04-12 |
| 950e8400-e29b-41d4-a716-446655440004 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440004 | Active | 2026-04-20 |
BillingService Consumer Creates Prorated Charge:
- Days in April: 30
- Added: April 20
- Days remaining: 30 - 20 + 1 = 11 days
- AutoTrader pricing: $40.00 (from billingconfiguration where
billingconfigurationtype = 'MonthlySubscription'andscrapingsourceid = '550e8400-e29b-41d4-a716-446655440004') - Prorated charge: ($40.00 / 30) * 11 = $14.67
Scheduling Logic: Source added on April 20 (on/after 15th) → Invoice scheduled for May 1
Table: billing (AutoTrader billing record created with MonthlySubscription type)
| billingid | dealerid | billingtype | billingconfigid | sourceid | amount | quantity | billingperiod | description | isinvoiced | createdat |
|---|---|---|---|---|---|---|---|---|---|---|
| a50e8400-e29b-41d4-a716-446655440006 | 850e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 650e8400-e29b-41d4-a716-446655440006 | 550e8400-e29b-41d4-a716-446655440004 | 14.67 | 11 | 2026-04 | AutoTrader - Prorated for 11 days | false | 2026-04-20 |
Step 6: Partial Payment (April 22)
Action: Reseller makes partial payment covering first invoice partially
Table: payment (payment record created)
| paymentid | resellerid | totalamount | paymentdate | paymentmethod | transactionreference | receiptno | notes | createdat |
|---|---|---|---|---|---|---|---|---|
| d50e8400-e29b-41d4-a716-446655440001 | 750e8400-e29b-41d4-a716-446655440001 | 150.00 | 2026-04-22 | BankTransfer | TXN-2026-04-22-001 | RCP-2026-04-22-001 | Partial payment for dealer ABC Auto Sales... | 2026-04-22 |
Table: paymentdetails (payment distribution to invoices)
| paymentdetailsid | paymentid | invoiceid | paidamount | createdat |
|---|---|---|---|---|
| e50e8400-e29b-41d4-a716-446655440001 | d50e8400-e29b-41d4-a716-446655440001 | b50e8400-e29b-41d4-a716-446655440001 | 150.00 | 2026-04-22 |
Table: invoice (updated payment status)
| invoiceid | resellerid | invoicenumber | totalamount | paymentamount | remainingamount | paymentstatus | billing_cycle_start | billing_cycle_end |
|---|---|---|---|---|---|---|---|---|
| b50e8400-e29b-41d4-a716-446655440001 | 750e8400-e29b-41d4-a716-446655440001 | INV-2026-04-0001 | 202.67 | 150.00 | 52.67 | PartiallyPaid | 2026-04-08 | 2026-04-30 |
Step 7: Invoice Generation for Late Source Addition (May 1)
Scheduled Invoice: AutoTrader source added April 20 (on/after 15th) → Invoice generates on May 1
Table: invoice (second invoice created at reseller level)
| invoiceid | resellerid | invoicenumber | invoicedate | totalamount | paymentamount | remainingamount | paymentstatus | billing_cycle_start | billing_cycle_end | createdat |
|---|---|---|---|---|---|---|---|---|---|---|
| b50e8400-e29b-41d4-a716-446655440001 | 750e8400-e29b-41d4-a716-446655440001 | INV-2026-04-0001 | 2026-04-15 | 202.67 | 150.00 | 52.67 | PartiallyPaid | 2026-04-08 | 2026-04-30 | 2026-04-15 |
| b50e8400-e29b-41d4-a716-446655440002 | 750e8400-e29b-41d4-a716-446655440001 | INV-2026-05-0001 | 2026-05-01 | 14.67 | 0.00 | 14.67 | UnPaid | 2026-04-20 | 2026-04-30 | 2026-05-01 |
Note: The second invoice includes billing_cycle_start (April 20 - when source was added) and billing_cycle_end (April 30 - end of that month).
Table: invoiceitem (linking to AutoTrader billing record)
| invoiceitemid | invoiceid | billingid | createdat |
|---|---|---|---|
| c50e8400-e29b-41d4-a716-446655440006 | b50e8400-e29b-41d4-a716-446655440002 | a50e8400-e29b-41d4-a716-446655440006 | 2026-05-01 |
Table: billing (AutoTrader billing record marked as invoiced)
| billingid | dealerid | billingtype | sourceid | amount | isinvoiced | createdat |
|---|---|---|---|---|---|---|
| a50e8400-e29b-41d4-a716-446655440006 | 850e8400-e29b-41d4-a716-446655440001 | MonthlySubscription | 550e8400-e29b-41d4-a716-446655440004 | 14.67 | true | 2026-04-20 |
Step 8: Final Payment (May 5)
Action: Reseller completes payment for both outstanding invoices
Table: payment (second payment record)
| paymentid | resellerid | totalamount | paymentdate | paymentmethod | transactionreference | receiptno | notes |
|---|---|---|---|---|---|---|---|
| d50e8400-e29b-41d4-a716-446655440001 | 750e8400-e29b-41d4-a716-446655440001 | 150.00 | 2026-04-22 | BankTransfer | TXN-2026-04-22-001 | RCP-2026-04-22-001 | Partial payment for dealer ABC Auto Sales... |
| d50e8400-e29b-41d4-a716-446655440002 | 750e8400-e29b-41d4-a716-446655440001 | 67.34 | 2026-05-05 | OnlineTransfer | TXN-2026-05-05-001 | RCP-2026-05-05-001 | Final payment completing all outstanding... |
Table: paymentdetails (final payment distribution)
| paymentdetailsid | paymentid | invoiceid | paidamount | createdat |
|---|---|---|---|---|
| e50e8400-e29b-41d4-a716-446655440001 | d50e8400-e29b-41d4-a716-446655440001 | b50e8400-e29b-41d4-a716-446655440001 | 150.00 | 2026-04-22 |
| e50e8400-e29b-41d4-a716-446655440002 | d50e8400-e29b-41d4-a716-446655440002 | b50e8400-e29b-41d4-a716-446655440001 | 52.67 | 2026-05-05 |
| e50e8400-e29b-41d4-a716-446655440003 | d50e8400-e29b-41d4-a716-446655440002 | b50e8400-e29b-41d4-a716-446655440002 | 14.67 | 2026-05-05 |
Table: invoice (final state - all paid)
| invoiceid | resellerid | invoicenumber | totalamount | paymentamount | remainingamount | paymentstatus | billing_cycle_start | billing_cycle_end |
|---|---|---|---|---|---|---|---|---|
| b50e8400-e29b-41d4-a716-446655440001 | 750e8400-e29b-41d4-a716-446655440001 | INV-2026-04-0001 | 202.67 | 202.67 | 0.00 | Paid | 2026-04-08 | 2026-04-30 |
| b50e8400-e29b-41d4-a716-446655440002 | 750e8400-e29b-41d4-a716-446655440001 | INV-2026-05-0001 | 14.67 | 14.67 | 0.00 | Paid | 2026-04-20 | 2026-04-30 |
Step 9: Monthly Usage Billing (May 1 - Automated Job)
Background: During April 2026, dealer ABC Auto Sales processed vehicle records through their scraping sources. The system tracks all insert and update operations in the dealerdatatransaction table.
Hangfire Job: On May 1, the monthly usage billing job aggregates April's transaction data and creates usage billing records.
Table: dealerdatatransaction (sample April usage data)
| transactionid | dealerid | sourceid | operationtype | recordcount | transactiondate | createdat |
|---|---|---|---|---|---|---|
| f50e8400-e29b-41d4-a716-446655440001 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440001 | Insert | 150 | 2026-04-15 | 2026-04-15 |
| f50e8400-e29b-41d4-a716-446655440002 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440002 | Insert | 120 | 2026-04-15 | 2026-04-15 |
| f50e8400-e29b-41d4-a716-446655440003 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440003 | Insert | 95 | 2026-04-16 | 2026-04-16 |
| f50e8400-e29b-41d4-a716-446655440004 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440001 | Update | 45 | 2026-04-20 | 2026-04-20 |
| f50e8400-e29b-41d4-a716-446655440005 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440002 | Update | 38 | 2026-04-22 | 2026-04-22 |
| f50e8400-e29b-41d4-a716-446655440006 | 850e8400-e29b-41d4-a716-446655440001 | 550e8400-e29b-41d4-a716-446655440004 | Insert | 82 | 2026-04-25 | 2026-04-25 |
Usage Calculation:
- Total records processed in April: 150 + 120 + 95 + 45 + 38 + 82 = 530 records
- Per-record rate: $0.10 (from billingconfiguration where
billingconfigurationtype = 'PricePerRecord') - Total usage charge: 530 * $0.10 = $53.00
Table: billing (UsageBasedCharges billing record created)
| billingid | dealerid | billingtype | billingconfigid | sourceid | amount | quantity | billingperiod | description | isinvoiced | createdat |
|---|---|---|---|---|---|---|---|---|---|---|
| a50e8400-e29b-41d4-a716-446655440007 | 850e8400-e29b-41d4-a716-446655440001 | UsageBasedCharges | 650e8400-e29b-41d4-a716-446655440007 | NULL | 53.00 | 530 | 2026-04 | April usage - 530 records processed | false | 2026-05-01 |
Key Change: Usage billing now uses billingtype = 'UsageBasedCharges' (instead of MonthlyUsage). Pricing is fetched from billingconfiguration where billingconfigurationtype = 'PricePerRecord'.
Note: This billing record is created on May 1 for April's usage. It will be included in the next invoice generation (along with May's monthly subscription charges on May 1).
Invoice Generation Impact:
Since the MonthlyUsage billing record is created on May 1 (same day as the regular monthly subscription invoice generation), it would typically be included in a May invoice. However, in this example workflow, the May 1 invoice (INV-2026-05-0001) was already generated for the AutoTrader source addition.
In a complete production scenario, the May 1 invoice would be generated AFTER all billing records are created (subscription + usage), combining:
- May MonthlySubscription: $50.00 (base) + $135.00 (4 sources @ $30/$25/$35/$40 monthly) = $185.00
- April MonthlyUsage: $53.00
- Total May invoice: $238.00
For clarity in this workflow, we'll show the usage charge being invoiced separately:
Table: invoice (usage invoice generated on May 1 at reseller level)
| invoiceid | resellerid | invoicenumber | invoicedate | totalamount | paymentamount | remainingamount | paymentstatus | billing_cycle_start | billing_cycle_end | createdat |
|---|---|---|---|---|---|---|---|---|---|---|
| b50e8400-e29b-41d4-a716-446655440003 | 750e8400-e29b-41d4-a716-446655440001 | INV-2026-05-0002 | 2026-05-01 | 53.00 | 0.00 | 53.00 | UnPaid | 2026-04-01 | 2026-04-30 | 2026-05-01 |
Note: Usage invoice billing cycle is for the full previous month (April 1 - April 30) since usage is billed in arrears.
Table: invoiceitem (linking to usage billing record)
| invoiceitemid | invoiceid | billingid | createdat |
|---|---|---|---|
| c50e8400-e29b-41d4-a716-446655440007 | b50e8400-e29b-41d4-a716-446655440003 | a50e8400-e29b-41d4-a716-446655440007 | 2026-05-01 |
Table: billing (usage record marked as invoiced)
| billingid | dealerid | billingtype | amount | quantity | billingperiod | description | isinvoiced | createdat |
|---|---|---|---|---|---|---|---|---|
| a50e8400-e29b-41d4-a716-446655440007 | 850e8400-e29b-41d4-a716-446655440001 | UsageBasedCharges | 53.00 | 530 | 2026-04 | April usage - 530 records processed | true | 2026-05-01 |
Key Points:
- Usage billing is always for the previous month (April usage billed on May 1)
- The
quantityfield stores the total record count (530 records) - The
amountfield stores the calculated charge ($53.00) - Usage charges are created automatically by Hangfire job
- Billing type is now
UsageBasedCharges(instead of MonthlyUsage) - Pricing is fetched from
billingconfigurationwherebillingconfigurationtype = 'PricePerRecord'
Review & Approval
-
Reviewer(s):
- Sanket Mal
- Ribhu Gautam
-
Approval Date: [To be completed after reviews]
Deployment Considerations
Infrastructure
- PostgreSQL (already in place).
- Hangfire PostgreSQL storage for job persistence (shared with ManagementService or separate database).
- RabbitMQ broker for MassTransit messaging (already in place).
- Ensure sufficient disk space for 7+ years of billing data retention (compliance requirement).
- Database backup strategy includes all billing tables (
billing,invoice,invoiceitem,billingconfiguration,payment,paymentdetails). - Monitoring: OpenTelemetry metrics for billing job execution time, invoice generation count, billing record creation rate.
Rollout Steps
-
Database Migration:
- Deploy billing tables migration
- Verify indexes and constraints
- Seed initial pricing configuration
-
Contract Library Deployment:
- Deploy DTOs and entities
- Deploy MassTransit message contracts
- Rebuild dependent services
-
BillingService Backend Deployment:
- Deploy DAL, Service Layer, and API Controllers
- Deploy MassTransit consumers
- Deploy Hangfire jobs and scheduler
- Register components in dependency injection
-
ManagementService Event Publishers:
- Update DealerService to publish lifecycle events
- Restart ManagementService
-
Scope Configuration:
- Deploy billing scopes migration
- Assign scopes to appropriate roles
- Verify scope enforcement
-
Hangfire Scheduler Activation:
- Verify recurring jobs registered
- Test manual trigger in staging
- Validate invoice generation
-
Integration Testing:
- Test dealer onboarding and deactivation workflows
- Verify billing event creation
- Test invoice generation (manual and automated)
- Test invoice export
-
Monitoring & Alerts:
- Set up metrics dashboards
- Configure alerts for job failures
- Monitor database growth
-
Production Rollout:
- Deploy during maintenance window
- Enable scheduled jobs
- Monitor first automated run
- Communicate to users
Risks & Mitigations
| Risk | Impact | Mitigation |
|---|---|---|
| Scheduled job failures block monthly billing | Critical | Hangfire automatic retries with exponential backoff; manual invoice generation endpoint as fallback; alerts on job failures. |
| Duplicate billing events from message replay | High | Idempotent event handlers: check if event already exists for dealer+period before creating; use unique constraint or deduplication logic. |
| Incorrect proration calculations | High | Comprehensive unit tests covering all edge cases (Feb 28/29, month-end onboarding, leap years); peer review of proration formula. |
| Pricing configuration errors affect all invoices | High | Validation logic: pricing values must be positive; approval workflow for pricing updates; pricing history maintained; unique constraint ensures only one active config per reseller. |
| Missing billing events due to publisher failures | High | Transactional outbox pattern for guaranteed event delivery (future enhancement); compensating transaction: background job to detect onboarded dealers without billing events and backfill. |
| Performance degradation from large invoice generation | Medium | Batch processing: limit invoices per job run; parallel invoice generation per reseller; database query optimization with composite indexes; monitor job execution time. |
Calculation Examples
Example 1: Prorated Charges on Onboarding
- Dealer onboarded: March 10, 2026
- Setup fee: $100.00 (full amount, stored in billingconfiguration)
- Base monthly subscription: $50.00
- Scraping sources selected: 2 (Craigslist, Facebook Marketplace)
- Scraping source monthly fee: $30.00 per source
- Days in March: 31
- Days active in March: 31 - 10 + 1 = 22 days
- Prorated base subscription: ($50.00 / 31) * 22 = $35.48
- Prorated source charges: ($30.00 / 31) * 22 * 2 sources = $42.58
- Total initial invoice: $100.00 + $35.48 + $42.58 = $178.06
Example 2: Dealer Deactivation (Month-End Policy - No Prorated Credit)
- Dealer deactivation requested: April 15, 2026
- Deactivation scheduled for: April 30, 2026 (month-end)
- Monthly subscription: $50.00 (base) + $60.00 (2 sources @ $30 each) = $110.00
- Charge for April: Full $110.00 (no prorated credit)
- Dealer remains active and billing continues until April 30
- May invoice: Dealer excluded (status = Disabled)
Example 3: Usage Charge Calculation
- Dealer D1 usage in March: 1,000 total records (inserts + updates)
- Per-record cost: $0.10
- Total usage charge: 1,000 * $0.10 = $100.00
Example 4: Scraping Source Mid-Cycle Addition (Added Before 15th)
- Dealer onboarded: April 1, 2026
- Monthly subscription paid: $50.00
- Scraping source added: April 5 (Craigslist)
- Source monthly fee: $30.00
- Days in April: 30
- Days remaining in April: 30 - 5 + 1 = 26 days
- Prorated source charge: ($30.00 / 30) * 26 = $26.00
- Invoice generated: April 15 (because added before 15th)
- May subscription: $50.00 (base) + $30.00 (Craigslist full month) = $80.00
Example 4b: Scraping Source Mid-Cycle Addition (Added After 15th)
- Dealer onboarded: April 1, 2026
- Monthly subscription paid: $50.00
- Scraping source added: April 20 (Facebook Marketplace)
- Source monthly fee: $30.00
- Days in April: 30
- Days remaining in April: 30 - 20 + 1 = 11 days
- Prorated source charge: ($30.00 / 30) * 11 = $11.00
- Invoice generated: May 1 (because added on/after 15th)
- May subscription: $50.00 (base) + $30.00 (Facebook Marketplace full month) = $80.00
Example 5: Invoice Total Calculation
- Billing Item 1 (Setup): $100.00
- Billing Item 2 (FirstMonthSubscription - Prorated): $35.48
- Billing Item 3 (MonthlyUsage): $100.00
- Total Amount: $235.48
Database Relationships Diagram
resellermaster (1) ──────< (N) dealermaster (1) ──────< (N) billing
│
│ (FK: dealerid)
│
▼
invoice (1) ──────< (N) invoiceitem
│
│ (FK: billingid, nullable)
│
▼
billing
resellermaster (1) ──────< (N) billingconfiguration
dealer (1) ──────< (N) payment (1) ──────< (N) paymentdetails ──────> (N) invoice
Invoice Numbering Logic
Format: INV-YYYY-MM-NNNN (e.g., INV-2026-04-0001, INV-2026-04-0002)
- Sequential counter resets per billing period
- Queries maximum existing number for billing period
- Increments by 1 and formats with 4-digit padding
- Use serializable transaction isolation or database sequence for concurrency safety
Proration Formula Reference
Formula: (MonthlyRate / DaysInMonth) * DaysActive
Calculation Steps:
- Get year and month from start date
- Calculate days in month using DateTime.DaysInMonth (handles leap years)
- Calculate days active: (EndDate.Day - StartDate.Day) + 1
- Apply formula and round to 2 decimal places
Edge Cases Handled:
- February leap years (28 vs. 29 days)
- Month-end onboarding (31st day of month)
- Scraping source added on last day of month (1 day prorated charge)
Not Applicable:
- Mid-month deactivation credits are NOT calculated - month-end scheduling policy ensures full monthly billing with no prorated refunds.