Skip to main content
Version: MarketPulse

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

VersionDateChangesAuthor
1.02026-04-06Initial draft for comprehensive Billing SystemAshik
1.12026-04-16Updated OAuth scopes (billing.settings.manage, billing.settings.view.all); Unified update endpoint to use BillingConfigurationRequest model; Removed separate UpdateBillingConfigurationRequest DTOAshik

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

  1. 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 (isInvoiced flag).
  2. Pricing Configuration

    • Store pricing tiers in billingconfiguration database 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 effectivefrom and effectiveto date ranges.
    • Active configurations have status = 'Active' and effectiveto = NULL.
    • When updating pricing, set effectiveto on the old record and create a new record with effectivefrom set 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.
  3. 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
  4. 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-NNNN with sequential counter per billing period.
    • Billing cycle dates support flexible periods (e.g., 1st-15th, 16th-end of month).
  5. 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 dealerdatatransaction records 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.
  6. 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.
  7. 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.
  8. Usage Tracking & Aggregation

    • Query dealerdatatransaction table 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.
  9. 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 scheduledremovaldate to 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."
    • 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."
  10. Billing Configuration Management

    • Fetch current pricing via GET /billing/configuration (requires billing.settings.view.all scope).
    • 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/configuration with billing.settings.manage scope.
    • Update scheduled pricing (admin only) via PUT /billing/configuration/{billingconfigid} with billing.settings.manage scope.
      • 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).
    • 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 (effectivefrom and effectiveto).
    • When changing active pricing, set effectiveto on the old record and create new active config with effectivefrom (maintains history).
    • Historical configurations (with effectiveto set) are included in paginated results for audit trail.
  11. 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

  1. Data Integrity

    • Event-sourcing pattern: All charges stored as immutable billing records (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).
  2. 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.
  3. 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).
  4. 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:

  1. 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.
  2. DealerService persists dealer record to database (status = Active) along with selected scraping sources.
  3. On success, DealerService publishes DealerOnboardedEvent via MassTransit:
    • Message payload: { DealerId, ResellerId, OnboardedDate, ActiveSourceCount, ScrapingSourceIds[] }
  4. DealerOnboardedConsumer (in BillingService) receives event.
  5. Consumer calls IBillingEngineService.ProcessDealerOnboardingAsync(...).
  6. Billing engine:
    • Fetches active pricing configurations for the dealer's reseller (setup fee, monthly subscription).
    • Fetches scraping source prices from billingconfiguration table where billingconfigurationtype = 'MonthlySubscription' and scrapingsourceid matches.
    • 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)
  7. Billing records are now available for invoice generation (on-demand or scheduled).

Dealer Deactivation Billing Workflow:

  1. User disables dealer via ManagementService API (PUT /dealers/{id}/status) changing status to Disable.
  2. DealerService does not immediately disable the dealer.
  3. Instead, system sets scheduleddeactivationdate to last day of current month on the dealer record.
  4. On success, DealerService publishes DealerDeactivationScheduledEvent:
    • Message payload: { DealerId, ScheduledDeactivationDate }
  5. Dealer remains active and continues billing until scheduled deactivation date.
  6. All scraping sources continue operating until scheduled deactivation.
  7. Display warning: "Dealer deactivation scheduled for [month-end date]. Full monthly charges apply."
  8. On 1st of next month, cleanup job executes dealer deactivations:
    • Query dealers with scheduleddeactivationdate <= yesterday's date.
    • Update dealer status to Disable.
    • Publish DealerDeactivatedEvent for audit trail.
  9. No prorated credit issued for partial month when dealer is disabled.
  10. Dealer excluded from future subscription invoices after scheduled deactivation (queried from dealermaster table).

Scraping Source Addition Billing Workflow (Mid-Cycle):

  1. User adds scraping source to dealer via ManagementService API (POST /dealers/{id}/sources).
  2. ScrapingSourceService persists scraping source record to database with status = Active.
  3. On success, ScrapingSourceService publishes ScrapingSourceAddedEvent via MassTransit:
    • Message payload: { DealerId, SourceType, AddedDate, SourceMonthlyFee }
  4. ScrapingSourceAddedConsumer (in BillingService) receives event.
  5. Consumer calls IBillingEngineService.ProcessScrapingSourceAdditionAsync(...).
  6. Billing engine:
    • Checks if addition occurred after 1st of month (mid-cycle addition).
    • If mid-cycle:
      • Fetches source monthly fee from billingconfiguration table where billingconfigurationtype = 'MonthlySubscription' and scrapingsourceid matches.
      • Calculates prorated charge for remaining days in current month.
      • Creates MonthlySubscription billing record with prorated amount and isinvoiced = 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
  7. Scraping source included in next month's full subscription invoice at full monthly rate.
  8. 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):

  1. 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 MonthlySubscription billing records (for scraping sources) where:
      • AddedDate is between 1st-14th of current month
      • isinvoiced = 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.
    • Job logs success/failure per reseller with OpenTelemetry tracing.
  2. Month-End Job (1st of month):

    • Runs as part of monthly subscription billing job.
    • Queries all uninvoiced MonthlySubscription billing records (for scraping sources) where:
      • AddedDate is between 15th-31st of previous month
      • isinvoiced = 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:

  1. User disables scraping source via ManagementService API (DELETE /dealers/{id}/sources/{sourceid} or PUT /dealers/{id}/sources/{sourceid}/disable).
  2. ScrapingSourceService does not immediately delete or disable the source.
  3. Instead, system sets scheduledremovaldate to last day of current month on the scraping source record.
  4. System publishes ScrapingSourceRemovalScheduledEvent:
    • Message payload: { DealerId, SourceType, ScheduledRemovalDate }
  5. Source remains active and continues operating until scheduled removal date.
  6. Source continues to be billed in monthly subscription until scheduled removal.
  7. Display warning to user: "This source will be removed on [month-end date]. You will continue to be billed until that date."
  8. 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 ScrapingSourceRemovedEvent for audit trail.
  9. No prorated credit issued for partial month when source is disabled.
  10. Next month's subscription invoice excludes the removed source.

Monthly Subscription Invoice Generation Workflow (Automated):

  1. Hangfire recurring job triggers on 1st of month at midnight UTC (0 0 1 * * cron).
  2. MonthlyBillingJob.RunMonthlySubscriptionBillingAsync() executes.
  3. Job queries all active resellers from resellermaster table (status = Active).
  4. For each reseller:
    • Calls IBillingEngineService.GenerateMonthlySubscriptionInvoiceAsync(resellerId, billingPeriod).
    • Billing period = current month (e.g., "2026-04" for April billing).
  5. Billing engine:
    • Queries all active dealers under the reseller.
    • For each dealer, queries all active scraping sources (excluding those with scheduledremovaldate in the past).
    • Calculates monthly subscription charge including:
      • Base monthly subscription fee (from billingconfiguration where billingconfigurationtype = 'MonthlySubscription' and scrapingsourceid IS NULL)
      • Per-source monthly fees for all active scraping sources (from billingconfiguration where billingconfigurationtype = 'MonthlySubscription' and scrapingsourceid matches)
    • 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).
  6. Job logs success/failure per reseller with OpenTelemetry tracing.
  7. Optional: Publish InvoiceGeneratedEvent for downstream notifications (email, reseller portal alert).

Monthly Usage Invoice Generation Workflow (Automated):

  1. Hangfire recurring job triggers on 1st of month at midnight UTC (0 0 1 * * cron).
  2. MonthlyBillingJob.RunMonthlyUsageBillingAsync() executes.
  3. Job calculates previous billing period: If today is April 1, billing period = "2026-03" (March usage).
  4. Job queries all active resellers from resellermaster table.
  5. For each reseller:
    • Calls IBillingEngineService.GenerateUsageInvoiceAsync(resellerId, billingPeriod).
  6. 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 billingconfiguration for dealer's reseller:
      • Queries usage price: SELECT price FROM billingconfiguration WHERE resellerid = ? AND billingconfigurationtype = 'PricePerRecord' AND status = 'Active' AND effectiveto IS NULL
    • 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.
  7. Usage invoice spans previous month's data (postpaid billing model).

Manual Invoice Generation Workflow (On-Demand):

  1. Admin user calls invoice generation endpoint with reseller ID and billing period.
  2. Controller validates JWT and billing.write scope.
  3. Controller calls service method to generate invoice.
  4. Service executes same invoice generation logic as scheduled jobs.
  5. Returns invoice ID and details in response.
  6. 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 billingconfiguration table and stored as VARCHAR strings: "Setup", "MonthlySubscription", "PricePerRecord". When using MonthlySubscription configuration type, you must provide a ScrapingSourceId.
  • BillingType is used in the billing table 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&lt;T&gt;, ServerPaginatedData&lt;T&gt;, or PaginatedResponse&lt;T&gt; 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

EndpointMethodPayloadResponseStatus Codes
/billing/configurationPOSTBillingConfigurationRequestCommonResponse200, 400, 403, 500
/billing/configuration/{billingconfigid}PUTBillingConfigurationRequestCommonResponse200, 400, 403, 404, 500
/billing/configurationGETBillingConfigurationFilterServerPaginatedData&lt;BillingConfiguration&gt;200, 403, 404, 500
/billing/invoicesPOSTInvoiceRequestCommonResponse201, 400, 403, 500
/billing/invoicesGETInvoiceFilter (query params)ServerPaginatedData&lt;InvoiceDetails&gt;200, 400, 403, 500
/billing/invoices/{invoiceid}GETPath: invoiceid (Guid)InvoiceDetails200, 403, 404, 500
/billing/invoices/{invoiceid}/exportGETPath: invoiceid (Guid)File (PDF)200, 403, 404, 500
/billing/paymentsPOSTPaymentRequestCommonResponse201, 400, 403, 500
/billing/paymentsGETPaymentFilter (query params)ServerPaginatedData&lt;PaymentDetails&gt;200, 400, 403, 500
/billing/payments/{paymentid}GETPath: paymentid (Guid)PaymentDetails200, 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 dealerdatatransaction records (inserts + updates combined).
  • ✅ Invoice numbering follows date-based format INV-YYYY-MM-NNNN with 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.manage scope).
  • ✅ Scheduled pricing configurations can be updated via PUT endpoint to modify price, effectiveFrom, or effectiveTo fields (admin only, billing.settings.manage scope).
  • ✅ 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.all scope can view billing configurations (403 otherwise).
  • ✅ Only users with billing.read scope can view invoices (403 otherwise).
  • ✅ Only users with billing.write scope can generate invoices and record payments (403 otherwise).
  • ✅ Only users with billing.settings.manage scope 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)

sourceidsourcenamestatuscreatedat
550e8400-e29b-41d4-a716-446655440001CraigslistActive2026-01-01
550e8400-e29b-41d4-a716-446655440002Facebook MarketplaceActive2026-01-01
550e8400-e29b-41d4-a716-446655440003CarGurusActive2026-01-01
550e8400-e29b-41d4-a716-446655440004AutoTraderActive2026-01-01

Table: billingconfiguration (Reseller pricing for all billing types including scraping sources)

billingconfigidreselleridbillingconfigurationtypescrapingsourceidpricestatuseffectivefromeffectiveto
650e8400-e29b-41d4-a716-446655440001750e8400-e29b-41d4-a716-446655440001SetupNULL100.00Active2026-04-01NULL
650e8400-e29b-41d4-a716-446655440002750e8400-e29b-41d4-a716-446655440001MonthlySubscriptionNULL50.00Active2026-04-01NULL
650e8400-e29b-41d4-a716-446655440003750e8400-e29b-41d4-a716-446655440001MonthlySubscription550e8400-e29b-41d4-a716-44665544000130.00Active2026-04-01NULL
650e8400-e29b-41d4-a716-446655440004750e8400-e29b-41d4-a716-446655440001MonthlySubscription550e8400-e29b-41d4-a716-44665544000225.00Active2026-04-01NULL
650e8400-e29b-41d4-a716-446655440005750e8400-e29b-41d4-a716-446655440001MonthlySubscription550e8400-e29b-41d4-a716-44665544000335.00Active2026-04-01NULL
650e8400-e29b-41d4-a716-446655440006750e8400-e29b-41d4-a716-446655440001MonthlySubscription550e8400-e29b-41d4-a716-44665544000440.00Active2026-04-01NULL
650e8400-e29b-41d4-a716-446655440007750e8400-e29b-41d4-a716-446655440001PricePerRecordNULL0.10Active2026-04-01NULL

Key Changes:

  • BillingConfigurationType enum values: Setup, MonthlySubscription, PricePerRecord
  • MonthlySubscription with ScrapingSourceId: For scraping source pricing, set billingconfigurationtype = 'MonthlySubscription' and specify the scrapingsourceid
  • 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)

dealeriddealernamereselleridstatuscreatedat
850e8400-e29b-41d4-a716-446655440001ABC Auto Sales750e8400-e29b-41d4-a716-446655440001Active2026-04-08

Table: dealerscrapingsources (scraping sources assigned to dealer)

iddealeridsourceidstatuscreatedat
950e8400-e29b-41d4-a716-446655440001850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440001Active2026-04-08
950e8400-e29b-41d4-a716-446655440002850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440002Active2026-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:

  1. Setup Fee: $100.00 (no proration) - from billingconfiguration where billingconfigurationtype = 'Setup'
  2. Base Subscription (Prorated): ($50.00 / 30) * 23 = $38.33 - from billingconfiguration where billingconfigurationtype = 'MonthlySubscription' and scrapingsourceid IS NULL
  3. Craigslist (Prorated): ($30.00 / 30) * 23 = $23.00 - from billingconfiguration where billingconfigurationtype = 'MonthlySubscription' and scrapingsourceid = '550e8400-e29b-41d4-a716-446655440001'
  4. Facebook Marketplace (Prorated): ($25.00 / 30) * 23 = $19.17 - from billingconfiguration where billingconfigurationtype = 'MonthlySubscription' and scrapingsourceid = '550e8400-e29b-41d4-a716-446655440002'

Table: billing (billing records created with BillingType enum values)

billingiddealeridbillingtypebillingconfigidsourceidamountquantitybillingperioddescriptionisinvoicedcreatedat
a50e8400-e29b-41d4-a716-446655440001850e8400-e29b-41d4-a716-446655440001Setup650e8400-e29b-41d4-a716-446655440001NULL100.0012026-04One-time setup feefalse2026-04-08
a50e8400-e29b-41d4-a716-446655440002850e8400-e29b-41d4-a716-446655440001MonthlySubscription650e8400-e29b-41d4-a716-446655440002NULL38.33232026-04Base subscription - Prorated for 23 daysfalse2026-04-08
a50e8400-e29b-41d4-a716-446655440003850e8400-e29b-41d4-a716-446655440001MonthlySubscription650e8400-e29b-41d4-a716-446655440003550e8400-e29b-41d4-a716-44665544000123.00232026-04Craigslist - Prorated for 23 daysfalse2026-04-08
a50e8400-e29b-41d4-a716-446655440004850e8400-e29b-41d4-a716-446655440001MonthlySubscription650e8400-e29b-41d4-a716-446655440004550e8400-e29b-41d4-a716-44665544000219.17232026-04Facebook Marketplace - Prorated for 23 daysfalse2026-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 billingconfiguration table using billingconfigid FK
  • sourceid field 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)

iddealeridsourceidstatuscreatedat
950e8400-e29b-41d4-a716-446655440001850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440001Active2026-04-08
950e8400-e29b-41d4-a716-446655440002850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440002Active2026-04-08
950e8400-e29b-41d4-a716-446655440003850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440003Active2026-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' and scrapingsourceid = '550e8400-e29b-41d4-a716-446655440003')
  • Prorated charge: ($35.00 / 30) * 19 = $22.17

Table: billing (new billing record created with MonthlySubscription type)

billingiddealeridbillingtypebillingconfigidsourceidamountquantitybillingperioddescriptionisinvoicedcreatedat
a50e8400-e29b-41d4-a716-446655440005850e8400-e29b-41d4-a716-446655440001MonthlySubscription650e8400-e29b-41d4-a716-446655440005550e8400-e29b-41d4-a716-44665544000322.17192026-04CarGurus - Prorated for 19 daysfalse2026-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)

invoiceidreselleridinvoicenumberinvoicedatetotalamountpaymentamountremainingamountpaymentstatusbilling_cycle_startbilling_cycle_endcreatedat
b50e8400-e29b-41d4-a716-446655440001750e8400-e29b-41d4-a716-446655440001INV-2026-04-00012026-04-15202.670.00202.67UnPaid2026-04-082026-04-302026-04-15

Key Changes:

  • Invoice now uses resellerid instead of dealerid (reseller-level invoicing)
  • Added billing_cycle_start and billing_cycle_end fields 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)

invoiceitemidinvoiceidbillingidcreatedat
c50e8400-e29b-41d4-a716-446655440001b50e8400-e29b-41d4-a716-446655440001a50e8400-e29b-41d4-a716-4466554400012026-04-15
c50e8400-e29b-41d4-a716-446655440002b50e8400-e29b-41d4-a716-446655440001a50e8400-e29b-41d4-a716-4466554400022026-04-15
c50e8400-e29b-41d4-a716-446655440003b50e8400-e29b-41d4-a716-446655440001a50e8400-e29b-41d4-a716-4466554400032026-04-15
c50e8400-e29b-41d4-a716-446655440004b50e8400-e29b-41d4-a716-446655440001a50e8400-e29b-41d4-a716-4466554400042026-04-15
c50e8400-e29b-41d4-a716-446655440005b50e8400-e29b-41d4-a716-446655440001a50e8400-e29b-41d4-a716-4466554400052026-04-15

Table: billing (updated - ALL billing records marked as invoiced)

billingiddealeridbillingtypesourceidamountisinvoicedcreatedat
a50e8400-e29b-41d4-a716-446655440001850e8400-e29b-41d4-a716-446655440001SetupNULL100.00true2026-04-08
a50e8400-e29b-41d4-a716-446655440002850e8400-e29b-41d4-a716-446655440001MonthlySubscriptionNULL38.33true2026-04-08
a50e8400-e29b-41d4-a716-446655440003850e8400-e29b-41d4-a716-446655440001MonthlySubscription550e8400-e29b-41d4-a716-44665544000123.00true2026-04-08
a50e8400-e29b-41d4-a716-446655440004850e8400-e29b-41d4-a716-446655440001MonthlySubscription550e8400-e29b-41d4-a716-44665544000219.17true2026-04-08
a50e8400-e29b-41d4-a716-446655440005850e8400-e29b-41d4-a716-446655440001MonthlySubscription550e8400-e29b-41d4-a716-44665544000322.17true2026-04-12

Step 5: Another Source Addition (April 20)

Action: Dealer adds AutoTrader scraping source after the 15th

Table: dealerscrapingsources (AutoTrader source added)

iddealeridsourceidstatuscreatedat
950e8400-e29b-41d4-a716-446655440001850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440001Active2026-04-08
950e8400-e29b-41d4-a716-446655440002850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440002Active2026-04-08
950e8400-e29b-41d4-a716-446655440003850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440003Active2026-04-12
950e8400-e29b-41d4-a716-446655440004850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440004Active2026-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' and scrapingsourceid = '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)

billingiddealeridbillingtypebillingconfigidsourceidamountquantitybillingperioddescriptionisinvoicedcreatedat
a50e8400-e29b-41d4-a716-446655440006850e8400-e29b-41d4-a716-446655440001MonthlySubscription650e8400-e29b-41d4-a716-446655440006550e8400-e29b-41d4-a716-44665544000414.67112026-04AutoTrader - Prorated for 11 daysfalse2026-04-20

Step 6: Partial Payment (April 22)

Action: Reseller makes partial payment covering first invoice partially

Table: payment (payment record created)

paymentidreselleridtotalamountpaymentdatepaymentmethodtransactionreferencereceiptnonotescreatedat
d50e8400-e29b-41d4-a716-446655440001750e8400-e29b-41d4-a716-446655440001150.002026-04-22BankTransferTXN-2026-04-22-001RCP-2026-04-22-001Partial payment for dealer ABC Auto Sales...2026-04-22

Table: paymentdetails (payment distribution to invoices)

paymentdetailsidpaymentidinvoiceidpaidamountcreatedat
e50e8400-e29b-41d4-a716-446655440001d50e8400-e29b-41d4-a716-446655440001b50e8400-e29b-41d4-a716-446655440001150.002026-04-22

Table: invoice (updated payment status)

invoiceidreselleridinvoicenumbertotalamountpaymentamountremainingamountpaymentstatusbilling_cycle_startbilling_cycle_end
b50e8400-e29b-41d4-a716-446655440001750e8400-e29b-41d4-a716-446655440001INV-2026-04-0001202.67150.0052.67PartiallyPaid2026-04-082026-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)

invoiceidreselleridinvoicenumberinvoicedatetotalamountpaymentamountremainingamountpaymentstatusbilling_cycle_startbilling_cycle_endcreatedat
b50e8400-e29b-41d4-a716-446655440001750e8400-e29b-41d4-a716-446655440001INV-2026-04-00012026-04-15202.67150.0052.67PartiallyPaid2026-04-082026-04-302026-04-15
b50e8400-e29b-41d4-a716-446655440002750e8400-e29b-41d4-a716-446655440001INV-2026-05-00012026-05-0114.670.0014.67UnPaid2026-04-202026-04-302026-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)

invoiceitemidinvoiceidbillingidcreatedat
c50e8400-e29b-41d4-a716-446655440006b50e8400-e29b-41d4-a716-446655440002a50e8400-e29b-41d4-a716-4466554400062026-05-01

Table: billing (AutoTrader billing record marked as invoiced)

billingiddealeridbillingtypesourceidamountisinvoicedcreatedat
a50e8400-e29b-41d4-a716-446655440006850e8400-e29b-41d4-a716-446655440001MonthlySubscription550e8400-e29b-41d4-a716-44665544000414.67true2026-04-20

Step 8: Final Payment (May 5)

Action: Reseller completes payment for both outstanding invoices

Table: payment (second payment record)

paymentidreselleridtotalamountpaymentdatepaymentmethodtransactionreferencereceiptnonotes
d50e8400-e29b-41d4-a716-446655440001750e8400-e29b-41d4-a716-446655440001150.002026-04-22BankTransferTXN-2026-04-22-001RCP-2026-04-22-001Partial payment for dealer ABC Auto Sales...
d50e8400-e29b-41d4-a716-446655440002750e8400-e29b-41d4-a716-44665544000167.342026-05-05OnlineTransferTXN-2026-05-05-001RCP-2026-05-05-001Final payment completing all outstanding...

Table: paymentdetails (final payment distribution)

paymentdetailsidpaymentidinvoiceidpaidamountcreatedat
e50e8400-e29b-41d4-a716-446655440001d50e8400-e29b-41d4-a716-446655440001b50e8400-e29b-41d4-a716-446655440001150.002026-04-22
e50e8400-e29b-41d4-a716-446655440002d50e8400-e29b-41d4-a716-446655440002b50e8400-e29b-41d4-a716-44665544000152.672026-05-05
e50e8400-e29b-41d4-a716-446655440003d50e8400-e29b-41d4-a716-446655440002b50e8400-e29b-41d4-a716-44665544000214.672026-05-05

Table: invoice (final state - all paid)

invoiceidreselleridinvoicenumbertotalamountpaymentamountremainingamountpaymentstatusbilling_cycle_startbilling_cycle_end
b50e8400-e29b-41d4-a716-446655440001750e8400-e29b-41d4-a716-446655440001INV-2026-04-0001202.67202.670.00Paid2026-04-082026-04-30
b50e8400-e29b-41d4-a716-446655440002750e8400-e29b-41d4-a716-446655440001INV-2026-05-000114.6714.670.00Paid2026-04-202026-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)

transactioniddealeridsourceidoperationtyperecordcounttransactiondatecreatedat
f50e8400-e29b-41d4-a716-446655440001850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440001Insert1502026-04-152026-04-15
f50e8400-e29b-41d4-a716-446655440002850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440002Insert1202026-04-152026-04-15
f50e8400-e29b-41d4-a716-446655440003850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440003Insert952026-04-162026-04-16
f50e8400-e29b-41d4-a716-446655440004850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440001Update452026-04-202026-04-20
f50e8400-e29b-41d4-a716-446655440005850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440002Update382026-04-222026-04-22
f50e8400-e29b-41d4-a716-446655440006850e8400-e29b-41d4-a716-446655440001550e8400-e29b-41d4-a716-446655440004Insert822026-04-252026-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)

billingiddealeridbillingtypebillingconfigidsourceidamountquantitybillingperioddescriptionisinvoicedcreatedat
a50e8400-e29b-41d4-a716-446655440007850e8400-e29b-41d4-a716-446655440001UsageBasedCharges650e8400-e29b-41d4-a716-446655440007NULL53.005302026-04April usage - 530 records processedfalse2026-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)

invoiceidreselleridinvoicenumberinvoicedatetotalamountpaymentamountremainingamountpaymentstatusbilling_cycle_startbilling_cycle_endcreatedat
b50e8400-e29b-41d4-a716-446655440003750e8400-e29b-41d4-a716-446655440001INV-2026-05-00022026-05-0153.000.0053.00UnPaid2026-04-012026-04-302026-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)

invoiceitemidinvoiceidbillingidcreatedat
c50e8400-e29b-41d4-a716-446655440007b50e8400-e29b-41d4-a716-446655440003a50e8400-e29b-41d4-a716-4466554400072026-05-01

Table: billing (usage record marked as invoiced)

billingiddealeridbillingtypeamountquantitybillingperioddescriptionisinvoicedcreatedat
a50e8400-e29b-41d4-a716-446655440007850e8400-e29b-41d4-a716-446655440001UsageBasedCharges53.005302026-04April usage - 530 records processedtrue2026-05-01

Key Points:

  • Usage billing is always for the previous month (April usage billed on May 1)
  • The quantity field stores the total record count (530 records)
  • The amount field 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 billingconfiguration where billingconfigurationtype = '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

  1. Database Migration:

    • Deploy billing tables migration
    • Verify indexes and constraints
    • Seed initial pricing configuration
  2. Contract Library Deployment:

    • Deploy DTOs and entities
    • Deploy MassTransit message contracts
    • Rebuild dependent services
  3. BillingService Backend Deployment:

    • Deploy DAL, Service Layer, and API Controllers
    • Deploy MassTransit consumers
    • Deploy Hangfire jobs and scheduler
    • Register components in dependency injection
  4. ManagementService Event Publishers:

    • Update DealerService to publish lifecycle events
    • Restart ManagementService
  5. Scope Configuration:

    • Deploy billing scopes migration
    • Assign scopes to appropriate roles
    • Verify scope enforcement
  6. Hangfire Scheduler Activation:

    • Verify recurring jobs registered
    • Test manual trigger in staging
    • Validate invoice generation
  7. Integration Testing:

    • Test dealer onboarding and deactivation workflows
    • Verify billing event creation
    • Test invoice generation (manual and automated)
    • Test invoice export
  8. Monitoring & Alerts:

    • Set up metrics dashboards
    • Configure alerts for job failures
    • Monitor database growth
  9. Production Rollout:

    • Deploy during maintenance window
    • Enable scheduled jobs
    • Monitor first automated run
    • Communicate to users

Risks & Mitigations

RiskImpactMitigation
Scheduled job failures block monthly billingCriticalHangfire automatic retries with exponential backoff; manual invoice generation endpoint as fallback; alerts on job failures.
Duplicate billing events from message replayHighIdempotent event handlers: check if event already exists for dealer+period before creating; use unique constraint or deduplication logic.
Incorrect proration calculationsHighComprehensive unit tests covering all edge cases (Feb 28/29, month-end onboarding, leap years); peer review of proration formula.
Pricing configuration errors affect all invoicesHighValidation 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 failuresHighTransactional 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 generationMediumBatch 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:

  1. Get year and month from start date
  2. Calculate days in month using DateTime.DaysInMonth (handles leap years)
  3. Calculate days active: (EndDate.Day - StartDate.Day) + 1
  4. 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.