Skip to content

Batch Billing System

Overview

The Pocketbook billing system uses batch invoicing instead of real-time per-transaction charging. Usage events are recorded immediately for tracking purposes, but customers are invoiced on their billing cycle (bi-weekly or monthly).

This approach provides:

  • Better cash flow management for customers
  • Reduced Stripe fees (fewer transactions)
  • Consolidated invoicing for easier accounting
  • Flexible billing cycles (bi-weekly, monthly, quarterly, annual)

How It Works

1. Usage Recording (Real-Time)

When a customer creates a certificate, voucher, or performs a bulk mint:

typescript
// Usage event created immediately
const usageEvent = await BillingService.recordUsage(
  userId,
  'certificate_minted',
  quantity,
  'certificate',
  metadata
);

// Event stored in database but NOT charged immediately
// totalCost is calculated based on tiered pricing
// Event status: includedInInvoiceId = null, invoicedAt = null

2. Invoice Generation (Batch Processing)

The AutomatedInvoicingService runs periodically (daily via cron) to generate invoices for customers whose billing periods have ended:

typescript
// Automated billing cycle processing
const result = await AutomatedInvoicingService.processBillingCycle();

// For each customer ready for billing:
// 1. Collect all uninvoiced usage events from billing period
// 2. Calculate total cost with tax
// 3. Create Stripe invoice with line items
// 4. Mark all events as invoiced (includedInInvoiceId set)
// 5. Reset billing period for next cycle

3. Usage Event Lifecycle

[Created] → [Pending Invoice] → [Invoiced] → [Paid]
   ↓              ↓                  ↓          ↓
  New         Waiting for      Invoice       Payment
 Event      billing cycle     generated     received
              to end

Event Fields:

  • includedInInvoiceId: Stripe invoice ID (null until invoiced)
  • invoicedAt: Timestamp when included in invoice
  • reportedToStripe: Legacy compatibility (set to true when invoiced)

Billing Cycles

Supported Cycles

CycleDurationDescription
biweekly14 daysInvoice every 2 weeks
monthly1 monthInvoice at month end (default)
quarterly3 monthsInvoice every quarter
annual12 monthsInvoice once per year

Setting Billing Cycle

For New Customers:

typescript
await CustomerBilling.create({
  userId: 'user_123',
  stripeCustomerId: 'cus_abc',
  billingEmail: 'customer@example.com',
  billingCycle: 'biweekly', // or 'monthly', 'quarterly', 'annual'
  // ... other fields
});

For Existing Customers:

javascript
db.customerbilling.updateOne(
  { userId: "user_123" },
  { $set: { billingCycle: "biweekly" } }
)

Invoice Structure

Each invoice includes:

Line Items

  1. Certificate Usage

    • Description: "Certificate Usage (X certificates) - MM/DD/YYYY to MM/DD/YYYY"
    • Amount: Based on tiered pricing
    • Metadata: certificateCount, userId, billingPeriod
  2. Voucher Usage

    • Description: "Voucher Lifecycle (X created / Y deleted)"
    • Amount: Based on net voucher creation
    • Metadata: voucherCounts, userId, billingPeriod
  3. API Usage (if applicable)

    • Description: "API Usage (X calls)"
    • Amount: Based on API call pricing
    • Metadata: apiCallCount, userId, billingPeriod
  4. Sales Tax

    • Description: "Sales Tax (Texas 6.25%)"
    • Amount: 6.25% of subtotal
    • Metadata: taxRate, taxType

Invoice Metadata

typescript
{
  userId: 'user_123',
  enterpriseId: 'ent_456',
  billingPeriodStart: '01/01/2025',
  billingPeriodEnd: '01/31/2025',
  certificateCount: '150',
  subtotal: '97.50',
  taxAmount: '6.09',
  totalCost: '103.59',
  paymentTerms: 'immediate',
  billingCycle: 'monthly'
}

Payment Terms

TermDue DateCollection Method
immediateOn receiptAuto-charge payment method
net3030 daysSend invoice email
net6060 daysSend invoice email
net9090 daysSend invoice email

Custom Contract Pricing

Customers with customContractPricing.enabled = true follow the same batch billing flow but:

  • ✅ Still have usage events recorded
  • ✅ Still receive invoices at billing cycle
  • NOT auto-charged (manual payment expected)
  • 📧 Invoice sent via email for manual processing

Automated Invoicing

Cron Schedule

The AutomatedInvoicingService should run daily at 2:00 AM UTC:

javascript
// Cron expression: "0 2 * * *"
// Runs: Every day at 2:00 AM UTC

cron.schedule('0 2 * * *', async () => {
  console.log('Starting automated billing cycle...');
  const result = await AutomatedInvoicingService.processBillingCycle();
  console.log(`Billing cycle complete: ${result.successful} invoices generated`);
});

Manual Trigger

typescript
// Trigger billing cycle manually (admin only)
const result = await AutomatedInvoicingService.processBillingCycle();

console.log({
  processed: result.processed,      // Total customers checked
  successful: result.successful,   // Invoices created
  failed: result.failed,           // Errors encountered
  totalAmount: result.totalAmount  // Total $ invoiced
});

Querying Usage Events

Get Uninvoiced Events

typescript
// All uninvoiced events across all customers
const uninvoiced = await UsageEvent.find({
  includedInInvoiceId: { $exists: false }
});

// Uninvoiced events for specific customer
const customerUninvoiced = await UsageEvent.find({
  userId: 'user_123',
  includedInInvoiceId: { $exists: false }
});

Get Events by Invoice

typescript
// All events included in a specific invoice
const invoiceEvents = await UsageEvent.find({
  includedInInvoiceId: 'in_abc123'
});

// Calculate invoice total from events
const total = invoiceEvents.reduce((sum, event) => sum + event.totalCost, 0);

Get Events by Period

typescript
// All events in a billing period (invoiced or not)
const periodEvents = await UsageEvent.find({
  userId: 'user_123',
  timestamp: {
    $gte: new Date('2025-01-01'),
    $lt: new Date('2025-02-01')
  }
});

Migration from Real-Time Billing

Before (Per-Usage Charging)

typescript
// OLD: Immediate Stripe charge
if (isCertificate && totalCost > 0 && !isCustomContractCustomer) {
  MeteredBillingService.reportUsageToStripe(usageEvent).catch(error => {
    console.error(`Failed to report usage event to Stripe:`, error);
  });
}

After (Batch Billing)

typescript
// NEW: Record event, invoice later
if (totalCost > 0) {
  console.log(`Usage recorded, invoice will be generated at billing cycle (${billingCycle})`);
}
// AutomatedInvoicingService handles charging at billing cycle end

Benefits

For Customers

Predictable billing - One invoice per cycle instead of dozens ✅ Better budgeting - Know monthly/bi-weekly costs in advance ✅ Easier accounting - Single invoice to reconcile ✅ Flexible payment - Choose immediate or net30/60/90 terms

For Platform

Lower Stripe fees - Fewer transactions = lower per-transaction fees ✅ Reduced API calls - Batch processing instead of per-event calls ✅ Better analytics - Period-based reporting ✅ Scalability - Handles high-volume customers efficiently

Monitoring

Dashboard Metrics

  • Uninvoiced usage events (total and per customer)
  • Upcoming invoices (next billing cycle)
  • Failed invoice generation attempts
  • Revenue recognized vs. invoiced

Logs to Monitor

typescript
// Successful invoice generation
[AutomatedInvoicing] Successfully generated invoice for customer user_123: $103.59

// Events marked as invoiced
[AutomatedInvoicing] Marked usage events as invoiced for customer user_123, invoice in_abc123

// Batch completion
[AutomatedInvoicing] Billing cycle completed: 42 successful, 0 failed, total $4,350.82

Troubleshooting

Issue: Customer Has Uninvoiced Events Past Billing Period

Cause: Billing cycle didn't run or failed Solution:

typescript
// Manually generate invoice for customer
const customer = await CustomerBilling.findByUserId('user_123');
const result = await AutomatedInvoicingService.generateInvoiceForCustomer(customer);

Issue: Events Invoiced Twice

Protection: Events with includedInInvoiceId are skipped during billing Check:

typescript
const duplicates = await UsageEvent.aggregate([
  { $match: { includedInInvoiceId: { $exists: true } } },
  { $group: { _id: '$includedInInvoiceId', count: { $sum: 1 } } },
  { $match: { count: { $gt: 1 } } }
]);

Issue: Billing Cycle Not Running

Check Cron:

bash
# Verify cron job is active
crontab -l | grep "billing"

# Check server logs for cron execution
tail -f /var/log/billing-cron.log

API Endpoints

Get Customer Usage (Current Period)

http
GET /api/billing/usage
Authorization: Bearer <jwt>

Response:
{
  "certificatesThisPeriod": 150,
  "vouchersCreatedThisPeriod": 25,
  "currentPeriodCost": 97.50,
  "billingCycle": "monthly",
  "billingPeriodEnd": "2025-01-31T23:59:59.999Z",
  "uninvoicedEvents": 175
}

Get Upcoming Invoice Preview

http
GET /api/billing/upcoming-invoice
Authorization: Bearer <jwt>

Response:
{
  "estimatedTotal": 103.59,
  "subtotal": 97.50,
  "tax": 6.09,
  "certificateCount": 150,
  "voucherCount": 25,
  "dueDate": "2025-02-01",
  "billingCycle": "monthly"
}

Support

For issues with batch billing:

  1. Check AutomatedInvoicingService logs
  2. Verify cron job is running daily
  3. Check for uninvoiced events past billing period
  4. Review customer billing cycle settings
  5. Contact billing team for manual invoice generation if needed

Released under the MIT License.