UpgradeFromFree transitions a free subscription to a paid plan on a real payment gateway. It cancels the old free subscription locally, creates a new paid subscription on the target gateway, hydrates plan items, and fires SubscriptionCreated — all inside a single database transaction.
Calling styles
Resolve the action from the container:use Pixelworxio\Subscribd\Actions\UpgradeFromFree;
use Pixelworxio\Subscribd\Models\Plan;
$freeSubscription = $user->subscription('default');
$paidPlan = Plan::where('key', 'pro')->firstOrFail();
$newSubscription = app(UpgradeFromFree::class)->execute(
$user,
$freeSubscription,
$paidPlan,
['gateway' => 'stripe'],
);
The action is also exposed as a REST endpoint. See POST /subscriptions//upgrade-from-free.curl -X POST https://your-app.com/subscribd/api/v1/subscriptions/1/upgrade-from-free \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{"plan_id": 3, "gateway": "stripe"}'
Parameters
| Parameter | Type | Required | Description |
|---|
$billable | Model&Billable | Yes | The billable that owns the free subscription |
$freeSubscription | Subscription | Yes | The existing subscription in free status |
$plan | Plan | Yes | The paid plan to upgrade to |
$options | array<string, mixed> | No | Additional options (see below) |
Options array
| Key | Type | Default | Description |
|---|
gateway | string | config('subscribd.default') | Payment gateway driver name. Cannot be null (the null driver). |
name | string | Inherited from $freeSubscription->name | Subscription slot name for the new subscription |
quantity | int | Resolved via QuantityResolverRegistry, then 1 | Number of seats or units |
plan_items | array<string, int> | — | Map of plan item key to quantity override |
How it works
- Validates that the subscription is in
free status. Throws SubscriptionException if not.
- Validates that the target gateway is not the null driver.
- Resolves the gateway driver and creates a customer record if one does not exist.
- Creates the new paid subscription on the gateway.
- Hydrates plan items for the new subscription from the plan’s active plan items.
- Cancels the old free subscription locally by transitioning it to
canceled and setting canceled_at. No gateway API call is made — the null gateway has no remote state.
- Fires
SubscriptionCreated for the new paid subscription.
Return value
Returns the new Subscription model in active status with the plan relationship loaded.
$newSubscription->status; // 'active'
$newSubscription->gateway; // 'stripe'
$newSubscription->name; // 'default' (inherited from the free subscription)
Errors
| Exception | When |
|---|
SubscriptionException | The subscription is not in free status (invalid state transition). |
SubscriptionException | The resolved gateway is the null driver ('null'). |
GatewayException | The gateway fails to create the subscription. |
Events fired
SubscriptionCreated is dispatched for the new paid subscription after the transaction commits.
SubscriptionCanceled is not fired for the old free subscription. The cancellation is an internal housekeeping step, not a user-initiated cancellation. This avoids triggering offboarding workflows for what is actually an upgrade.