marshmallow/cashier-mollie

这是Mollie Laravel Cashier包的一个分支。

v1.16.2 2021-03-13 20:47 UTC

README

使用Laravel Cashier对Mollie进行订阅计费

Latest Version on Packagist Github Actions

Laravel Cashier提供了使用Mollie的计费服务进行订阅的流畅接口。

安装

首先,请确保将Mollie密钥添加到您的.env文件中。您可以从Mollie仪表板获取API密钥。

MOLLIE_KEY="test_xxxxxxxxxxx"

接下来,使用composer引入此包

composer require laravel/cashier-mollie "^1.0"

设置

一旦引入了包

  1. 运行php artisan cashier:install

  2. 将以下字段添加到您的账单模型迁移中(通常是默认的"create_user_table"迁移)

    $table->string('mollie_customer_id')->nullable();
    $table->string('mollie_mandate_id')->nullable();
    $table->decimal('tax_percentage', 6, 4)->default(0); // optional
    $table->dateTime('trial_ends_at')->nullable(); // optional
    $table->text('extra_billing_information')->nullable(); // optional
  3. 运行迁移:php artisan migrate

  4. 确保您已正确配置.env文件中的MOLLIE_KEY。您可以从Mollie仪表板获取API密钥。

    MOLLIE_KEY="test_xxxxxxxxxxxxxxxxxxxxxx"
  5. 准备配置文件

    • config/cashier_plans.php中配置至少一个订阅计划。

    • config/cashier_coupons.php中,您可以管理任何优惠券。默认情况下启用了一个示例优惠券。在生产部署之前,请考虑禁用它。

    • 基本配置在config/cashier中。在修改时请小心,在大多数情况下,您可能不需要修改。

  6. 准备账单模型(通常是默认的Laravel User模型)

    • 添加Laravel\Cashier\Billable特质。

    • 可选地,覆盖mollieCustomerFields()方法来配置在创建Mollie客户时存储哪些账单模型字段。默认情况下,mollieCustomerFields()方法使用默认的Laravel User模型字段。

    public function mollieCustomerFields() {
        return [
            'email' => $this->email,
            'name' => $this->name,
        ];
    }

    有关在Mollie客户中存储数据的更多信息,请参阅此处

    • 实现Laravel\Cashier\Order\Contracts\ProvidesInvoiceInformation接口。例如
    /**
    * Get the receiver information for the invoice.
    * Typically includes the name and some sort of (E-mail/physical) address.
    *
    * @return array An array of strings
    */
    public function getInvoiceInformation()
    {
       return [$this->name, $this->email];
    }
    
    /**
    * Get additional information to be displayed on the invoice. Typically a note provided by the customer.
    *
    * @return string|null
    */
    public function getExtraBillingInformation()
    {
       return null;
    }
  7. 安排一个周期性作业来执行Cashier::run()

    $schedule->command('cashier:run')
        ->daily() // run as often as you like (Daily, monthly, every minute, ...)
        ->withoutOverlapping(); // make sure to include this

有关使用Laravel安排作业的更多信息,请参阅此处

🎉 现在您可以开始了 :)。

定制

如果您需要更改此包使用的模型,您可以像这样覆盖它们

Cashier::setOrderModel(OrderModelClass::class);
Cashier::setOrderItemModel(OrderItemModelClass::class);

用法

创建订阅

要创建订阅,首先获取您的账单模型实例,这通常是App\User的一个实例。获取模型实例后,您可以使用newSubscription方法创建模型的订阅

$user = User::find(1);
// Make sure to configure the 'premium' plan in config/cashier_plans.php
$result = $user->newSubscription('main', 'premium')->create();

如果客户已经有有效的Mollie授权,则$result将是Subscription

如果客户还没有有效的Mollie授权,则$result将是RedirectToCheckoutResponse,将客户重定向到Mollie结账页面进行首次付款。一旦收到付款,订阅将开始。

以下是一个创建订阅的基本控制器示例

namespace App\Http\Controllers;

use Laravel\Cashier\SubscriptionBuilder\RedirectToCheckoutResponse;
use Illuminate\Support\Facades\Auth;

class CreateSubscriptionController extends Controller
{
    /**
     * @param string $plan
     * @return \Illuminate\Http\RedirectResponse
     */
    public function __invoke(string $plan)
    {
        $user = Auth::user();

        $name = ucfirst($plan) . ' membership';

        if(!$user->subscribed($name, $plan)) {

            $result = $user->newSubscription($name, $plan)->create();

            if(is_a($result, RedirectToCheckoutResponse::class)) {
                return $result; // Redirect to Mollie checkout
            }

            return back()->with('status', 'Welcome to the ' . $plan . ' plan');
        }

        return back()->with('status', 'You are already on the ' . $plan . ' plan');
    }
}

为了始终强制重定向到Mollie结账页面,请使用newSubscriptionViaMollieCheckout方法而不是newSubscription

$redirect = $user->newSubscriptionViaMollieCheckout('main', 'premium')->create(); // make sure to configure the 'premium' plan in config/cashier.php

优惠券

Cashier Mollie中的优惠券处理旨在提供最大灵活性。

优惠券可以在config/cashier_coupons.php中定义。

您可以通过扩展\Cashier\Discount\BaseCouponHandler来提供自己的优惠券处理程序。

默认情况下,提供了一个基本的FixedDiscountHandler

兑换现有订阅的优惠券

要兑换现有订阅的优惠券,请在账单特质上使用redeemCoupon()方法

$user->redeemCoupon('your-coupon-code');

此功能将验证优惠券代码并兑换它。优惠券将应用于即将到来的订单。

可选地,指定要应用的订阅

$user->redeemCoupon('your-coupon-code', 'main');

默认情况下,所有其他活动的已兑换优惠券将被撤销。您可以通过将$revokeOtherCoupons标志设置为false来防止此操作

$user->redeemCoupon('your-coupon-code', 'main', false);

检查订阅状态

一旦用户订阅了您的应用程序,您可以使用各种方便的方法轻松检查他们的订阅状态。首先,如果用户有活动的订阅,即使订阅目前处于试用期,subscribed方法也会返回true

if ($user->subscribed('main')) {
    //
}

subscribed方法也是一个很好的路由中间件的候选者,允许您根据用户的订阅状态过滤对路由和控制器的访问

public function handle($request, Closure $next)
{
    if ($request->user() && ! $request->user()->subscribed('main')) {
        // This user is not a paying customer...
        return redirect('billing');
    }

    return $next($request);
}

如果您想确定用户是否仍在试用期,可以使用onTrial方法。此方法可用于向用户显示他们仍在试用期的警告

if ($user->subscription('main')->onTrial()) {
    //
}

可以使用subscribedToPlan方法确定用户是否基于配置的计划订阅了给定的计划。在本例中,我们将确定用户的main订阅是否已积极订阅了monthly计划

if ($user->subscribedToPlan('monthly', 'main')) {
    //
}

已取消订阅状态

要确定用户是否曾经是活动的订阅者,但现在已取消订阅,可以使用cancelled方法

if ($user->subscription('main')->cancelled()) {
    //
}

您还可以确定用户是否已取消订阅,但仍在他们的“宽限期”内,直到订阅完全到期。例如,如果用户在3月5日取消了原定于3月10日到期的订阅,则用户将在3月10日之前处于“宽限期”。请注意,在此期间,subscribed方法仍然返回true

if ($user->subscription('main')->onGracePeriod()) {
    //
}

更改计划

用户订阅了您的应用程序后,他们可能会偶尔想要更改到新的订阅计划。要将用户切换到新的订阅,请将计划的标识符传递给swapswapNextCycle方法

$user = App\User::find(1);

// Swap right now
$user->subscription('main')->swap('other-plan-id');

// Swap once the current cycle has completed
$user->subscription('main')->swapNextCycle('other-plan-id');

如果用户正在试用,试用期将保持不变。此外,如果订阅存在“数量”,该数量也将保持不变。

订阅数量

有时订阅会受到“数量”的影响。例如,您的应用程序可能会对一个账户中的每个用户每月收取10欧元。要轻松增加或减少订阅数量,请使用incrementQuantitydecrementQuantity方法

$user = User::find(1);

$user->subscription('main')->incrementQuantity();

// Add five to the subscription's current quantity...
$user->subscription('main')->incrementQuantity(5);

$user->subscription('main')->decrementQuantity();

// Subtract five to the subscription's current quantity...
$user->subscription('main')->decrementQuantity(5);

或者,您可以使用updateQuantity方法设置特定数量

$user->subscription('main')->updateQuantity(10);

订阅税

要指定用户在订阅上支付的税率百分比,请在您的可计费模型上实现taxPercentage方法,并返回介于0和100之间的数值,最多两位小数。

public function taxPercentage() {
    return 20;
}

taxPercentage方法使您能够根据模型应用税率,这有助于跨越多个国家和税率的用户基础。

同步税率百分比

当更改由taxPercentage方法返回的硬编码值时,用户现有订阅的税设置将保持不变。如果您希望使用返回的taxPercentage值更新现有订阅的税值,应在用户的订阅实例上调用syncTaxPercentage方法

$user->subscription('main')->syncTaxPercentage();

订阅锚定日期

尚未实现,但您可以通过安排Cashier::run()仅在每月的特定一天执行来实现这一点。

取消订阅

要取消订阅,请在对用户的订阅上调用cancel方法

$user->subscription('main')->cancel();

$user->subscription('main')->cancelAt(now());

当取消订阅时,收银员会自动设置您数据库中的ends_at列。此列用于确定何时subscribed方法应开始返回false。例如,如果客户在3月1日取消了订阅,但订阅计划直到3月5日才结束,则subscribed方法将继续返回true直到3月5日。

您可以使用onGracePeriod方法确定用户是否已取消订阅但仍处于“宽限期”。

if ($user->subscription('main')->onGracePeriod()) {
    //
}

恢复订阅

如果用户已取消订阅并且您希望恢复它,请使用resume方法。用户必须仍在宽限期内才能恢复订阅。

$user->subscription('main')->resume();

如果用户在订阅完全到期之前取消订阅并恢复,则不会立即收费。相反,他们的订阅将被重新激活,并在原始计费周期中收费。

更新客户支付授权

即将推出。

订阅试用期

预先收取授权

如果您想在收集客户支付方式信息的同时为客户提供试用期,请在创建订阅时使用trialDays方法。

$user = User::find(1);

$user->newSubscription('main', 'monthly')
            ->trialDays(10)
            ->create();

此方法将在数据库中的订阅记录中设置试用期结束日期。

{note} 客户将被重定向到Mollie结账页面进行首次支付以注册授权。您可以在收银员配置文件中修改金额。

{note} 如果客户在试用期结束日期之前未取消订阅,则在试用期结束后将立即收费,因此您应确保通知您的用户他们的试用期结束日期。

trialUntil方法允许您提供Carbon实例以指定试用期何时结束。

use Carbon\Carbon;

$user->newSubscription('main', 'monthly')
            ->trialUntil(Carbon::now()->addDays(10))
            ->create();

您可以使用用户实例的onTrial方法或订阅实例的onTrial方法来确定用户是否在其试用期。以下两个示例是相同的

if ($user->onTrial('main')) {
    //
}

if ($user->subscription('main')->onTrial()) {
    //
}

不预先收取授权

如果您想在收集用户支付方式信息之前提供试用期,您可以将用户记录上的trial_ends_at列设置为所需的试用期结束日期。这通常在用户注册时完成。

$user = User::create([
    // Populate other user properties...
    'trial_ends_at' => now()->addDays(10),
]);

{note} 请确保在模型定义中为trial_ends_at添加日期转换器

收银员将此类试用称为“通用试用”,因为它未附加到任何现有订阅。如果当前日期未超过trial_ends_at的值,则User实例上的onTrial方法将返回true

if ($user->onTrial()) {
    // User is within their trial period...
}

如果您希望确切知道用户是否在其“通用”试用期并且尚未创建实际订阅,则可以使用onGenericTrial方法。

if ($user->onGenericTrial()) {
    // User is within their "generic" trial period...
}

一旦您准备好为用户创建实际订阅,您就可以像平常一样使用newSubscription方法。

$user = User::find(1);

$user->newSubscription('main', 'monthly')->create();

定义Webhook事件处理器

收银员会自动处理失败收费的订阅取消。

此外,监听以下事件(在Laravel\Cashier\Events命名空间中)以添加应用程序特定的行为

  • OrderPaymentPaidOrderPaymentFailed
  • FirstPaymentPaidFirstPaymentFailed

一次性收费

即将推出。

发票

监听OrderInvoiceAvailable事件(在Laravel\Cashier\Events命名空间中)。当处理了新订单时,您可以通过以下方式获取发票

$invoice = $event->order->invoice();
$invoice->view(); // get a Blade view
$invoice->pdf(); // get a pdf of the Blade view
$invoice->download(); // get a download response for the pdf

要列出发票,请使用以下方法访问用户的订单:$user->orders->invoices()。这包括所有订单的发票,即使是未处理或失败的订单。

发票列表

<ul class="list-unstyled">
    @foreach(auth()->user()->orders as $order)
    <li>

        <a href="/download-invoice/{{ $order->id }}">
            {{ $order->invoice()->id() }} -  {{ $order->invoice()->date() }}
        </a>
    </li>
    @endforeach
</ul>

并在web.php中添加此路由

Route::middleware('auth')->get('/download-invoice/{orderId}', function($orderId){

    return (request()->user()->downloadInvoice($orderId));
});

退款费用

即将推出。

客户余额

在某些情况下(例如,当切换到更便宜的计划时),客户可能会多付费用。多付的金额将添加到客户余额中。

客户余额会在每个订单中自动处理。

每种货币都保留一个单独的余额。

有一些方法可以直接与余额交互。请谨慎使用:小心使用:

$credit = $user->credit('EUR');
$user->addCredit(money(10, 'EUR')); // add €10.00
$user->hasCredit('EUR');

当处理总金额为负的订单时,该金额将记入用户余额。如果此时用户没有任何活跃的订阅,将引发一个BalanceTurnedStale事件。如果您想退款剩余余额或通知用户,请监听此事件。

客户区域

Mollie提供针对客户区域定制的结账界面。为此,它会猜测访客的区域。要覆盖默认区域,请在config/cashier.php中进行配置。这对于服务单个国家来说很方便。

如果您处理多个区域并希望覆盖Mollie的默认行为,请在可收费模型上实现getLocale()方法。一种常见的方法是在用户表中添加一个可空的locale字段并检索其值。

class User extends Model
{
    /**
     * @return string
     * @link https://docs.mollie.com/reference/v2/payments-api/create-payment#parameters
     * @example 'nl_NL'
     */
    public function getLocale() {
        return $this->locale;
    }
}

所有Cashier事件

您可以从Laravel\Cashier\Events命名空间监听以下事件

BalanceTurnedStale事件

用户有正的账户余额,但没有活跃的订阅。考虑退款。

CouponApplied事件

将优惠券应用于订单项。注意区分兑换优惠券和应用优惠券。兑换的优惠券可以应用于多个订单。例如,使用单个(兑换的)优惠券在每月订阅上应用6个月的折扣。

FirstPaymentFailed事件

用于获取授权的第一笔付款失败。

FirstPaymentPaid事件

用于获取授权的第一笔付款成功。

MandateClearedFromBillable事件

在可收费模型上清除了mollie_mandate_id。这通常发生在由于无效授权而付款失败时。

MandateUpdated事件

可收费模型上的授权已更新。这通常意味着注册了新的支付卡。

OrderCreated事件

已创建订单。

OrderInvoiceAvailable事件

订单上可用的发票。使用$event->order->invoice()访问。

OrderPaymentFailed事件

订单的付款失败。

OrderPaymentFailedDueToInvalidMandate事件

由于无效的支付授权,订单的付款失败。例如,当客户的信用卡过期时。

OrderPaymentPaid事件

订单的付款成功。

OrderProcessed事件

订单已完全处理。

SubscriptionStarted事件

开始了一个新的订阅。

SubscriptionCancelled事件

订阅已取消。

SubscriptionResumed事件

订阅已恢复。

SubscriptionPlanSwapped事件

订阅计划已交换。

SubscriptionQuantityUpdated事件

订阅数量已更新。

基于可变金额的计量计费

某些业务场景需要动态的订阅金额。

为了提供完全的灵活性,Cashier Mollie允许您定义自己的订阅订单项预处理程序集合。这些预处理程序在订单项到期时被调用,在将其处理为Mollie付款之前。

如果您使用计量计费,这是一个基于使用统计计算金额并重置任何计数器以进行下一个计费周期的便捷位置。

您可以在 cashier_plans 配置文件中定义预处理器。

好的。那么这一切到底是如何工作的呢?

此收银员实现从客户端调度触发支付,而不是依赖于Mollie的订阅管理。(是的,Mollie也提供订阅API,但它不支持Cashier的所有功能,因此此软件包提供自己的订阅引擎。)

从高层次的角度来看,这个过程看起来是这样的

  1. 使用 MandatePaymentSubscriptionBuilder(跳转到Mollie的结账页面以创建 Mandate)或 PremandatedSubscriptionBuilder(使用现有的 Mandate)创建 Subscription
  2. Subscription 在每个计费周期的开始产生一个计划的 OrderItem
  3. 只要可能,就会通过计划作业(即每天)预处理和捆绑应付款的 OrderItemsOrders 中。这样做是为了让您的客户在链的后面收到多个项目的单一付款/发票)。根据您的配置,预处理 OrderItems 可能涉及应用动态折扣或计费计量。
  4. 订单通过相同的计划作业处理成付款
    • 首先,(如果可用),客户的余额会在 Order 中进行处理。
    • 如果应付款项总额为正,则会产生Mollie付款。
    • 如果应付款项总额为0,则不会发生任何事情。
    • 如果应付款项总额为负,则金额会加到用户的余额中。如果用户没有剩余的活跃订阅,则将引发 BalanceTurnedStale 事件。
  5. 您可以为用户生成 Invoice(html/pdf)。

常见问题解答 - 常见问题

我的计费模型使用UUID,我该如何让Cashier Mollie与这个模型一起工作?

默认情况下,Cashier Mollie使用计费模型关系的 unsignedInteger 字段。如果您的计费模型需要,请修改cashier迁移以使用UUID。

// Replace this:
$table->unsignedInteger('owner_id');

// By this:
$table->uuid('owner_id');

计费是如何处理的?

Cashier Mollie默认应用计费比例。使用计费比例,客户在每个计费周期开始时被计费。

这意味着当订阅数量更新或切换到另一个计划时

  1. 计费周期将被重置
  2. 客户将获得未使用时间的信用,这意味着多付的金额将被加到客户的余额中。
  3. 以新的订阅设置开始一个新的计费周期。生成一个订单(和付款)来处理所有之前的内容,包括将信用余额应用于订单。

这不适用于 $subscription->swapNextCycle('other-plan'),它只是等待下一个计费周期更新订阅计划。一个常见的用例是在计费周期结束时降级计划。

我如何从数据库中加载优惠券和/或计划?

由于Cashier Mollie使用很多合约,因此扩展Cashier Mollie并使用自己的实现非常简单。您可以从数据库、文件或甚至是JSON API中加载优惠券/计划。

例如,从数据库中获取计划的简单实现

首先,您需要创建自己的计划存储库实现并实现 Laravel\Cashier\Plan\Contracts\PlanRepository。根据您的需求实现方法,并确保您将返回一个 Laravel\Cashier\Plan\Contracts\Plan

use App\Plan;
use Laravel\Cashier\Exceptions\PlanNotFoundException;
use Laravel\Cashier\Plan\Contracts\PlanRepository;

class DatabasePlanRepository implements PlanRepository
{
    public static function find(string $name)
    {
        $plan = Plan::where('name', $name)->first();

        if (is_null($plan)) {
            return null;
        }

        // 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)
    {
        if (($result = self::find($name)) === null) {
            throw new PlanNotFoundException;
        }

        return $result;
    }
}
示例计划模型(app/Plan.php)带有 buildCashierPlan 并返回一个 \Laravel\Cashier\Plan\Plan
<?php

namespace App;

use Laravel\Cashier\Plan\Plan as CashierPlan;
use Illuminate\Database\Eloquent\Model;

class Plan extends Model
{
    /**
     * 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(mollie_array_to_money($this->amount))
            ->setInterval($this->interval)
            ->setDescription($this->description)
            ->setFirstPaymentMethod($this->first_payment_method)
            ->setFirstPaymentAmount(mollie_array_to_money($this->first_payment_amount))
            ->setFirstPaymentDescription($this->first_payment_description)
            ->setFirstPaymentRedirectUrl($this->first_payment_redirect_url)
            ->setFirstPaymentWebhookUrl($this->first_payment_webhook_url)
            ->setOrderItemPreprocessors(Preprocessors::fromArray($this->order_item_preprocessors));
    }
}

注意:在这种情况下,您需要添加访问器以获取所有值(如金额、间隔、first_payment_method等),以确保您将使用默认值(config/cashier_plans.php > defaults)。

然后,您只需通过在服务提供商中注册绑定将您的实现绑定到Laravel/Illuminate容器中

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(\Laravel\Cashier\Plan\Contracts\PlanRepository::class, DatabasePlanRepository::class);
    }
}

收银员Mollie现在将使用您实现的PlanRepository。对于优惠券来说基本相同,只需确保您实现了CouponRepository合约并将其绑定到您自己的实现。

测试

收银员Mollie已通过Mollie的测试API进行测试。

开始时,将phpunit.xml.dist复制到phpunit.xml,并在phpunit.xml中设置以下环境变量

Mollie API测试密钥您可以在注册后立即从仪表板获取此密钥。

<env name="MOLLIE_KEY" value="YOUR_VALUE_HERE"/>

具有有效直接借记授权的客户的ID

<env name="MANDATED_CUSTOMER_DIRECTDEBIT" value="YOUR_VALUE_HERE"/>

授权的ID(前面提到的客户的ID)

<env name="MANDATED_CUSTOMER_DIRECTDEBIT_MANDATE_ID" value="YOUR_VALUE_HERE"/>

客户成功(已支付)支付的ID请使用1000欧元金额。

<env name="PAYMENT_PAID_ID" value="YOUR_VALUE_HERE"/>

客户未成功(失败)支付的ID

<env name="PAYMENT_FAILED_ID" value="YOUR_VALUE_HERE"/>

现在您可以运行

composer test

贡献

有关详细信息,请参阅CONTRIBUTING

安全性

如果您发现任何安全相关的问题,请通过电子邮件support@mollie.com联系,而不是使用问题跟踪器。

鸣谢

许可协议

MIT许可协议(MIT)。有关更多信息,请参阅许可文件