Laravel Cashier
介绍
Laravel Cashier 提供了一个对 Stripe 和 Braintree 订阅计费服务的表达性、流畅的接口。它几乎处理了所有你不想写的订阅计费样板代码。除了基本的订阅管理,Cashier 还可以处理优惠券、交换订阅、订阅“数量”、取消宽限期,甚至生成发票 PDF。
如果你只进行“一次性”收费而不提供订阅,你不应该使用 Cashier。相反,直接使用 Stripe 和 Braintree SDK。
升级 Cashier
在升级到 Cashier 的新主要版本时,务必仔细查看升级指南。
配置
Stripe
Composer
首先,将 Stripe 的 Cashier 包添加到你的依赖项中:
composer require laravel/cashier
数据库迁移
在使用 Cashier 之前,我们还需要准备数据库。我们需要在 users
表中添加几个列,并创建一个新的 subscriptions
表来保存所有客户的订阅:
Schema::table('users', function ($table) {
$table->string('stripe_id')->nullable()->collation('utf8mb4_bin');
$table->string('card_brand')->nullable();
$table->string('card_last_four', 4)->nullable();
$table->timestamp('trial_ends_at')->nullable();
});
Schema::create('subscriptions', function ($table) {
$table->increments('id');
$table->unsignedInteger('user_id');
$table->string('name');
$table->string('stripe_id')->collation('utf8mb4_bin');
$table->string('stripe_plan');
$table->integer('quantity');
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
});
创建迁移后,运行 migrate
Artisan 命令。
可计费模型
接下来,将 Billable
trait 添加到你的模型定义中。此 trait 提供了各种方法,允许你执行常见的计费任务,例如创建订阅、应用优惠券和更新信用卡信息:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
API 密钥
最后,你应该在 services.php
配置文件中配置你的 Stripe 密钥。你可以从 Stripe 控制面板中获取你的 Stripe API 密钥:
'stripe' => [
'model' => App\User::class,
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],
Braintree
Braintree 注意事项
对于许多操作,Cashier 的 Stripe 和 Braintree 实现功能相同。两种服务都提供信用卡订阅计费,但 Braintree 还支持通过 PayPal 付款。然而,Braintree 也缺乏一些 Stripe 支持的功能。在决定使用 Stripe 或 Braintree 时,你应该记住以下几点:
- Braintree 支持 PayPal,而 Stripe 不支持。
- Braintree 不支持订阅的
increment
和decrement
方法。这是 Braintree 的限制,而不是 Cashier 的限制。 - Braintree 不支持基于百分比的折扣。这是 Braintree 的限制,而不是 Cashier 的限制。
Composer
首先,将 Braintree 的 Cashier 包添加到你的依赖项中:
composer require "laravel/cashier-braintree":"~2.0"
计划信用优惠券
在使用 Braintree 的 Cashier 之前,你需要在 Braintree 控制面板中定义一个 plan-credit
折扣。此折扣将用于正确按比例调整从年度到月度计费或从月度到年度计费的订阅。
在 Braintree 控制面板中配置的折扣金额可以是你希望的任何值,因为 Cashier 将在每次应用优惠券时用我们自己的自定义金额覆盖定义的金额。此优惠券是必需的,因为 Braintree 本身不支持跨订阅频率按比例调整订阅。
数据库迁移
在使用 Cashier 之前,我们需要准备数据库。我们需要在 users
表中添加几个列,并创建一个新的 subscriptions
表来保存所有客户的订阅:
Schema::table('users', function ($table) {
$table->string('braintree_id')->nullable();
$table->string('paypal_email')->nullable();
$table->string('card_brand')->nullable();
$table->string('card_last_four')->nullable();
$table->timestamp('trial_ends_at')->nullable();
});
Schema::create('subscriptions', function ($table) {
$table->increments('id');
$table->unsignedInteger('user_id');
$table->string('name');
$table->string('braintree_id');
$table->string('braintree_plan');
$table->integer('quantity');
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
});
创建迁移后,运行 migrate
Artisan 命令。
可计费模型
接下来,将 Billable
trait 添加到你的模型定义中:
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
API 密钥
接下来,你应该在 services.php
文件中配置以下选项:
'braintree' => [
'model' => App\User::class,
'environment' => env('BRAINTREE_ENV'),
'merchant_id' => env('BRAINTREE_MERCHANT_ID'),
'public_key' => env('BRAINTREE_PUBLIC_KEY'),
'private_key' => env('BRAINTREE_PRIVATE_KEY'),
],
然后你应该将以下 Braintree SDK 调用添加到 AppServiceProvider
服务提供者的 boot
方法中:
\Braintree_Configuration::environment(config('services.braintree.environment'));
\Braintree_Configuration::merchantId(config('services.braintree.merchant_id'));
\Braintree_Configuration::publicKey(config('services.braintree.public_key'));
\Braintree_Configuration::privateKey(config('services.braintree.private_key'));
货币配置
默认的 Cashier 货币是美元 (USD)。你可以通过在一个服务提供者的 boot
方法中调用 Cashier::useCurrency
方法来更改默认货币。useCurrency
方法接受两个字符串参数:货币和货币的符号:
use Laravel\Cashier\Cashier;
Cashier::useCurrency('eur', '€');
订阅
创建订阅
要创建订阅,首先检索你的可计费模型的实例,通常是 App\User
的实例。一旦你检索到模型实例,你可以使用 newSubscription
方法来创建模型的订阅:
$user = User::find(1);
$user->newSubscription('main', 'premium')->create($stripeToken);
传递给 newSubscription
方法的第一个参数应该是订阅的名称。如果你的应用程序只提供一个订阅,你可以称之为 main
或 primary
。第二个参数是用户订阅的特定 Stripe / Braintree 计划。此值应与 Stripe 或 Braintree 中计划的标识符相对应。
create
方法接受一个 Stripe 信用卡 / 源令牌,将开始订阅并更新你的数据库中的客户 ID 和其他相关的计费信息。
额外的用户详细信息
如果你想指定额外的客户详细信息,可以将它们作为 create
方法的第二个参数传递:
$user->newSubscription('main', 'monthly')->create($stripeToken, [
'email' => $email,
]);
要了解 Stripe 或 Braintree 支持的其他字段,请查看 Stripe 的客户创建文档或相应的Braintree 文档。
优惠券
如果你想在创建订阅时应用优惠券,可以使用 withCoupon
方法:
$user->newSubscription('main', 'monthly')
->withCoupon('code')
->create($stripeToken);
检查订阅状态
一旦用户订阅了你的应用程序,你可以使用各种方便的方法轻松检查他们的订阅状态。首先,subscribed
方法返回 true
,如果用户有一个活动的订阅,即使订阅当前在其试用期内:
if ($user->subscribed('main')) {
//
}
subscribed
方法也可以作为路由中间件的一个很好的候选者,允许你根据用户的订阅状态过滤对路由和控制器的访问:
public function handle($request, Closure $next)
{
if ($request->user() && ! $request->user()->subscribed('main')) {
// 该用户不是付费客户...
return redirect('billing');
}
return $next($request);
}
如果你想确定用户是否仍在其试用期内,可以使用 onTrial
方法。此方法可用于向用户显示警告,告知他们仍在试用期内:
if ($user->subscription('main')->onTrial()) {
//
}
subscribedToPlan
方法可用于确定用户是否订阅了基于给定 Stripe / Braintree 计划 ID 的给定计划。在此示例中,我们将确定用户的 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()) {
//
}
更改计划
用户订阅你的应用程序后,他们可能偶尔想要更改为新的订阅计划。要将用户交换到新的订阅,请将计划的标识符传递给 swap
方法:
$user = App\User::find(1);
$user->subscription('main')->swap('provider-plan-id');
如果用户在试用期内,试用期将保持不变。此外,如果订阅存在“数量”,该数量也将保持不变。
如果你想交换计划并取消用户当前的任何试用期,可以使用 skipTrial
方法:
$user->subscription('main')
->skipTrial()
->swap('provider-plan-id');
订阅数量
订阅数量仅由 Cashier 的 Stripe 版本支持。Braintree 没有与 Stripe 的“数量”相对应的功能。
有时订阅会受到“数量”的影响。例如,你的应用程序可能会为帐户上的每个用户每月收取 10 美元。要轻松增加或减少订阅数量,请使用 incrementQuantity
和 decrementQuantity
方法:
$user = User::find(1);
$user->subscription('main')->incrementQuantity();
// 在订阅的当前数量上增加五个...
$user->subscription('main')->incrementQuantity(5);
$user->subscription('main')->decrementQuantity();
// 在订阅的当前数量上减少五个...
$user->subscription('main')->decrementQuantity(5);
或者,你可以使用 updateQuantity
方法设置特定数量:
$user->subscription('main')->updateQuantity(10);
noProrate
方法可用于在不按比例分配费用的情况下更新订阅的数量:
$user->subscription('main')->noProrate()->updateQuantity(10);
有关订阅数量的更多信息,请查阅Stripe 文档。
订阅税
要指定用户在订阅上支付的税率,请在你的可计费模型上实现 taxPercentage
方法,并返回一个介于 0 和 100 之间的数值,最多保留两位小数。
public function taxPercentage() {
return 20;
}
taxPercentage
方法使你能够在模型基础上应用税率,这对于跨多个国家和税率的用户群可能很有帮助。
taxPercentage
方法仅适用于订阅费用。如果你使用 Cashier 进行“一次性”收费,你需要在那时手动指定税率。
同步税率
当更改 taxPercentage
方法返回的硬编码值时,用户的任何现有订阅的税率设置将保持不变。如果你希望使用返回的 taxPercentage
值更新现有订阅的税率,你应该在用户的订阅实例上调用 syncTaxPercentage
方法:
$user->subscription('main')->syncTaxPercentage();
订阅锚定日期
修改订阅锚定日期仅由 Cashier 的 Stripe 版本支持。
默认情况下,计费周期锚定是订阅创建的日期,或者如果使用试用期,则是试用期结束的日期。如果你想修改计费锚定日期,可以使用 anchorBillingCycleOn
方法:
use App\User;
use Carbon\Carbon;
$user = User::find(1);
$anchor = Carbon::parse('first day of next month');
$user->newSubscription('main', 'premium')
->anchorBillingCycleOn($anchor->startOfDay())
->create($stripeToken);
有关管理订阅计费周期的更多信息,请查阅Stripe 计费周期文档
取消订阅
要取消订阅,请在用户的订阅上调用 cancel
方法:
$user->subscription('main')->cancel();
当订阅被取消时,Cashier 将自动在你的数据库中设置 ends_at
列。此列用于知道何时 subscribed
方法应开始返回 false
。例如,如果客户在 3 月 1 日取消订阅,但订阅原定于 3 月 5 日到期,则 subscribed
方法将继续返回 true
,直到 3 月 5 日。
你可以使用 onGracePeriod
方法确定用户是否已取消其订阅,但仍在其“宽限期”内:
if ($user->subscription('main')->onGracePeriod()) {
//
}
如果你希望立即取消订阅,请在用户的订阅上调用 cancelNow
方法:
$user->subscription('main')->cancelNow();
恢复订阅
如果用户已取消其订阅并且你希望恢复它,请使用 resume
方法。用户必须仍在其宽限期内才能恢复订阅:
$user->subscription('main')->resume();
如果用户取消订阅,然后在订阅完全过期之前恢复该订阅,他们将不会立即被计费。相反,他们的订阅将被重新激活,并且他们将在原始计费周期上被计费。
订阅试用
需要信用卡
如果你想在仍然收集支付方式信息的情况下向客户提供试用期,你应该在创建订阅时使用 trialDays
方法:
$user = User::find(1);
$user->newSubscription('main', 'monthly')
->trialDays(10)
->create($stripeToken);
此方法将在数据库中的订阅记录上设置试用期结束日期,并指示 Stripe / Braintree 在此日期之后才开始向客户计费。
如果客户的订阅在试用期结束日期之前未取消,他们将在试用期到期后立即被收费,因此你应该确保通知用户他们的试用期结束日期。
trialUntil
方法允许你提供一个 DateTime
实例来指定试用期应何时结束:
use Carbon\Carbon;
$user->newSubscription('main', 'monthly')
->trialUntil(Carbon::now()->addDays(10))
->create($stripeToken);
你可以使用用户实例的 onTrial
方法或订阅实例的 onTrial
方法来确定用户是否在其试用期内。以下两个示例是相同的:
if ($user->onTrial('main')) {
//
}
if ($user->subscription('main')->onTrial()) {
//
}
不需要信用卡
如果你想在不收集用户支付方式信息的情况下提供试用期,你可以将用户记录上的 trial_ends_at
列设置为你想要的试用期结束日期。这通常在用户注册期间完成:
$user = User::create([
// 填充其他用户属性...
'trial_ends_at' => now()->addDays(10),
]);
确保在你的模型定义中为 trial_ends_at
添加一个日期变换器。
Cashier 将此类型的试用称为“通用试用”,因为它不附加到任何现有订阅。User
实例上的 onTrial
方法将返回 true
,如果当前日期未超过 trial_ends_at
的值:
if ($user->onTrial()) {
// 用户在其试用期内...
}
如果你希望具体知道用户在其“通用”试用期内并且尚未创建实际订阅,可以使用 onGenericTrial
方法:
if ($user->onGenericTrial()) {
// 用户在其“通用”试用期内...
}
一旦你准备好为用户创建实际订阅,可以像往常一样使用 newSubscription
方法:
$user = User::find(1);
$user->newSubscription('main', 'monthly')->create($stripeToken);
客户
创建客户
有时,你可能希望创建一个 Stripe 客户而不开始订阅。你可以使用 createAsStripeCustomer
方法实现此目的:
$user->createAsStripeCustomer();
一旦客户在 Stripe 中创建,你可以在以后开始订阅。
此方法的 Braintree 等效方法是 createAsBraintreeCustomer
方法。
信用卡
检索信用卡
可计费模型实例上的 cards
方法返回一个 Laravel\Cashier\Card
实例的集合:
$cards = $user->cards();
要检索默认卡,可以使用 defaultCard
方法:
$card = $user->defaultCard();
确定是否有卡
你可以使用 hasCardOnFile
方法检查客户是否在其帐户中附加了信用卡:
if ($user->hasCardOnFile()) {
//
}
更新信用卡
updateCard
方法可用于更新客户的信用卡信息。此方法接受一个 Stripe 令牌,并将新信用卡分配为默认计费来源:
$user->updateCard($stripeToken);
要将你的卡信息与 Stripe 中客户的默认卡信息同步,可以使用 updateCardFromStripe
方法:
$user->updateCardFromStripe();
删除信用卡
要删除卡,你应该首先使用 cards
方法检索客户的卡。然后,你可以在要删除的卡实例上调用 delete
方法:
foreach ($user->cards() as $card) {
$card->delete();
}
如果你删除了默认卡,请确保使用 updateCardFromStripe
方法将新默认卡与数据库同步。
deleteCards
方法将删除应用程序存储的所有卡信息:
$user->deleteCards();
如果用户有一个活动的订阅,你应该考虑阻止他们删除最后一个剩余的支付来源。
处理 Stripe Webhooks
Stripe 和 Braintree 都可以通过 webhooks 通知你的应用程序各种事件。要处理 Stripe webhooks,定义一个指向 Cashier 的 webhook 控制器的路由。此控制器将处理所有传入的 webhook 请求并将它们分派到正确的控制器方法:
Route::post(
'stripe/webhook',
'\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);
注册路由后,请确保在 Stripe 控制面板设置中配置 webhook URL。
默认情况下,此控制器将自动处理取消订阅的过多失败收费(由你的 Stripe 设置定义)、客户更新、客户删除、订阅更新和信用卡更改;然而,正如我们将很快发现的,你可以扩展此控制器以处理你喜欢的任何 webhook 事件。
确保使用 Cashier 的包含的webhook 签名验证中间件保护传入请求。
Webhooks 和 CSRF 保护
由于 Stripe webhooks 需要绕过 Laravel 的CSRF 保护,请确保在 VerifyCsrfToken
中间件中将 URI 列为例外,或将路由列在 web
中间件组之外:
protected $except = [
'stripe/*',
];
定义 Webhook 事件处理程序
Cashier 自动处理失败收费的订阅取消,但如果你有其他 Stripe webhook 事件想要处理,请扩展 Webhook 控制器。你的方法名称应与 Cashier 期望的约定相对应,具体来说,方法应以 handle
和你希望处理的 Stripe webhook 的“驼峰式”名称为前缀。例如,如果你希望处理 invoice.payment_succeeded
webhook,你应该在控制器中添加一个 handleInvoicePaymentSucceeded
方法:
<?php
namespace App\Http\Controllers;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;
class WebhookController extends CashierController
{
/**
* 处理发票支付成功。
*
* @param array $payload
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handleInvoicePaymentSucceeded($payload)
{
// 处理事件
}
}
接下来,在你的 routes/web.php
文件中定义一个指向 Cashier 控制器的路由:
Route::post(
'stripe/webhook',
'\App\Http\Controllers\WebhookController@handleWebhook'
);
处理失败的订阅
如果客户的信用卡过期怎么办?不用担心 - Cashier 包含一个 Webhook 控制器,可以轻松为你取消客户的订阅。如上所述,你只需将路由指向控制器:
Route::post(
'stripe/webhook',
'\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);
就是这样!失败的付款将被控制器捕获和处理。控制器将在 Stripe 确定订阅失败时(通常在三次失败的付款尝试后)取消客户的订阅。
验证 Webhook 签名
要保护你的 webhooks,可以使用Stripe 的 webhook 签名。为了方便起见,Cashier 自动包含一个中间件,该中间件验证传入的 Stripe webhook 请求是否有效。
要启用 webhook 验证,请确保在 services
配置文件中设置 stripe.webhook.secret
配置值。webhook secret
可以从你的 Stripe 帐户仪表板中获取。
处理 Braintree Webhooks
Stripe 和 Braintree 都可以通过 webhooks 通知你的应用程序各种事件。要处理 Braintree webhooks,定义一个指向 Cashier 的 webhook 控制器的路由。此控制器将处理所有传入的 webhook 请求并将它们分派到正确的控制器方法:
Route::post(
'braintree/webhook',
'\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);
注册路由后,请确保在 Braintree 控制面板设置中配置 webhook URL。
默认情况下,此控制器将自动处理取消订阅的过多失败收费(由你的 Braintree 设置定义);然而,正如我们将很快发现的,你可以扩展此控制器以处理你喜欢的任何 webhook 事件。
Webhooks 和 CSRF 保护
由于 Braintree webhooks 需要绕过 Laravel 的CSRF 保护,请确保在 VerifyCsrfToken
中间件中将 URI 列为例外,或将路由列在 web
中间件组之外:
protected $except = [
'braintree/*',
];
定义 Webhook 事件处理程序
Cashier 自动处理失败收费的订阅取消,但如果你有其他 Braintree webhook 事件想要处理,请扩展 Webhook 控制器。你的方法名称应与 Cashier 期望的约定相对应,具体来说,方法应以 handle
和你希望处理的 Braintree webhook 的“驼峰式”名称为前缀。例如,如果你希望处理 dispute_opened
webhook,你应该在控制器中添加一个 handleDisputeOpened
方法:
<?php
namespace App\Http\Controllers;
use Braintree\WebhookNotification;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;
class WebhookController extends CashierController
{
/**
* 处理新的争议。
*
* @param \Braintree\WebhookNotification $webhook
* @return \Symfony\Component\HttpFoundation\Responses
*/
public function handleDisputeOpened(WebhookNotification $webhook)
{
// 处理 Webhook...
}
}
处理失败的订阅
如果客户的信用卡过期怎么办?不用担心 - Cashier 包含一个 Webhook 控制器,可以轻松为你取消客户的订阅。只需将路由指向控制器:
Route::post(
'braintree/webhook',
'\Laravel\Cashier\Http\Controllers\WebhookController@handleWebhook'
);
就是这样!失败的付款将被控制器捕获和处理。控制器将在 Braintree 确定订阅失败时(通常在三次失败的付款尝试后)取消客户的订阅。不要忘记:你需要在 Braintree 控制面板设置中配置 webhook URI。
单次收费
简单收费
使用 Stripe 时,charge
方法接受你希望收费的金额,以应用程序使用的货币的最低分母表示。然而,使用 Braintree 时,你应该将完整的美元金额传递给 charge
方法:
如果你想对订阅客户的信用卡进行“一次性”收费,可以在可计费模型实例上使用 charge
方法。
// Stripe 接受以美分为单位的收费...
$stripeCharge = $user->charge(100);
// Braintree 接受以美元为单位的收费...
$user->charge(1);
charge
方法接受一个数组作为其第二个参数,允许你将任何选项传递给底层的 Stripe / Braintree 收费创建。请查阅 Stripe 或 Braintree 文档,了解创建收费时可用的选项:
$user->charge(100, [
'custom_option' => $value,
]);
如果收费失败,charge
方法将抛出异常。如果收费成功,完整的 Stripe / Braintree 响应将从方法中返回:
try {
$response = $user->charge(100);
} catch (Exception $e) {
//
}
带发票的收费
有时你可能需要进行一次性收费,但也需要为收费生成发票,以便你可以向客户提供 PDF 收据。invoiceFor
方法可以让你做到这一点。例如,让我们为客户开具 5.00 美元的“一次性费用”发票:
// Stripe 接受以美分为单位的收费...
$user->invoiceFor('One Time Fee', 500);
// Braintree 接受以美元为单位的收费...
$user->invoiceFor('One Time Fee', 5);
发票将立即对用户的信用卡进行收费。invoiceFor
方法还接受一个数组作为其第三个参数。此数组包含发票项目的计费选项。方法接受的第四个参数也是一个数组。此最终参数接受发票本身的计费选项:
$user->invoiceFor('Stickers', 500, [
'quantity' => 50,
], [
'tax_percent' => 21,
]);
如果你使用 Braintree 作为你的计费提供商,你必须在调用 invoiceFor
方法时包含一个 description
选项:
$user->invoiceFor('One Time Fee', 500, [
'description' => 'your invoice description here',
]);
invoiceFor
方法将创建一个 Stripe 发票,该发票将重试失败的计费尝试。如果你不希望发票重试失败的收费,你需要在第一次失败的收费后使用 Stripe API 关闭它们。
退款
如果你需要退款 Stripe 收费,可以使用 refund
方法。此方法接受 Stripe 收费 ID 作为其唯一参数:
$stripeCharge = $user->charge(100);
$user->refund($stripeCharge->id);
发票
你可以使用 invoices
方法轻松检索可计费模型的发票数组:
$invoices = $user->invoices();
// 在结果中包含待处理的发票...
$invoices = $user->invoicesIncludingPending();
在为客户列出发票时,你可以使用发票的辅助方法来显示相关的发票信息。例如,你可能希望在表格中列出每张发票,允许用户轻松下载其中的任何一张:
<table>
@foreach ($invoices as $invoice)
<tr>
<td>{{ $invoice->date()->toFormattedDateString() }}</td>
<td>{{ $invoice->total() }}</td>
<td><a href="/user/invoice/{{ $invoice->id }}">下载</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',
]);
});