Everyone

Recurring Billing Engine

How Opterius Commerce automatically generates renewal invoices and manages the subscription billing cycle.

Last updated 1776211200

Overview

Commerce's recurring billing engine runs entirely on scheduled Artisan commands. There is no background daemon — the Laravel task scheduler drives everything. Add this single cron entry to your server to enable all scheduled commands:

* * * * * cd /opt/opterius && php artisan schedule:run >> /dev/null 2>&1

Scheduled Commands

Command Time Purpose
commerce:generate-renewal-invoices Daily 00:00 Creates invoices for services approaching renewal
commerce:mark-overdue-invoices Daily 00:30 Marks unpaid past-due invoices as overdue
commerce:check-overdue-services Daily 01:00 Suspends services past the grace period
commerce:check-expiring-domains Daily 02:00 Generates domain renewal invoices 30 days before expiry

How Renewal Invoice Generation Works

commerce:generate-renewal-invoices runs at 00:00 each day and:

  1. Finds all active services where next_due_date is within the renewal window (default: 7 days, configurable in Admin → Settings → Billing → Renewal Window)
  2. Checks that no unpaid or overdue invoice already exists for that service — prevents duplicate renewal invoices
  3. Creates a new invoice with line items matching the service's product pricing
  4. Sets the due date based on your configured payment terms
  5. Sends the Invoice Created email to the client

[!TIP] A renewal window of 7 days means: if a service is due on April 20, Commerce creates the renewal invoice on April 13. This gives the client a week to pay before the service expires.

Billing Cycle Advancement

When a renewal invoice is paid:

  1. The invoice status moves to paid
  2. The service's next_due_date advances by one billing cycle

Billing cycle periods:

Cycle next_due_date advance
Monthly +1 month
Quarterly +3 months
Semi-annual +6 months
Annual +1 year
Biennial +2 years

[!IMPORTANT] next_due_date is always calculated from the previous next_due_date, not from the payment date. A late payment does not reset the billing anchor — the client is still billed on the original cycle.

Domain Renewals

commerce:check-expiring-domains (runs at 02:00) works separately from hosting renewal. It queries the domain registrar API for each domain registered through Commerce and generates renewal invoices 30 days before expiry. The 30-day window is fixed and not configurable in the current release.

Verifying the Scheduler

Check that all scheduled commands are registered and their next run time:

php artisan schedule:list

To test renewal invoice generation without waiting for midnight:

php artisan commerce:generate-renewal-invoices --dry-run

The --dry-run flag logs which services would receive invoices without creating anything.

To actually run the command immediately (useful after initial setup):

php artisan commerce:generate-renewal-invoices

[!WARNING] Running commerce:generate-renewal-invoices manually without --dry-run will create real invoices and send emails. Only do this if you intend to generate invoices immediately.

Idempotency

The commands are safe to run multiple times. The duplicate-check ensures that running commerce:generate-renewal-invoices twice in one day does not create two invoices for the same service. The notified_at guard in commerce:mark-overdue-invoices prevents duplicate overdue emails.

Monitoring

Log entries are written to storage/logs/laravel.log for every invoice created and every service suspended. Review this file if you suspect invoices are not being generated:

grep "renewal-invoices" storage/logs/laravel.log | tail -20

Related Topics