Yuno Subscriptions and Trial
Summary
Yuno has its own subscriptions API and Farfalla uses it directly. Once the subscription is created on the Yuno side, recurring charges are triggered by Yuno autonomously — Farfalla runs no jobs to initiate them, only reacts to webhooks. In this regard Yuno resembles Stripe and MercadoPago, which also manage recurring charges from their own infrastructure.
There are two subscription signup flows:
- Without trial: the first cycle is charged upfront as a one-off payment, and the subscription is created only afterwards, chained to the successful payment.
- With trial: there is no initial charge, only a card enrollment. The subscription is created pointing forward in time.
The detail on the SDK side (what is mounted in each case, how the token is obtained, how 3DS is handled) is covered in Yuno Frontend / SDK. This doc focuses on what happens on the backend side: how the subscription is created and maintained against the Yuno API.
Customer and Customer Session
The customer is a required field in the Yuno API: any checkout, enrollment, or subscription creation requires a valid customer identifier. Farfalla maintains the association between the user and the customer in gateways_users_references.
The retrieval routine covers cases where state may diverge between Farfalla and Yuno. First it looks for the local reference. If it exists, it queries Yuno to confirm the customer is still active: if it was deleted on the Yuno side (something that happens in sandbox or after manual cleanups), it clears the local reference and creates a new one. If there is no local reference, it tries to create the customer in Yuno using the internal ID as the merchant identifier. And if that creation fails because Yuno responds that the identifier already exists, it retrieves it by searching with that same identifier. Only then does it persist the Yuno ID in gateways_users_references.
On top of that customer, a customer session is mounted, which is a temporary SDK-use token. The customer session is what is passed to the SDK on the frontend so it can display the card or enrollment form. It must not be confused with the vaulted token: the customer session authorizes the SDK to operate on behalf of the customer; the vaulted token is the stored card itself.
Plans and the Absence of plans_gateways_meta
For Stripe, Farfalla maintains the plans_gateways_meta table, where it stores the plan ID that exists on the gateway side. Stripe requires the plan to exist as a reusable entity and for subscriptions to reference it.
Yuno does not have that concept. The subscription creation API receives the amount, currency, and frequency (monthly with a number of months per cycle) directly. There is no plan to create in advance and nothing to persist in plans_gateways_meta. That is why that table has no rows for Yuno and there is no associated plan strategy in GatewaysMetaStrategy.
The conversion of the billing interval to the format expected by Yuno is handled by the billing interval enum, translating the cycle (monthly, quarterly, annual) to the number of months that goes in the frequency field.
Non-Trial Recurring Flow
This flow applies when the plan has no trial period or when the user has already consumed it previously. The check for "trial already consumed" looks at whether the user has previous non-pending UserPlans with a trial.
The reason the first charge goes through a one-off payment (with Payment Lite on the frontend side) rather than directly through the subscription creation API is that the Yuno subscription API does not support 3DS. The workaround is explained in detail in Yuno OTT (backend side) and Yuno Frontend / SDK (SDK side).
Chained to the successful payment, Farfalla creates the subscription in Yuno using the resulting vaulted token. The start date is set in the future: as many months ahead as the billing cycle duration. This way Yuno does not charge the second cycle until that date arrives.
After creating the subscription, Farfalla polls with exponential backoff (up to 5 attempts: 0.5 s, 1 s, 2 s, 4 s) on the subscription status. Yuno transitions through intermediate states when activating it (for example CREATED) before reaching ACTIVE, so the polling confirms the final state synchronously before continuing. Once confirmed, it persists the first payment locally and approves the order and the UserPlan.
Trial Recurring Flow
When the plan has a trial and the user has not consumed it before, there is no initial charge. The backend prepares a customer session and starts an enrollment within that session, which is what the frontend uses to show the SDK in Enrollment Lite mode. Yuno responds with an enrollment vaulted token — the card is stored without anything being debited.
When the frontend confirms the enrollment, the vaulted token reaches the backend and Farfalla creates the subscription using that token. The start date is set in the future: today plus the trial days. Yuno will trigger the first real charge exactly when that date arrives.
There is a relevant detail for status mapping in this flow. Yuno responds with the subscription in CREATED state right after creation, because no payment has been processed yet; that state is considered approved only when the plan has a trial. For non-trial subscriptions, CREATED is not enough to approve the order — what approves it there is the successful payment of the first cycle. The logic of which states count as approved depending on whether there is a trial or not lives in YunoPaymentStatus.
Subsequent Recurring Charges
From the second cycle onward (without trial) or from the first real charge (with trial), Yuno takes over. There is no Farfalla job equivalent to CheckMercadoPagoRecurringPayments. Yuno charges automatically each cycle and, if the charge is declined, retries on its own because the subscription is created with retry_on_decline enabled. The retry policy (how many times, at what cadence) is managed by Yuno according to its internal configuration; Farfalla only learns of the final result.
Subscription webhooks are the only path through which Farfalla updates the order and the UserPlan on subsequent charges. The detail of each event, what action it triggers, and how the order, tenant, and status are resolved is in Yuno IPN / Webhooks.
Cancellation, Pause, and Resume
What a user or admin calls "cancellation" does not always cancel the subscription in Yuno. The logic of which operation is sent to the gateway lives in OrderStatusService::handleGatewaySubscriptionCancellation and applies to all gateways with subscriptions, not just Yuno. The two classifications it decides are who triggered the cancellation (SubscriptionCancellationBy) and why (SubscriptionCancellationReason):
- Human (user or admin): the operation sent to Yuno is a pause, not cancellation. The reasoning is that it is a reversible decision — the user may change their mind, the admin may reactivate — and keeping the subscription paused on the gateway side allows resuming it without having to create a new one. On the Farfalla side the UserPlan is marked as cancelled with
valid_to = now, but on the Yuno side the subscription is paused. - System (job, expiration, cleanup): definitive cancellation in Yuno. No reactivation is expected.
- Subscription without successful payments: if the UserPlan never had a real charge (trial cancelled before the first charge, or all payments rejected), Farfalla cancels definitively even if the origin is human. Some gateways do not allow pausing subscriptions that were never activated.
- Originated by the gateway (IPN, webhook, refund): Farfalla sends nothing to the gateway; the gateway already acted.
For Yuno specifically, the three operations exposed in YunoRecurringPaymentService (cancelSubscription, pauseSubscription, resumeSubscription) call the corresponding Yuno API routes directly. The actual state change is confirmed by the webhook that Yuno dispatches afterwards: subscription.cancel, subscription.pause, or subscription.resume. The cancelled state is considered valid whether Yuno responds with CANCELLED or CANCELED (the two spellings it uses internally, depending on the endpoint).
The practical consequence of this is that the most common flow — a user cancels their subscription from My Account — ends with the subscription paused in Yuno, not cancelled. If the user re-subscribes to the same plan, Farfalla today creates a new subscription instead of resuming the paused one: the method YunoRecurringPaymentService::resumeSubscription and the corresponding connector endpoint exist, but no checkout flow invokes it. The pause on the Yuno side remains orphaned until it is manually cancelled or expires due to inactivity.
Payment Method Change
From My Account the user can change the card associated with an active subscription without cancelling or recreating it. The flow does not use Payment Lite and does not make any validation charge: it reuses exactly the same Enrollment Lite used for trials.
The backend prepares a customer session on the customer that owns the order, starts an enrollment within that session, and delivers it to the frontend. The user enters the new card inside the SDK. When enrollment is confirmed, the SDK passes the new vaulted token to the backend, which updates the existing subscription in Yuno with that token via a PATCH. The subscription is not cancelled or recreated: only the associated payment method changes.
Subscription Status Mapping
This is the translation between the states that travel in Yuno webhooks and what Farfalla reflects in the order and the UserPlan:
| Stage | Yuno state | Order / UserPlan |
|---|---|---|
| Checkout initiated | — | pending |
| Trial created (enrollment complete) | CREATED | approved (CREATED accepted only in trial) |
| First real charge approved | ACTIVE | approved |
| Recurring charge approved | ACTIVE | no change |
| Cancelled by user or API | CANCELLED | cancelled, valid_to = now |
| Cancelled by IPN after failures | CANCELLED | cancelled |
| Reactivated after failure | ACTIVE | re-approved |
| Paused | PAUSED | no change |
Differences With Other Gateways
The particularity of Yuno compared to the other Farfalla gateways with subscriptions is that the API has no reusable plan: each subscription creation call carries its own amount and frequency, and Farfalla does not maintain plans_gateways_meta for Yuno. In Stripe the plan is persisted and reused.
For details on plan behavior in each gateway, see Payments Overview.
Edge Cases
Trial cancelled before the first charge. The user cancels during the trial. The Yuno API marks the subscription as cancelled, the corresponding webhook arrives, and the UserPlan is cancelled. No payments are recorded: the first charge never occurred.
Post-trial charge declined. Yuno retries automatically. If it ultimately declines, Yuno cancels or pauses the subscription according to its internal configuration, and Farfalla reacts to the resulting webhook. If the subscription becomes active again afterwards, Farfalla re-approves the UserPlan it had cancelled.
Plan change during trial. There is no native migration: the current subscription is cancelled and a new one is created. Farfalla has no special logic for "migrating" subscriptions on the Yuno side.
Coupon with a single discount period. If the coupon applies to a single period, the first charge or the initial subscription uses the discounted amount, but the subscription that remains active on the Yuno side is created with the original price restored. The subscription creation routine looks at the coupon associated with the UserPlan and, if it has a discount period, restores the original price before sending the amount to Yuno.
Price update on an active subscription. When a discount period expires, Farfalla updates the amount of the already-active subscription in Yuno with a PATCH. Yuno uses that new amount from the next cycle onward.
Code References
| File | Role |
|---|---|
app/Domains/Commerce/Gateways/Yuno/YunoRecurringPaymentService.php | Create, cancel, pause, and update subscriptions; enrollment session assembly |
app/Domains/Commerce/Gateways/Yuno/YunoCustomerService.php | Defensive customer retrieval and creation in Yuno |
app/Domains/Commerce/Gateways/Yuno/YunoBaseService.php | Connector and shared helpers |
app/Domains/Commerce/Gateways/Yuno/YunoPaymentStatus.php | Status enum and approval logic depending on trial presence |
app/Domains/Commerce/Gateways/Yuno/Events/Subscription.php | Subscription webhook processor |
app/Domains/Commerce/Support/Yuno/YunoConnector.php | HTTP client against the Yuno API |
app/Domains/Commerce/Support/Yuno/YunoSubscription.php | Subscription entity |
app/Domains/Commerce/Support/Yuno/YunoCustomerSession.php | Customer session entity |
app/Domains/Commerce/Support/Yuno/YunoPaymentMethodsToEnroll.php | Enrollment entity |
app/Http/Livewire/CheckoutV2/YunoPaymentForm.php | Livewire component orchestrating checkout, initial payment, and subscription creation |
app/Http/Livewire/MyAccount/ChangeYunoPaymentMethod.php | Livewire component for payment method change |
resources/js/yuno-libraries.js | SDK handlers for subscriptions, trial, and method change |
resources/js/alpine/alpine-components/yuno-checkout-v2-component.js | Alpine store mediating between Livewire and the SDK |