483 lines
13 KiB
Markdown
483 lines
13 KiB
Markdown
# 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
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```bash
|
|
# 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
|
|
```
|