FAQ
How does subscription billing actually work?
Section titled “How does subscription billing actually work?”This Cashier implementation schedules triggering payments from the client side, instead of relying on subscription management at Mollie. Mollie also offers a Subscription API, but it does not support all the niceties you’ve come to expect from Cashier, so this package provides its own subscription engine.
From a high level perspective, this is what the process looks like:
- A
Subscriptionis created using theMandatePaymentSubscriptionBuilder(redirecting to Mollie’s checkout to create aMandate) orPremandatedSubscriptionBuilder(using an existingMandate). - The
Subscriptionyields a scheduledOrderItemat the beginning of each billing cycle. OrderItemswhich are due are preprocessed and bundled intoOrderswhenever possible by a scheduled job (i.e. daily). This is done, so your customer will receive a single payment/invoice for multiple items later on in the chain. Preprocessing theOrderItemsmay involve applying dynamic discounts or metered billing, depending on your configuration.- The
Orderis processed by the same scheduled job into a payment:- First, (if available) the customer’s balance is processed in the
Order. - If the total due is positive, a Mollie payment is incurred.
- If the total due is 0, nothing happens.
- If the total due is negative, the amount is added to the user’s balance. If the user has no active subscriptions left, the
BalanceTurnedStaleevent will be raised.
- First, (if available) the customer’s balance is processed in the
- You can generate an
Invoice(html/pdf) for the user.
My billable model uses UUIDs. How can I get Cashier Mollie to work with this?
Section titled “My billable model uses UUIDs. How can I get Cashier Mollie to work with this?”By default Cashier Mollie uses unsignedInteger fields for the billable model relationships.
If required for your billable model, modify the cashier migrations for UUIDs:
// Replace this:$table->unsignedInteger('owner_id');
// By this:$table->uuid('owner_id');And in your Billable model add
protected $keyType = 'string';How is prorating handled?
Section titled “How is prorating handled?”Cashier Mollie applies prorating by default. With prorating, customers are billed at the start of each billing cycle.
This means that when the subscription quantity is updated or is switched to another plan:
- The billing cycle is reset.
- The customer is credited for unused time, meaning that the amount that was overpaid is added to the customer’s balance.
- A new billing cycle is started with the new subscription settings. An Order (and payment) is generated to deal with all the previous, including applying the credited balance to the Order.
This does not apply to $subscription->swapNextCycle('other-plan'), which simply waits for the next billing cycle
to update the subscription plan. A common use case for this is downgrading the plan at the end of the billing cycle.
How can I load coupons and/or plans from the database?
Section titled “How can I load coupons and/or plans from the database?”Because Cashier Mollie uses contracts a lot it’s quite easy to extend Cashier Mollie and use your own implementations. You can load coupons/plans from database, a file or even a JSON API.
For example a simple implementation of plans from the database:
First, create your own implementation of the plan repository and implement Laravel\Cashier\Plan\Contracts\PlanRepository.
Implement the methods according to your needs and make sure you’ll return a Laravel\Cashier\Plan\Contracts\Plan.
use App\Models\Plan;use Laravel\Cashier\Exceptions\PlanNotFoundException;use Laravel\Cashier\Plan\Contracts\Plan as PlanContract;use Laravel\Cashier\Plan\Contracts\PlanRepository;
class DatabasePlanRepository implements PlanRepository{ public static function find(string $name): PlanContract { /** @var Plan $plan */ $plan = Plan::where('name', $name)->firstOrFail();
// Return a \Laravel\Cashier\Plan\Plan by creating one from the database values return $plan->buildCashierPlan();
// Or if your model implements the contract: \Laravel\Cashier\Plan\Contracts\Plan return $plan; }
public static function findOrFail(string $name): PlanContract { if (($result = self::find($name)) === null) { throw new PlanNotFoundException; }
return $result; }}Example Plan model (app/Plan.php) with buildCashierPlan
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Model;use Laravel\Cashier\Cashier;use Laravel\Cashier\Plan\Plan as CashierPlan;use Money\Currency;use Money\Money;use Laravel\Cashier\Order\OrderItemPreprocessorCollection as Preprocessors;
class Plan extends Model{ use HasFactory;
/** * Builds a Cashier plan from the current model. * * @returns \Laravel\Cashier\Plan\Plan */ public function buildCashierPlan(): CashierPlan { $plan = new CashierPlan($this->name);
return $plan->setAmount($this->getAmount()) ->setInterval($this->interval) ->setDescription($this->description) ->setFirstPaymentMethod(config('cashier.first_payment.method')) ->setFirstPaymentAmount($this->getAmount()) ->setFirstPaymentDescription($this->description) ->setFirstPaymentRedirectUrl(route('plans-payment.success', ['plan' => $this->id])) ->setFirstPaymentWebhookUrl(Cashier::firstPaymentWebhookUrl()) ->setOrderItemPreprocessors(Preprocessors::fromArray(config('cashier_plans.defaults.order_item_preprocessors'))); }
private function getAmount(): Money { return new Money($this->price, new Currency($this->currency)); }}The DB schema for the above Model could look like the following:
Schema::create('plans', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('description'); $table->string('price'); $table->string('currency'); $table->string('interval'); $table->timestamps();});You can also create a PlanFactory:
class PlanFactory extends Factory{ /** * Define the model's default state. * * @return array<string, mixed> */ public function definition(): array { return [ 'name' => $this->faker->name, 'description' => $this->faker->sentence, 'price' => '10.00', 'currency' => $this->faker->currencyCode, 'interval' => $this->faker->randomElement(['month', 'year']), ]; }}Then bind your implementation to the Laravel/Illuminate container by registering the binding in a service provider:
class AppServiceProvider extends ServiceProvider{ public function register() { $this->app->bind(\Laravel\Cashier\Plan\Contracts\PlanRepository::class, DatabasePlanRepository::class); }}To test if your implementation is set up correctly, you can run the following code snippet:
use Laravel\Cashier\Plan\Contracts\PlanRepository;use App\Models\Plan;
Plan::factory()->create(["name" => "test"]);resolve(PlanRepository::class)->find("test");Cashier Mollie will now use your implementation of the PlanRepository. For coupons this is basically the same: make sure you implement the CouponRepository contract and bind the contract to your own implementation.
I have enabled iDEAL and/or Bancontact on my Mollie dashboard but the checkout only lists creditcard. Why?
Section titled “I have enabled iDEAL and/or Bancontact on my Mollie dashboard but the checkout only lists creditcard. Why?”Ensure to also enable directdebit on your Mollie dashboard if you want to allow iDEAL and/or Bancontact on your checkout.