Skip to main content

Refund Flow

A refund can arrive through three conceptual paths. Each path ends by inserting a new row in payments with status = 'refunded'; the original approved row is never modified. See Payment Ledger Model.

The critical thing to know: a refund has three possible outcomes, and only one of them automatically cancels the subscription.


Three paths, two ingress points

Mechanically, refunds enter the system through two ingress points:

  • The IPN pipeline, for gateway-originated refund events.
  • The dashboard refund action, for support- or admin-initiated refunds.

Conceptually, the three paths users care about are:

PathWho triggers itEntry point
IPN-drivenThe gateway notifies Farfalla that a charge was reversedIPN handler maps the gateway status to refunded and calls the payment store service
Dashboard-initiatedA support or admin user initiates the refund from the control panelThe dashboard refund handler calls the gateway API via the partial-refund service, then stores the result
Subscription cancellationA subscription cancel flow includes a refund of the last chargeThe checkout service routes through the same code path as the dashboard refund

All three paths eventually call the same payment store service, so the ledger rule is enforced regardless of origin.


Three outcomes

A refund attempt always returns one of three states:

Success: the gateway confirmed the refund. The payment row is inserted. For MercadoPago and Stripe, the gateway response is synchronous: success is determined inline. There is no pending state.

Whether access is revoked depends on the refund scope:

  • Full refund (IPN-driven or full dashboard refund): the associated UserPlan is cancelled and the user loses access immediately.
  • Partial refund (dashboard, less than the full approved amount): the refund row is inserted but the UserPlan stays active and the user keeps access. See Partial refunds.

Pending (Yuno only): the gateway accepted the request but has not yet confirmed it. No ledger row is inserted yet. Instead, a row is written to the pending_payment_refunds table. The UserPlan is not cancelled. VerifyPendingRefunds polls the gateway every 5 minutes; after 12 attempts (~1 hour) the pending row is marked failed and does not get promoted to a payments ledger row.

Failure: the gateway rejected the refund. The error is logged. The UserPlan is not cancelled. No payment row is inserted. Manual intervention may be needed.

The important consequence: if you see a UserPlan that was not cancelled after a refund was attempted, the refund is either pending or failed, not completed.


Refund time limit

Dashboard refunds are accepted for 30 days after the original payment (measured from payment_date). The window applies regardless of gateway; this limit may change over time, so confirm against the partial-refund service before relying on it. After the window expires, dashboard refunds are blocked and any attempt must go through the gateway portal directly.


Which gateways support refunds

Stripe, MercadoPago, and Yuno are the only gateways with a refund path. PayU and Manual orders have no refund path; the check is a hard gate that returns false before reaching any gateway code.


Partial refunds

The dashboard supports partial refunds: refunding less than the full approved amount. The available balance is computed from the ledger; see Balance calculation for the formula. The refund-flow contribution is the shipping fork: for shipping payments (user_plan_id = null), the scope uses plan_type = 'shipping' in place of user_plan_id.

For physical orders, the product payment and the carrier cost payment can be partially refunded independently. See Shipping Payment.


X

Graph View