Phase 12: Native Subscriptions
Implement a complete recurring billing engine: subscription creation, renewal processing, dunning, upgrade/downgrade with proration, and a self-service customer portal.
Files Written
Section titled “Files Written”src/Models/Subscription.phpsrc/Models/Renewal.phpsrc/Repositories/SubscriptionRepository.phpsrc/Repositories/RenewalRepository.phpsrc/Services/Subscriptions/RenewalProcessor.phpsrc/Services/Subscriptions/DunningManager.phpsrc/Services/Subscriptions/BillingScheduler.phpsrc/Services/Subscriptions/GatewayBridge.phpsrc/Services/Subscriptions/Proration.php
Subscription Model
Section titled “Subscription Model”$sub->status // active | trial | on-hold | pending-cancel | cancelled | expired$sub->billing_period // daily | weekly | monthly | yearly$sub->next_payment // datetime string$sub->recurring_total // float$sub->currency // ISO 4217$sub->is_active() // true for active|trial$sub->status_label() // human-readable stringBilling Cycle
Section titled “Billing Cycle”BillingScheduler schedules WP-Cron → wplm_process_renewals↓RenewalProcessor::process_due() for each subscription where next_payment <= now AND status = active ↓ GatewayBridge::charge($sub, $amount) ↓ success RenewalRepository::record(status='completed') BillingScheduler::advance_next_payment($sub) LicenseService::extend_expiry($license_id, $period) do_action('wplm_subscription_renewed', $sub) ↓ failure DunningManager::on_failure($sub, $renewal)GatewayBridge
Section titled “GatewayBridge”$result = apply_filters('wplm_gateway_charge', null, $subscription, $amount);If no gateway has hooked wplm_gateway_charge, the filter returns null and GatewayBridge::charge() returns WP_Error('no_gateway', ...). This is intentional — WPLM ships without a bundled payment gateway. Merchants wire their own:
add_filter('wplm_gateway_charge', function($result, $sub, $amount) { return MyGateway::charge($sub->customer_id, $amount, $sub->currency);}, 10, 3);DunningManager
Section titled “DunningManager”Configurable retry schedule (stored in settings, default: 3/7/14 days after initial failure).
On each failed renewal:
- Increment
failure_counton the subscription. - Record a
Renewalrow withstatus='failed'. - If
failure_count < max_retries: schedule next retry viaBillingScheduler. - If
failure_count >= max_retries: set subscriptionstatus = 'cancelled'and firedo_action('wplm_subscription_payment_failed_final', $sub). - On each failure, fire
do_action('wplm_subscription_payment_failed', $sub, $renewal)— default handler sends the “Payment Failed” email to the customer.
Proration
Section titled “Proration”Proration::calculate_credit(Subscription $old, Subscription $new): float
Computes the unused portion of the current billing cycle as a credit toward the new plan:
credit = (remaining_days / total_days) × old_recurring_totalcharge = new_recurring_total - creditUsed by the upgrade/downgrade endpoint: POST /wplm/v1/subscriptions/{id}/change-plan.