veekthoven/laravel-cashier-paystack

一个Laravel Cashier包装器,它提供了一种表达性、流畅的界面,用于Paystack的订阅发票服务。

1.0.1 2024-08-27 18:46 UTC

This package is auto-updated.

Last update: 2024-09-27 19:47:46 UTC


README

Laravel Cashier For Paystack

Paystack的Laravel Cashier

介绍

Laravel Cashier Paystack提供了一个表达性、流畅的界面,用于Paystack的订阅计费服务。它几乎处理了您所畏惧编写的大多数订阅计费代码。

Composer

首先,将Paystack的Cashier包添加到您的依赖中

composer require veekthoven/laravel-cashier-paystack

配置

您可以使用此命令发布配置文件

php artisan vendor:publish --provider="veekthoven\Cashier\CashierServiceProvider"

一个名为cashier-paystack.php的配置文件将放置在您的config目录中,其中包含一些合理的默认设置

<?php

return [
    /**
     * Public Key From Paystack Dashboard
     *
     */
    'public_key' => env('PAYSTACK_PUBLIC_KEY'),

    /**
     * Secret Key From Paystack Dashboard
     *
     */
    'secret_key' => env('PAYSTACK_SECRET_KEY'),

    /**
     * Paystack Payment URL
     *
     */
    'path' => env('PAYSTACK_PATH'),

    /**
     * Optional email address of the merchant
     *
     */
    'merchant_email' => env('MERCHANT_EMAIL'),

    /**
     * User model for customers
     *
     */
    'model' => env('PAYSTACK_MODEL'),

];

数据库迁移

在开始使用Cashier之前,我们还需要准备数据库。您只需发布迁移文件并运行迁移命令即可

账单模型

接下来,将Billable特性添加到您的模型定义中。此特性提供了各种方法,允许您执行常见的计费任务,例如创建订阅、应用优惠券和更新信用卡信息

use veekthoven\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

货币配置

Cashier的默认货币是尼日利亚奈拉(NGN)。您可以通过在某个服务提供商的boot方法中调用Cashier::useCurrency方法来更改默认货币。useCurrency方法接受两个字符串参数:货币和货币的符号

use veekthoven\Cashier\Cashier;

Cashier::useCurrency('ngn', '');
Cashier::useCurrency('ghs', 'GH₵');

订阅

创建订阅

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

$user = User::find(1);
$plan_name = // Paystack plan name e.g default, main, yakata
$plan_code = // Paystack plan code  e.g PLN_gx2wn530m0i3w3m
$auth_token = // Paystack card auth token for customer
// Accepts an card authorization authtoken for the customer
$user->newSubscription($plan_name, $plan_code)->create($auth_token);
// The customer's most recent authorization would be used to charge subscription
$user->newSubscription($plan_name, $plan_code)->create(); 
// Initialize a new charge for a subscription
$user->newSubscription($plan_name, $plan_code)->charge(); 

传递给newSubscription方法的第一个参数应该是订阅的名称。如果您的应用程序只提供单个订阅,您可能称之为main或primary。第二个参数是用户所订阅的特定Paystack代码。此值应与Paystack中的代码标识符相对应

接受Paystack授权令牌的create方法将开始订阅,并将客户/用户ID和其他相关计费信息更新到您的数据库中

charge方法初始化一个事务,返回一个包含付款授权URL和访问码的响应

附加用户详情

$user->newSubscription('default', 'PLN_cgumntiwkkda3cw')->create($auth_token, [
    'metadata' => json_encode(['pass_through' => 'customer data']),
]);

如果您想指定附加的客户详情,可以在create方法的第二个参数中传递它们

查看订阅状态

一旦用户订阅了您的应用程序,您就可以使用各种方便的方法轻松检查他们的订阅状态。首先,subscribed方法在用户有活跃订阅的情况下返回true,即使订阅目前处于试用期内

// Paystack plan name e.g default, main, yakata
if ($user->subscribed('default')) {
    //
}

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

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

    return $next($request);
}

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

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

您可以使用subscribedToPaystack方法确定用户是否根据给定的Paystack代码订阅了特定的Paystack。在这个例子中,我们将确定用户的主要订阅是否正在订阅月度Paystack

$plan_name = // Paystack plan name e.g default, main, yakata
$plan_code = // Paystack Paystack Code  e.g PLN_gx2wn530m0i3w3m
if ($user->subscribedToPlan($plan_code, $plan_code)) {
    //
}

已取消订阅状态

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

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

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

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

取消订阅

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

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

当订阅被取消时,Cashier 会自动将您数据库中的 ends_at 列设置为。此列用于确定 subscribed 方法何时应开始返回 false。例如,如果客户在3月1日取消了原定于3月5日结束的订阅,则 subscribed 方法将继续在3月5日之前返回 true。

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

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

如果您希望立即取消订阅,请在用户的订阅上调用 cancelNow 方法。

$user->subscription('default')->cancelNow();

恢复订阅

遗憾的是,对于 Paystack,如果用户取消了订阅,则无法恢复,他们将必须创建新的订阅。启用订阅端点是用于已达到生命周期末尾的订阅。可以通过调用 resume 方法使用 enable subscription API 重新激活已完成生命周期的订阅。

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

订阅试用期

带预付费

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

$user = User::find(1);

$user->newSubscription('default', 'PLN_gx2wn530m0i3w3m')
    ->trialDays(10)
    ->create($auth_token);

此方法将在数据库中的订阅记录上设置试用期结束日期,并指示 Paystack 在此日期之后不对客户开始收费。

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

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

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

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

无预付费

如果您想提供无预付费的试用期,您可以将用户记录上的 trial_ends_at 列设置为所需的试用期结束日期。这通常在用户注册期间完成。

$user = Customer::create([
    // Populate other user properties...
    'billable_id' => $user->getKey(),
    'billable_type' => $user->getMorphClass(),
    'trial_ends_at' => now()->addDays(10),
]);

请确保在模型定义中添加 trial_ends_at 的日期突变器。

Cashier 将此类试用称为“通用试用”,因为它未附加到任何现有订阅。如果当前日期未超过 trial_ends_at 的值,则用户实例上的 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);
$plan_code = // Paystack Paystack Code  e.g PLN_gx2wn530m0i3w3m
// With Paystack card auth token for customer
$user->newSubscription('default', $plan_code)->create($auth_token);
$user->newSubscription('default', $plan_code)->create();

客户

创建客户

偶尔,您可能希望创建一个没有开始订阅的 Paystack 客户。您可以使用 createAsCustomer 方法完成此操作。

$user->createAsCustomer();

这通常在新的用户在您的应用程序上注册时完成。您可以监听 Registered 事件,并在事件监听器的 handle 方法中调用 createAsCustomer 方法。

class CreatePaystackCustomer
{
    /**
     * Handle the event.
     */
    public function handle(Registered $event): void
    {
        $event->user->createAsCustomer();
    }
}

一旦在 Paystack 中创建了客户,您可以在稍后日期开始订阅。

支付方式

检索认证的支付方式

在可计费模型实例上,卡片方法返回一个包含veekthoven\Cashier\Card实例的集合

$cards = $user->cards();

删除支付方式

要删除卡片,您应该首先使用卡片方法检索客户的认证。然后,您可以在要删除的实例上调用删除方法

foreach ($user->cards() as $card) {
    $card->delete();
}

删除客户的全部卡片支付认证

$user->deleteCards();

处理Paystack Webhooks

Paystack可以通过webhooks通知您的应用程序各种事件。要处理Paystack webhooks,定义一个指向Cashier的webhook控制器的路由。如果已配置,这已经设置为/paystack。该控制器将处理所有传入的webhook请求并将它们分派到适当的控制器方法:然而,您可以通过设置PAYSTACK_PATH环境变量或创建全新的路由来覆盖此设置

Route::post(
    'paystack/webhook',
    '\veekthoven\Cashier\Http\Controllers\WebhookController'
);

注册您的路由后,请确保在Paystack仪表板设置中配置webhook URL。

默认情况下,该控制器将自动处理取消由于过多失败收费(如您的Paystack设置定义的)而取消的订阅、收费成功、转账成功或失败、发票更新和订阅更改;然而,正如我们将很快发现的,您可以将此控制器扩展以处理任何您喜欢的webhook事件。

确保使用Cashier包含的webhook签名验证中间件保护传入的请求。

Webhooks & CSRF保护

由于Paystack webhooks需要绕过Laravel的CSRF保护,请确保在VerifyCsrfToken中间件中将URI列为异常。在Laravel 10及之前,这将在app/Http/Middleware/VerifyCsrfToken.php文件中完成

protected $except = [
    'paystack/*',
];

在Laravel 11及以上版本中,这将在bootstap/app.php文件中完成

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        //...
    )
    ->withMiddleware(function (Middleware $middleware) {
        // ...

        $middleware->validateCsrfTokens(
            except: ['paystack/*']
        );
        //..
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //...
    })->create();

定义Webhook事件处理器

如果您想处理额外的Paystack webhook事件,请扩展Webhook控制器。您的方法名称应与Cashier的预期约定相匹配,具体来说,方法应以前缀handle和您要处理的Paystack webhook事件的“驼峰命名”开始。

<?php

namespace App\Http\Controllers;

use veekthoven\Cashier\Http\Controllers\WebhookController as CashierController;

class WebhookController extends CashierController
{
    /**
     * Handle invoice payment succeeded.
     *
     * @param  array  $payload
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function handleInvoiceUpdate($payload)
    {
        // Handle The Event
    }
}

接下来,在您的routes/web.php文件中定义指向Cashier控制器的路由

Route::post(
    'paystack/webhook',
    '\App\Http\Controllers\WebhookController'
);

单次收费

简单收费

当使用Paystack时,收费方法接受您希望以应用程序使用的货币最低分母进行收费的金额。

如果您想对已订阅客户的信用卡进行一次性收费,您可以在可计费模型实例上使用收费方法。

// Paystack Accepts Charges In Kobo for Naira...
$PaystackCharge = $user->charge(10000);

收费方法将其第二个参数接受为数组,允许您将任何选项传递给底层Paystack收费创建。有关创建收费时可用选项的说明,请参阅Paystack文档

$user->charge(100, [
    'more_option' => $value,
]);

如果收费失败,收费方法将抛出异常。如果收费成功,方法将返回完整的Paystack响应

try {
    // Paystack Accepts Charges In Kobo for Naira...
    $response = $user->charge(10000);
} catch (Exception $e) {
    //
}

使用发票收费

有时您可能需要一次性收费但也要生成收费的发票,以便您可以向客户提供PDF收据。invoiceFor方法让您做到这一点。例如,让我们为“一次性费用”向客户开具2000.00₦的发票

// Paystack Accepts Charges In Kobo for Naira...
$user->invoiceFor('One Time Fee', 200000);

发票将立即对用户的信用卡进行收费。invoiceFor方法也接受一个数组作为其第三个参数。此数组包含发票项目的计费选项。该方法接受的第四个参数也是一个数组。此最终参数接受发票本身的计费选项

$user->invoiceFor('Stickers', 50000, [
    'line_items' => [ ],
    'tax' => [{"name":"VAT", "amount":2000}]
]);

如果您想指定附加的客户详情,可以在create方法的第二个参数中传递它们

退款收费如果需要退款Paystack收费,您可以使用退款方法。此方法仅接受Paystack收费ID作为其唯一参数

$paystackCharge = $user->charge(100);

$user->refund($paystackCharge->reference);

发票您可以使用invoices方法轻松检索可计费模型发票的数组

$invoices = $user->invoices();

// Include only pending invoices in the results...
$invoices = $user->invoicesOnlyPending();

// Include only paid invoices in the results...
$invoices = $user->invoicesOnlyPaid();

在列出客户的发票时,您可以使用发票的辅助方法来显示相关的发票信息。例如,您可能希望在一个表格中列出每个发票,让用户可以轻松下载其中的任何一个

<table>
    @foreach ($invoices as $invoice)
        <tr>
            <td>{{ $invoice->date()->toFormattedDateString() }}</td>
            <td>{{ $invoice->total() }}</td>
            <td><a href="/user/invoice/{{ $invoice->id }}">Download</a></td>
        </tr>
    @endforeach
</table>

在路由或控制器内部生成发票PDF,使用downloadInvoice方法来生成PDF格式的发票下载。此方法会自动生成适当的HTTP响应,将下载发送到浏览器

use Illuminate\Http\Request;

Route::get('user/invoice/{invoice}', function (Request $request, $invoiceId) {
    return $request->user()->downloadInvoice($invoiceId, [
        'vendor'  => 'Your Company',
        'product' => 'Your Product',
    ]);
});