13 KiB
13 KiB
Milestone 3: Billing Engine
Generate invoices, process payments, and manage account credits.
Overview
The Billing Engine transforms metered usage into invoices, processes SYNOR payments, and manages prepaid balances and credits.
Components
3.1 Invoice Generator
// crates/synor-economics/src/billing/invoice.rs
/// Invoice for a billing period
pub struct Invoice {
/// Invoice ID
pub invoice_id: InvoiceId,
/// Account ID
pub account_id: AccountId,
/// Billing period
pub period_start: u64,
pub period_end: u64,
/// Line items
pub line_items: Vec<LineItem>,
/// Subtotal before discounts
pub subtotal_micro_synor: u64,
/// Applied discounts
pub discounts: Vec<AppliedDiscount>,
/// Credits applied
pub credits_applied: u64,
/// Total due
pub total_due_micro_synor: u64,
/// Invoice status
pub status: InvoiceStatus,
/// Due date
pub due_date: u64,
/// Payment transaction (if paid)
pub payment_tx: Option<TxId>,
/// Created at
pub created_at: u64,
}
pub struct LineItem {
pub service: ServiceType,
pub resource: ResourceType,
pub description: String,
pub quantity: u64,
pub unit: MeteringUnit,
pub unit_price_micro_synor: u64,
pub amount_micro_synor: u64,
}
pub struct AppliedDiscount {
pub discount_type: DiscountType,
pub description: String,
pub amount_micro_synor: u64,
}
pub enum DiscountType {
/// Volume discount based on usage tier
Volume { tier: u8 },
/// Commitment discount (reserved capacity)
Commitment { commitment_id: CommitmentId },
/// Promotional credit
Promotional { promo_code: String },
/// Referral credit
Referral { referrer_id: AccountId },
}
pub enum InvoiceStatus {
Draft,
Finalized,
Sent,
Paid,
Overdue,
Voided,
}
pub struct InvoiceGenerator {
metering: UsageAggregator,
pricing: PricingEngine,
discounts: DiscountEngine,
}
impl InvoiceGenerator {
/// Generate invoice for an account
pub async fn generate_invoice(
&self,
account: &AccountId,
period_start: u64,
period_end: u64,
) -> Result<Invoice, BillingError> {
// Get usage summary
let usage = self.metering.get_usage_summary(account, period_start, period_end).await?;
// Convert to line items
let line_items = self.usage_to_line_items(&usage);
// Calculate subtotal
let subtotal = line_items.iter().map(|li| li.amount_micro_synor).sum();
// Apply discounts
let discounts = self.discounts.calculate_discounts(account, &line_items).await?;
let discount_total: u64 = discounts.iter().map(|d| d.amount_micro_synor).sum();
// Apply credits
let available_credits = self.get_available_credits(account).await?;
let credits_applied = std::cmp::min(available_credits, subtotal - discount_total);
// Calculate total
let total_due = subtotal.saturating_sub(discount_total).saturating_sub(credits_applied);
Ok(Invoice {
invoice_id: InvoiceId::new(),
account_id: account.clone(),
period_start,
period_end,
line_items,
subtotal_micro_synor: subtotal,
discounts,
credits_applied,
total_due_micro_synor: total_due,
status: InvoiceStatus::Draft,
due_date: period_end + 30 * 24 * 3600, // 30 days
payment_tx: None,
created_at: current_timestamp(),
})
}
}
3.2 Payment Processor
// crates/synor-economics/src/billing/payment.rs
/// Payment processor for SYNOR token payments
pub struct PaymentProcessor {
/// Synor blockchain client
blockchain: SynorClient,
/// Billing contract address
billing_contract: Address,
}
pub struct PaymentRequest {
pub invoice_id: InvoiceId,
pub amount_micro_synor: u64,
pub payer: Address,
}
pub struct PaymentResult {
pub payment_id: PaymentId,
pub tx_id: TxId,
pub amount_micro_synor: u64,
pub timestamp: u64,
pub status: PaymentStatus,
}
pub enum PaymentStatus {
Pending,
Confirmed,
Failed { reason: String },
}
impl PaymentProcessor {
/// Process payment for an invoice
pub async fn process_payment(
&self,
request: PaymentRequest,
) -> Result<PaymentResult, PaymentError> {
// Verify invoice exists and is payable
let invoice = self.get_invoice(&request.invoice_id).await?;
if invoice.status != InvoiceStatus::Finalized && invoice.status != InvoiceStatus::Sent {
return Err(PaymentError::InvoiceNotPayable);
}
// Build payment transaction
let tx = self.blockchain.build_transaction(
TransactionType::Transfer {
to: self.billing_contract,
amount: request.amount_micro_synor,
memo: format!("Invoice payment: {}", request.invoice_id),
},
request.payer,
)?;
// Submit and wait for confirmation
let tx_id = self.blockchain.submit_transaction(tx).await?;
let confirmation = self.blockchain.wait_for_confirmation(&tx_id).await?;
// Update invoice status
self.mark_invoice_paid(&request.invoice_id, &tx_id).await?;
Ok(PaymentResult {
payment_id: PaymentId::new(),
tx_id,
amount_micro_synor: request.amount_micro_synor,
timestamp: current_timestamp(),
status: PaymentStatus::Confirmed,
})
}
/// Set up auto-pay for an account
pub async fn setup_auto_pay(
&self,
account: &AccountId,
funding_source: Address,
max_amount_per_invoice: u64,
) -> Result<AutoPayConfig, PaymentError> {
// Verify funding source has sufficient balance/allowance
// Store auto-pay configuration
Ok(AutoPayConfig {
account_id: account.clone(),
funding_source,
max_amount_per_invoice,
enabled: true,
})
}
}
3.3 Credit System
// crates/synor-economics/src/billing/credit.rs
/// Account credit management
pub struct CreditManager {
storage: CreditStorage,
}
pub struct AccountCredit {
pub account_id: AccountId,
pub balance_micro_synor: u64,
pub credits: Vec<CreditEntry>,
}
pub struct CreditEntry {
pub credit_id: CreditId,
pub amount_micro_synor: u64,
pub remaining_micro_synor: u64,
pub source: CreditSource,
pub expires_at: Option<u64>,
pub created_at: u64,
}
pub enum CreditSource {
/// Prepaid balance deposit
Prepaid { tx_id: TxId },
/// Promotional credit
Promotional { campaign: String },
/// Referral bonus
Referral { referred_by: AccountId },
/// SLA credit (compensation)
SlaCredit { incident_id: String },
/// Manual adjustment
Manual { reason: String, admin: Address },
}
impl CreditManager {
/// Add credits to an account
pub async fn add_credits(
&self,
account: &AccountId,
amount: u64,
source: CreditSource,
expires_at: Option<u64>,
) -> Result<CreditEntry, CreditError> {
let entry = CreditEntry {
credit_id: CreditId::new(),
amount_micro_synor: amount,
remaining_micro_synor: amount,
source,
expires_at,
created_at: current_timestamp(),
};
self.storage.add_credit(account, &entry).await?;
Ok(entry)
}
/// Get available credit balance
pub async fn get_balance(&self, account: &AccountId) -> Result<u64, CreditError> {
let credits = self.storage.get_credits(account).await?;
let now = current_timestamp();
let balance = credits.iter()
.filter(|c| c.expires_at.map(|e| e > now).unwrap_or(true))
.map(|c| c.remaining_micro_synor)
.sum();
Ok(balance)
}
/// Apply credits to an invoice
pub async fn apply_credits(
&self,
account: &AccountId,
amount: u64,
invoice_id: &InvoiceId,
) -> Result<u64, CreditError> {
let mut credits = self.storage.get_credits(account).await?;
let now = current_timestamp();
// Sort by expiration (FIFO for expiring credits)
credits.sort_by_key(|c| c.expires_at.unwrap_or(u64::MAX));
let mut remaining = amount;
let mut applied = 0u64;
for credit in credits.iter_mut() {
if remaining == 0 {
break;
}
// Skip expired credits
if credit.expires_at.map(|e| e <= now).unwrap_or(false) {
continue;
}
let apply_amount = std::cmp::min(credit.remaining_micro_synor, remaining);
credit.remaining_micro_synor -= apply_amount;
remaining -= apply_amount;
applied += apply_amount;
// Record usage
self.storage.record_credit_usage(
&credit.credit_id,
apply_amount,
invoice_id,
).await?;
}
Ok(applied)
}
}
3.4 Prepaid Balance
// crates/synor-economics/src/billing/prepaid.rs
/// Prepaid balance management (pay-as-you-go with deposits)
pub struct PrepaidManager {
credits: CreditManager,
blockchain: SynorClient,
deposit_address: Address,
}
impl PrepaidManager {
/// Deposit SYNOR to prepaid balance
pub async fn deposit(
&self,
account: &AccountId,
amount: u64,
from: Address,
) -> Result<DepositResult, PrepaidError> {
// Build deposit transaction
let tx = self.blockchain.build_transaction(
TransactionType::Transfer {
to: self.deposit_address,
amount,
memo: format!("Prepaid deposit: {}", account),
},
from,
)?;
// Submit and confirm
let tx_id = self.blockchain.submit_transaction(tx).await?;
self.blockchain.wait_for_confirmation(&tx_id).await?;
// Add to credits
self.credits.add_credits(
account,
amount,
CreditSource::Prepaid { tx_id: tx_id.clone() },
None, // Prepaid credits don't expire
).await?;
Ok(DepositResult {
tx_id,
amount,
new_balance: self.credits.get_balance(account).await?,
})
}
/// Check if account has sufficient balance
pub async fn has_sufficient_balance(
&self,
account: &AccountId,
required: u64,
) -> Result<bool, PrepaidError> {
let balance = self.credits.get_balance(account).await?;
Ok(balance >= required)
}
/// Deduct from prepaid balance (for real-time billing)
pub async fn deduct(
&self,
account: &AccountId,
amount: u64,
reason: &str,
) -> Result<DeductResult, PrepaidError> {
let balance = self.credits.get_balance(account).await?;
if balance < amount {
return Err(PrepaidError::InsufficientBalance {
available: balance,
required: amount,
});
}
// Create a temporary invoice for the deduction
let invoice_id = InvoiceId::new_instant();
self.credits.apply_credits(account, amount, &invoice_id).await?;
Ok(DeductResult {
amount_deducted: amount,
remaining_balance: self.credits.get_balance(account).await?,
})
}
}
Tasks
- Implement Invoice data model and generator
- Build PaymentProcessor for SYNOR payments
- Implement CreditManager for account credits
- Build PrepaidManager for deposit-based billing
- Create billing smart contract
- Implement auto-pay functionality
- Add invoice email/notification system
- Build billing dashboard API
- Implement usage alerts and spending limits
Validation Commands
# Check account balance
synor billing balance
# Get current invoice
synor billing invoice --current
# View invoice history
synor billing invoices --limit 10
# Add prepaid balance
synor billing deposit 100 SYNOR
# Set up auto-pay
synor billing auto-pay enable --max 500
# Export usage report
synor billing export --format csv --period 2026-01
API Endpoints
GET /api/v1/billing/balance - Get account balance
GET /api/v1/billing/invoices - List invoices
GET /api/v1/billing/invoices/:id - Get invoice details
POST /api/v1/billing/invoices/:id/pay - Pay invoice
GET /api/v1/billing/usage - Get usage summary
POST /api/v1/billing/deposit - Deposit prepaid balance
GET /api/v1/billing/credits - List credits
POST /api/v1/billing/auto-pay - Configure auto-pay