Laravel Cashier Paddle
介绍
Laravel Cashier Paddle 提供了一个对 Paddle 订阅计费服务的表达性、流畅的接口。它几乎处理了所有你不想编写的订阅计费样板代码。除了基本的订阅管理,Cashier 还可以处理:优惠券、交换订阅、订阅“数量”、取消宽限期等。
在使用 Cashier 时,我们建议你也参考 Paddle 的用户指南和API 文档。
升级 Cashier
在升级到新版本的 Cashier 时,务必仔细查看升级指南。
安装
首先,通过 Composer 安装 Paddle 的 Cashier 包:
composer require laravel/cashier-paddle
为确保 Cashier 正确处理所有 Paddle 事件,请记得设置 Cashier 的 webhook 处理。
数据库迁移
Cashier 服务提供者注册了自己的数据库迁移目录,因此在安装包后记得迁移数据库。Cashier 迁移将创建一个新的 customers
表。此外,还将创建一个新的 subscriptions
表来存储所有客户的订阅。最后,还将创建一个新的 receipts
表来存储所有的收据信息:
php artisan migrate
如果需要覆盖 Cashier 包附带的迁移,可以使用 vendor:publish
Artisan 命令发布它们:
php artisan vendor:publish --tag="cashier-migrations"
如果希望完全阻止 Cashier 的迁移运行,可以使用 Cashier 提供的 ignoreMigrations
。通常,此方法应在 AppServiceProvider
的 register
方法中调用:
use Laravel\Paddle\Cashier;
Cashier::ignoreMigrations();
配置
计费模型
在使用 Cashier 之前,必须将 Billable
trait 添加到用户模型定义中。此 trait 提供了各种方法,允许你执行常见的计费任务,例如创建订阅、应用优惠券和更新支付方式信息:
use Laravel\Paddle\Billable;
class User extends Authenticatable
{
use Billable;
}
如果有不是用户的计费实体,也可以将 trait 添加到这些类中:
use Laravel\Paddle\Billable;
class Team extends Model
{
use Billable;
}
API 密钥
接下来,应在 .env
文件中配置 Paddle 密钥。可以从 Paddle 控制面板中检索 Paddle API 密钥:
PADDLE_VENDOR_ID=your-paddle-vendor-id
PADDLE_VENDOR_AUTH_CODE=your-paddle-vendor-auth-code
PADDLE_PUBLIC_KEY="your-paddle-public-key"
Paddle JS
Paddle 依赖于其自己的 JavaScript 库来启动 Paddle 结账小部件。可以通过在应用程序布局的关闭 </head>
标签之前放置 @paddleJS
指令来加载 JavaScript 库:
<head>
...
@paddleJS
</head>
货币配置
默认的 Cashier 货币是美元 (USD)。可以通过设置 CASHIER_CURRENCY
环境变量来更改默认货币:
CASHIER_CURRENCY=EUR
除了配置 Cashier 的货币外,还可以指定一个用于在发票上显示货币值时使用的区域设置。内部,Cashier 使用 PHP 的 NumberFormatter
类 来设置货币区域设置:
CASHIER_CURRENCY_LOCALE=nl_BE
为了使用 en
以外的区域设置,请确保在服务器上安装并配置了 ext-intl
PHP 扩展。
核心概念
支付链接
Paddle 缺乏一个广泛的 CRUD API 来执行状态更改。因此,大多数与 Paddle 的交互是通过其结账小部件完成的。在我们可以显示结账小部件之前,我们将使用 Cashier 生成一个“支付链接”:
$user = User::find(1);
$payLink = $user->newSubscription('default', $premium = 34567)
->returnTo(route('home'))
->create();
return view('billing', ['payLink' => $payLink]);
Cashier 包含一个 paddle-button
Blade 组件。我们可以将支付链接 URL 作为“属性”传递给此组件。当单击此按钮时,将显示 Paddle 的结账小部件:
<x-paddle-button :url="$payLink" class="px-8 py-4">
Subscribe
</x-paddle-button>
默认情况下,这将显示一个带有标准 Paddle 样式的按钮。可以通过向组件添加 data-theme="none"
属性来移除所有 Paddle 样式:
<x-paddle-button :url="$payLink" class="px-8 py-4" data-theme="none">
Subscribe
</x-paddle-button>
Paddle 结账小部件是异步的。一旦用户在小部件中创建或更新订阅,Paddle 将向我们的应用程序发送 webhooks,以便我们可以正确更新我们自己数据库中的订阅状态。因此,重要的是要正确设置 webhooks以适应来自 Paddle 的状态更改。
在订阅状态更改后,接收相应 webhook 的延迟通常是最小的,但你应该在应用程序中考虑到这一点,因为用户的订阅可能在完成结账后不会立即可用。
有关更多信息,可以查看 Paddle API 文档关于支付链接生成。
内嵌结账
如果不想使用“覆盖”样式的结账小部件,Paddle 还提供了一个选项,可以内嵌显示小部件。虽然这种方法不允许你调整结账的任何 HTML 字段,但它允许你在应用程序中嵌入小部件。
为了让你更轻松地开始使用内嵌结账,Cashier 包含一个 paddle-checkout
Blade 组件。要开始使用,应该生成一个支付链接并将支付链接传递给组件的 override
属性:
<x-paddle-checkout :override="$payLink" class="w-full" />
要调整内嵌结账组件的高度,可以将 height
属性传递给 Blade 组件:
<x-paddle-checkout :override="$payLink" class="w-full" height="500" />
无支付链接的内嵌结账
或者,可以使用自定义选项而不是支付链接来自定义小部件:
$options = [
'product' => $productId,
'title' => 'Product Title',
];
<x-paddle-checkout :options="$options" class="w-full" />
请查阅 Paddle 的内嵌结账指南以及他们的参数参考以获取有关可用选项的更多详细信息。
如果希望在指定自定义选项时也使用 passthrough
选项,则应提供一个键/值数组,因为 Cashier 将自动处理将数组转换为 JSON 字符串。此外,customer_id
passthrough 选项是为 Cashier 内部使用保留的。
用户识别
与 Stripe 相比,Paddle 用户在整个 Paddle 中是唯一的,而不是每个 Paddle 账户唯一的。因此,Paddle 的 API 目前不提供更新用户详细信息(如电子邮件地址)的方法。在生成支付链接时,Paddle 使用 customer_email
参数来识别用户。在创建订阅时,Paddle 将尝试将用户提供的电子邮件与现有的 Paddle 用户匹配。
鉴于这种行为,在使用 Cashier 和 Paddle 时需要注意一些重要事项。首先,你应该意识到,即使 Cashier 中的订阅与同一应用程序用户相关联,它们也可能与 Paddle 内部系统中的不同用户相关联。其次,每个订阅都有其自己的连接支付方式信息,并且在 Paddle 的内部系统中也可能有不同的电子邮件地址(取决于创建订阅时分配给用户的电子邮件)。
因此,在显示订阅时,应该始终告知用户与订阅相关的电子邮件地址或支付方式信息。可以使用 Subscription
模型上的以下方法检索此信息:
$subscription = $user->subscription('default');
$customerEmailAddress = $subscription->paddleEmail();
$paymentMethod = $subscription->paymentMethod();
$cardBrand = $subscription->cardBrand();
$cardLastFour = $subscription->cardLastFour();
$cardExpirationDate = $subscription->cardExpirationDate();
目前无法通过 Paddle API 修改用户的电子邮件地址。当用户希望在 Paddle 中更新其电子邮件地址时,唯一的方法是联系 Paddle 客户支持。在与 Paddle 通信时,他们需要提供订阅的 paddleEmail
值以协助 Paddle 更新正确的用户。
价格
Paddle 允许你根据货币自定义价格,实质上允许你为不同国家配置不同的价格。Cashier Paddle 允许你使用 productPrices
方法检索给定产品的所有价格:
use Laravel\Paddle\Cashier;
// 检索两个产品的价格...
$prices = Cashier::productPrices([123, 456]);
货币将根据请求的 IP 地址确定;但是,你可以选择性地提供一个特定的国家来检索价格:
use Laravel\Paddle\Cashier;
// 检索两个产品的价格...
$prices = Cashier::productPrices([123, 456], ['customer_country' => 'BE']);
在检索价格后,可以随意显示它们:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product_title }} - {{ $price->price()->gross() }}</li>
@endforeach
</ul>
你还可以显示净价(不含税)并单独显示税额:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product_title }} - {{ $price->price()->net() }} (+ {{ $price->price()->tax() }} 税)</li>
@endforeach
</ul>
如果你检索了订阅计划的价格,可以分别显示其初始和经常性价格:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product_title }} - 初始: {{ $price->initialPrice()->gross() }} - 经常性: {{ $price->recurringPrice()->gross() }}</li>
@endforeach
</ul>
有关更多信息,请查看 Paddle 的 API 文档关于价格。
客户
如果用户已经是客户,并且希望显示适用于该客户的价格,可以通过直接从客户实例中检索价格来实现:
use App\User;
// 检索两个产品的价格...
$prices = User::find(1)->productPrices([123, 456]);
在内部,Cashier 将使用用户的 paddleCountry
方法来检索其货币的价格。因此,例如,居住在美国的用户将看到美元的价格,而比利时的用户将看到欧元的价格。如果找不到匹配的货币,将使用产品的默认货币。可以在 Paddle 控制面板中自定义产品或订阅计划的所有价格。
优惠券
你还可以选择在优惠券折扣后显示价格。在调用 productPrices
方法时,可以将优惠券作为逗号分隔的字符串传递:
use Laravel\Paddle\Cashier;
$prices = Cashier::productPrices([123, 456], ['coupons' => 'SUMMERSALE,20PERCENTOFF']);
然后,使用 price
方法显示计算后的价格:
<ul>
@foreach ($prices as $price)
<li>{{ $price->product_title }} - {{ $price->price()->gross() }}</li>
@endforeach
</ul>
可以使用 listPrice
方法显示原始列出的价格(不含优惠券折扣):
<ul>
@foreach ($prices as $price)
<li>{{ $price->product_title }} - {{ $price->listPrice()->gross() }}</li>
@endforeach
</ul>
使用价格 API 时,Paddle 仅允许将优惠券应用于一次性购买产品,而不适用于订阅计划。
客户
客户默认值
Cashier 允许你在创建支付链接时为客户设置一些有用的默认值。设置这些默认值可以让你预先填写客户的电子邮件地址、国家和邮政编码,以便他们可以立即进入结账小部件的支付部分。可以通过覆盖计费用户上的以下方法来设置这些默认值:
/**
* 获取要与 Paddle 关联的客户电子邮件地址。
*
* @return string|null
*/
public function paddleEmail()
{
return $this->email;
}
/**
* 获取要与 Paddle 关联的客户国家。
*
* 这需要是一个 2 位代码。请参阅下面的链接以获取支持的国家。
*
* @return string|null
* @link https://developer.paddle.com/reference/platform-parameters/supported-countries
*/
public function paddleCountry()
{
//
}
/**
* 获取要与 Paddle 关联的客户邮政编码。
*
* 请参阅下面的链接以获取需要此信息的国家。
*
* @return string|null
* @link https://developer.paddle.com/reference/platform-parameters/supported-countries#countries-requiring-postcode
*/
public function paddlePostcode()
{
//
}
这些默认值将用于 Cashier 中生成的每个支付链接的操作。
订阅
创建订阅
要创建订阅,首先检索计费模型的实例,通常是 App\User
的实例。一旦检索到模型实例,可以使用 newSubscription
方法创建模型的订阅支付链接:
$user = User::find(1);
$payLink = $user->newSubscription('default', $premium = 12345)
->returnTo(route('home'))
->create();
return view('billing', ['payLink' => $payLink]);
传递给 newSubscription
方法的第一个参数应该是订阅的名称。如果你的应用程序只提供一个订阅,可以将其称为 default
或 primary
。第二个参数是用户订阅的特定计划。此值应对应于 Paddle 中计划的标识符。returnTo
方法接受一个 URL,用户在成功完成结账后将被重定向到该 URL。
create
方法将创建一个支付链接,可以用来生成支付按钮。支付按钮可以使用 Cashier Paddle 附带的 paddle-button
Blade 组件生成:
<x-paddle-button :url="$payLink" class="px-8 py-4">
Subscribe
</x-paddle-button>
在用户完成结账后,Paddle 将发送一个 subscription_created
webhook。Cashier 将接收此 webhook 并为客户设置订阅。为了确保所有 webhooks 都能被应用程序正确接收和处理,请确保已正确设置 webhook 处理。
额外细节
如果希望指定额外的客户或订阅详细信息,可以通过将它们作为键/值数组传递给 create
方法来实现:
$payLink = $user->newSubscription('default', $monthly = 12345)
->returnTo(route('home'))
->create([
'vat_number' => $vatNumber,
]);
要了解 Paddle 支持的其他字段,请查看 Paddle 关于生成支付链接的文档。
优惠券
如果希望在创建订阅时应用优惠券,可以使用 withCoupon
方法:
$payLink = $user->newSubscription('default', $monthly = 12345)
->returnTo(route('home'))
->withCoupon('code')
->create();
元数据
还可以使用 withMetadata
方法传递元数据数组:
$payLink = $user->newSubscription('default', $monthly = 12345)
->returnTo(route('home'))
->withMetadata(['key' => 'value'])
->create();
提供元数据时,请避免使用 subscription_name
作为元数据键。此键为 Cashier 内部使用保留。
检查订阅状态
一旦用户订阅了你的应用程序,可以使用多种方便的方法检查他们的订阅状态。首先,subscribed
方法返回 true
,如果用户有一个活动的订阅,即使订阅当前处于试用期:
if ($user->subscribed('default')) {
//
}
subscribed
方法也非常适合用作路由中间件,允许你根据用户的订阅状态过滤对路由和控制器的访问:
public function handle($request, Closure $next)
{
if ($request->user() && ! $request->user()->subscribed('default')) {
// 该用户不是付费客户...
return redirect('billing');
}
return $next($request);
}
如果希望确定用户是否仍在试用期内,可以使用 onTrial
方法。此方法可用于向用户显示警告,告知他们仍在试用期内:
if ($user->subscription('default')->onTrial()) {
//
}
subscribedToPlan
方法可用于根据给定的 Paddle 计划 ID 确定用户是否订阅了给定计划。在此示例中,我们将确定用户的 default
订阅是否积极订阅了月度计划:
if ($user->subscribedToPlan($monthly = 12345, 'default')) {
//
}
通过将数组传递给 subscribedToPlan
方法,可以确定用户的 default
订阅是否积极订阅了月度或年度计划:
if ($user->subscribedToPlan([$monthly = 12345, $yearly = 54321], 'default')) {
//
}
recurring
方法可用于确定用户当前是否订阅并且不再处于试用期:
if ($user->subscription('default')->recurring()) {
//
}
取消的订阅状态
要确定用户是否曾经是活跃的订阅者,但已取消其订阅,可以使用 cancelled
方法:
if ($user->subscription('default')->cancelled()) {
//
}
还可以确定用户是否已取消其订阅,但仍在其“宽限期”内。例如,如果用户在 3 月 5 日取消订阅,而订阅原定于 3 月 10 日到期,则用户在 3 月 10 日之前处于“宽限期”。请注意,在此期间 subscribed
方法仍返回 true
:
if ($user->subscription('default')->onGracePeriod()) {
//
}
要确定用户是否已取消其订阅并且不再处于“宽限期”,可以使用 ended
方法:
if ($user->subscription('default')->ended()) {
//
}
订阅范围
大多数订阅状态也可用作查询范围,以便你可以轻松地查询数据库中处于给定状态的订阅:
// 获取所有活动的订阅...
$subscriptions = Subscription::query()->active()->get();
// 获取用户的所有已取消订阅...
$subscriptions = $user->subscriptions()->cancelled()->get();
可用范围的完整列表如下:
Subscription::query()->active();
Subscription::query()->onTrial();
Subscription::query()->notOnTrial();
Subscription::query()->pastDue();
Subscription::query()->recurring();
Subscription::query()->ended();
Subscription::query()->paused();
Subscription::query()->notPaused();
Subscription::query()->onPausedGracePeriod();
Subscription::query()->notOnPausedGracePeriod();
Subscription::query()->cancelled();
Subscription::query()->notCancelled();
Subscription::query()->onGracePeriod();
Subscription::query()->notOnGracePeriod();
逾期状态
如果订阅的付款失败,将标记为 past_due
。当你的订阅处于此状态时,它将不会激活,直到客户更新其支付信息。可以使用订阅实例上的 pastDue
方法确定订阅是否逾期:
if ($user->subscription('default')->pastDue()) {
//
}
当订阅逾期时,应该指示用户更新其支付信息。可以在 Paddle 订阅设置中配置如何处理逾期订阅。
如果希望订阅在 past_due
时仍被视为活动,可以使用 Cashier 提供的 keepPastDueSubscriptionsActive
方法。通常,此方法应在 AppServiceProvider
的 register
方法中调用:
use Laravel\Paddle\Cashier;
/**
* 注册任何应用程序服务。
*
* @return void
*/
public function register()
{
Cashier::keepPastDueSubscriptionsActive();
}
当订阅处于 past_due
状态时,无法更改,直到支付信息已更新。因此,当订阅处于 past_due
状态时,swap
和 updateQuantity
方法将抛出异常。
订阅单次收费
订阅单次收费允许你对订阅者进行一次性收费:
$response = $user->subscription('default')->charge(12.99, 'Support Add-on');
与单次收费不同,此方法将立即对订阅的客户存储的支付方式进行收费。收费金额始终以订阅当前设置的货币为单位。
更新支付信息
Paddle 始终为每个订阅保存一个支付方式。如果要更新订阅的默认支付方式,首先应使用订阅模型上的 updateUrl
方法生成一个订阅“更新 URL”:
$user = App\User::find(1);
$updateUrl = $user->subscription('default')->updateUrl();
然后,可以使用生成的 URL 结合 Cashier 提供的 paddle-button
Blade 组件,允许用户启动 Paddle 小部件并更新其支付信息:
<x-paddle-button :url="$updateUrl" class="px-8 py-4">
Update Card
</x-paddle-button>
当用户完成更新其信息后,Paddle 将发送一个 subscription_updated
webhook,并在应用程序的数据库中更新订阅详细信息。
更改计划
在用户订阅了你的应用程序后,他们可能偶尔想要更改为新的订阅计划。要将用户切换到新的订阅计划,应将 Paddle 计划的标识符传递给订阅的 swap
方法:
$user = App\User::find(1);
$user->subscription('default')->swap($premium = 34567);
如果用户处于试用期,试用期将保持不变。此外,如果订阅存在“数量”,该数量也将保持不变。
如果希望在切换计划时取消用户当前的试用期,可以使用 skipTrial
方法:
$user->subscription('default')
->skipTrial()
->swap($premium = 34567);
如果希望在切换计划时立即向用户开具发票,而不是等待他们的下一个计费周期,可以使用 swapAndInvoice
方法:
$user = App\User::find(1);
$user->subscription('default')->swapAndInvoice($premium = 34567);
按比例分配
默认情况下,Paddle 在计划之间切换时按比例分配费用。noProrate
方法可用于在不按比例分配费用的情况下更新订阅:
$user->subscription('default')->noProrate()->swap($premium = 34567);
订阅数量
有时订阅会受到“数量”的影响。例如,应用程序可能会按每个账户每月收费 10 美元。要轻松增加或减少订阅数量,请使用 incrementQuantity
和 decrementQuantity
方法:
$user = User::find(1);
$user->subscription('default')->incrementQuantity();
// 在订阅的当前数量上增加五个...
$user->subscription('default')->incrementQuantity(5);
$user->subscription('default')->decrementQuantity();
// 在订阅的当前数量上减少五个...
$user->subscription('default')->decrementQuantity(5);
或者,可以使用 updateQuantity
方法设置特定数量:
$user->subscription('default')->updateQuantity(10);
noProrate
方法可用于在不按比例分配费用的情况下更新订阅数量:
$user->subscription('default')->noProrate()->updateQuantity(10);
暂停订阅
要暂停订阅,请调用用户订阅上的 pause
方法:
$user->subscription('default')->pause();
当订阅暂停时,Cashier 将自动在数据库中设置 paused_from
列。此列用于知道何时 paused
方法应开始返回 true
。例如,如果客户在 3 月 1 日暂停订阅,但订阅原定于 3 月 5 日续订,则 paused
方法将继续返回 false
,直到 3 月 5 日。
可以使用 onPausedGracePeriod
方法确定用户是否已暂停其订阅,但仍在其“宽限期”内:
if ($user->subscription('default')->onPausedGracePeriod()) {
//
}
要恢复暂停的订阅,可以调用用户订阅上的 unpause
方法:
$user->subscription('default')->unpause();
暂停的订阅无法修改。如果要切换到不同的计划或更新数量,必须先恢复订阅。
取消订阅
要取消订阅,请调用用户订阅上的 cancel
方法:
$user->subscription('default')->cancel();
当订阅被取消时,Cashier 将自动在数据库中设置 ends_at
列。此列用于知道何时 subscribed
方法应开始返回 false
。例如,如果客户在 3 月 1 日取消订阅,但订阅原定于 3 月 5 日到期,则 subscribed
方法将继续返回 true
,直到 3 月 5 日。
可以使用 onGracePeriod
方法确定用户是否已取消其订阅,但仍在其“宽限期”内:
if ($user->subscription('default')->onGracePeriod()) {
//
}
如果希望立即取消订阅,可以调用用户订阅上的 cancelNow
方法:
$user->subscription('default')->cancelNow();
Paddle 的订阅在取消后无法恢复。如果客户希望恢复其订阅,他们将不得不订阅新的订阅。
订阅试用
提前支付方式
在试用和收集支付方式详细信息时,Paddle 阻止任何订阅更改,例如切换计划或更新数量。如果希望在试用期间允许客户切换计划,则必须取消并重新创建订阅。
如果希望在仍然收集支付方式信息的情况下向客户提供试用期,应在创建订阅支付链接时使用 trialDays
方法:
$user = User::find(1);
$payLink = $user->newSubscription('default', $monthly = 12345)
->returnTo(route('home'))
->trialDays(10)
->create();
return view('billing', ['payLink' => $payLink]);
此方法将在数据库中的订阅记录上设置试用期结束日期,并指示 Paddle 在此日期之后才开始向客户收费。
如果客户的订阅在试用期结束日期之前未被取消,他们将在试用期到期后立即被收费,因此应确保通知用户其试用期结束日期。
可以使用用户实例的 onTrial
方法或订阅实例的 onTrial
方法确定用户是否在其试用期内。以下两个示例具有相同的行为:
if ($user->onTrial('default')) {
//
}
if ($user->subscription('default')->onTrial()) {
//
}
在 Paddle / Cashier 中定义试用天数
可以选择在 Paddle 仪表板中定义计划的试用天数,或者始终使用 Cashier 显式传递它们。如果选择在 Paddle 中定义计划的试用天数,则应注意新订阅,包括过去曾有订阅的客户的新订阅,将始终获得试用期,除非显式调用 trialDays(0)
方法。
无需提前支付方式
如果希望在不收集用户支付方式信息的情况下提供试用期,可以在用户附加的客户记录上设置 trial_ends_at
列为所需的试用期结束日期。这通常在用户注册期间完成:
$user = User::create([
// 其他用户属性...
]);
$user->createAsCustomer([
'trial_ends_at' => now()->addDays(10)
]);
Cashier 将此类型的试用称为“通用试用”,因为它不附加到任何现有订阅。User
实例上的 onTrial
方法将在当前日期未超过 trial_ends_at
的值时返回 true
:
if ($user->onTrial()) {
// 用户在其试用期内...
}
如果希望具体知道用户在其“通用”试用期内且尚未创建实际订阅,可以使用 onGenericTrial
方法:
if ($user->onGenericTrial()) {
// 用户在其“通用”试用期内...
}
一旦准备好为用户创建实际订阅,可以照常使用 newSubscription
方法:
$user = User::find(1);
$payLink = $user->newSubscription('default', $monthly = 12345)
->returnTo(route('home'))
->create();
在 Paddle 订阅创建后,无法延长或修改试用期。
处理 Paddle Webhooks
可以使用 Valet 的 share
命令来帮助在本地开发期间测试 webhooks。
Paddle 可以通过 webhooks 通知应用程序各种事件。默认情况下,通过 Cashier 服务提供者配置了指向 Cashier 的 webhook 控制器的路由。此控制器将处理所有传入的 webhook 请求。
默认情况下,此控制器将自动处理取消订阅(如果有太多失败的收费)(如你的 Paddle 订阅设置中定义的),订阅更新和支付方式更改;然而,正如我们将很快发现的那样,可以扩展此控制器以处理任何你喜欢的 webhook 事件。
为了确保应用程序能够处理 Paddle webhooks,请确保在 Paddle 控制面板中配置 webhook URL。默认情况下,Cashier 的 webhook 控制器监听 /paddle/webhook
URL 路径。你应该在 Paddle 控制面板中配置的所有 webhooks 的完整列表如下:
- 订阅已创建
- 订阅已更新
- 订阅已删除
- 付款成功
- 订阅付款成功
确保使用 Cashier 附带的webhook 签名验证中间件保护传入请求。
Webhooks 和 CSRF 保护
由于 Paddle webhooks 需要绕过 Laravel 的CSRF 保护,请确保在 VerifyCsrfToken
中间件中将 URI 列为例外,或将路由列在 web
中间件组之外:
protected $except = [
'paddle/*',
];
定义 Webhook 事件处理程序
Cashier 自动处理失败收费的订阅取消,但如果有其他 webhook 事件希望处理,应扩展 WebhookController
。方法名称应与 Cashier 期望的约定相对应,具体来说,方法应以 handle
和希望处理的 webhook 的“驼峰式”名称为前缀。例如,如果希望处理 payment_succeeded
webhook,应在控制器中添加一个 handlePaymentSucceeded
方法:
<?php
namespace App\Http\Controllers;
use Laravel\Paddle\Http\Controllers\WebhookController as CashierController;
class WebhookController extends CashierController
{
/**
* 处理付款成功。
*
* @param array $payload
* @return void
*/
public function handlePaymentSucceeded($payload)
{
// 处理事件
}
}
接下来,在 routes/web.php
文件中定义指向 Cashier 控制器的路由。这将覆盖 Cashier 附带的路由:
Route::post(
'paddle/webhook',
'\App\Http\Controllers\WebhookController'
);
Cashier 在接收到 webhook 时会发出 Laravel\Paddle\Events\WebhookReceived
事件,并在处理 webhook 时发出 Laravel\Paddle\Events\WebhookHandled
事件。这两个事件都包含 Paddle webhook 的完整负载。
Cashier 还会发出专用于接收到的 webhook 类型的事件。除了来自 Paddle 的完整负载外,它们还包含用于处理 webhook 的相关模型,例如计费模型、订阅或收据:
PaymentSucceeded
SubscriptionPaymentSucceeded
SubscriptionCreated
SubscriptionUpdated
SubscriptionCancelled
你还可以选择通过在 .env
文件中设置 CASHIER_WEBHOOK
环境变量来覆盖默认的内置 webhook 路由。此值应为指向 webhook 路由的完整 URL,并需要与 Paddle 控制面板中设置的 URL 匹配:
CASHIER_WEBHOOK=https://example.com/my-paddle-webhook-url
处理失败的订阅
如果客户的信用卡过期怎么办?不用担心 - Cashier 的 Webhook 控制器将为你取消客户的订阅。失败的付款将自动被控制器捕获和处理。控制器将在 Paddle 确定订阅失败时(通常在三次失败的付款尝试后)取消客户的订阅。
验证 Webhook 签名
为了保护你的 webhooks,可以使用 Paddle 的 webhook 签名。为了方便起见,Cashier 自动包含一个中间件,用于验证传入的 Paddle webhook 请求是否有效。
要启用 webhook 验证,请确保在 .env
文件中设置了 PADDLE_PUBLIC_KEY
环境变量。可以从 Paddle 账户仪表板中检索公钥。
单次收费
简单收费
如果希望对客户进行“一次性”收费,可以使用计费模型实例上的 charge
方法生成收费的支付链接。charge
方法接受收费金额(浮点数)作为第一个参数,收费描述作为第二个参数:
$payLink = $user->charge(12.99, 'Product Title');
return view('pay', ['payLink' => $payLink]);
在生成支付链接后,可以使用 Cashier 提供的 paddle-button
Blade 组件,允许用户启动 Paddle 小部件并完成收费:
<x-paddle-button :url="$payLink" class="px-8 py-4">
Buy
</x-paddle-button>
charge
方法接受一个数组作为第三个参数,允许你将任何选项传递给底层的 Paddle 支付链接创建。请查阅 Paddle 文档以了解创建收费时可用的选项:
$payLink = $user->charge(12.99, 'Product Title', [
'custom_option' => $value,
]);
收费以 cashier.currency
配置选项中指定的货币进行。默认情况下,设置为 USD。可以通过在 .env
文件中设置 CASHIER_CURRENCY
来覆盖默认货币:
CASHIER_CURRENCY=EUR
你还可以使用 Paddle 的动态定价匹配系统覆盖每种货币的价格。为此,请传递一个价格数组而不是固定金额:
$payLink = $user->charge([
'USD:19.99',
'EUR:15.99',
], 'Product Title');
产品收费
如果希望对 Paddle 中配置的特定产品进行“一次性”收费,可以使用计费模型实例上的 chargeProduct
方法生成支付链接:
$payLink = $user->chargeProduct($productId);
return view('pay', ['payLink' => $payLink]);
然后,可以将支付链接提供给 paddle-button
组件,以允许用户初始化 Paddle 小部件:
<x-paddle-button :url="$payLink" class="px-8 py-4">
Buy
</x-paddle-button>
chargeProduct
方法接受一个数组作为第二个参数,允许你将任何选项传递给底层的 Paddle 支付链接创建。请查阅 Paddle 文档以了解创建收费时可用的选项:
$payLink = $user->chargeProduct($productId, [
'custom_option' => $value,
]);
订单退款
如果需要退款 Paddle 订单,可以使用 refund
方法。此方法接受 Paddle 订单 ID 作为第一个参数。可以使用 receipts
方法检索给定计费实体的收据:
$receipt = $user->receipts()->first();
$refundRequestId = $user->refund($receipt->order_id);
还可以选择性地指定要退款的特定金额以及退款原因:
$receipt = $user->receipts()->first();
$refundRequestId = $user->refund(
$receipt->order_id, 5.00, 'Unused product time'
);
在联系 Paddle 支持时,可以使用 $refundRequestId
作为退款的参考。
收据
可以使用 receipts
方法轻松检索计费模型的收据数组:
$receipts = $user->receipts();
在为客户列出收据时,可以使用收据的辅助方法显示相关的收据信息。例如,可以选择在表格中列出每个收据,允许用户轻松下载任何收据:
<table>
@foreach ($receipts as $receipt)
<tr>
<td>{{ $receipt->paid_at->toFormattedDateString() }}</td>
<td>{{ $receipt->amount() }}</td>
<td><a href="{{ $receipt->receipt_url }}" target="_blank">Download</a></td>
</tr>
@endforeach
</table>
过去和即将到来的付款
可以使用 lastPayment
和 nextPayment
方法显示客户的过去或即将到来的经常性订阅付款:
$subscription = $user->subscription('default');
$lastPayment = $subscription->lastPayment();
$nextPayment = $subscription->nextPayment();
这两个方法都将返回一个 Laravel\Paddle\Payment
实例;然而,当计费周期结束时(例如订阅已取消),nextPayment
将返回 null
:
下次付款:{{ $nextPayment->amount() }} 到期于 {{ $nextPayment->date()->format('d/m/Y') }}
处理失败的付款
订阅付款因各种原因而失败,例如卡过期或卡余额不足。当这种情况发生时,我们建议你让 Paddle 为你处理付款失败。具体来说,可以在 Paddle 仪表板中设置 Paddle 的自动计费电子邮件。
或者,可以通过捕获 subscription_payment_failed
webhook 并在 Paddle 仪表板的 Webhook 设置中启用“订阅付款失败”选项来进行更精确的自定义:
<?php
namespace App\Http\Controllers;
use Laravel\Paddle\Http\Controllers\WebhookController as CashierController;
class WebhookController extends CashierController
{
/**
* 处理订阅付款失败。
*
* @param array $payload
* @return void
*/
public function handleSubscriptionPaymentFailed($payload)
{
// 处理失败的订阅付款...
}
}
测试
Paddle 目前缺乏一个适当的 CRUD API,因此需要手动测试计费流程。Paddle 也缺乏一个沙盒开发环境,因此你进行的任何卡收费都是实时收费。为了绕过这一点,我们建议在测试期间使用 100% 折扣的优惠券或免费产品。