centric/plans-for-laravel

Laravel Plans 是一个用于需要管理计划、功能、订阅和计划或有限、可计数功能的SaaS应用程序的包。

v1.0.0 2023-07-03 05:06 UTC

This package is not auto-updated.

Last update: 2024-09-24 08:58:11 UTC


README

Build Status codecov StyleCI Latest Stable Version Total Downloads Monthly Downloads License

PayPal

Laravel Plans

Laravel Plans 是一个用于需要管理计划、功能、订阅和计划或有限、可计数功能的SaaS应用程序的包。

Laravel Cashier

虽然 Laravel Cashier 在这项工作中做得很好,但也有一些对SaaS应用程序有用的功能

  • 可计数的、有限的功能 - 如果您计划限制订阅者可以拥有的资源量并跟踪使用情况,此包可为您完成此操作。
  • 内置循环性,可自定义循环周期 - 虽然Stripe或其他服务将您限制为每天、每周、每月或每年订阅用户,但此包允许您为任何订阅或计划定义自己的天数。
  • 事件驱动 - 您可以监听事件。如果用户按时支付发票,您是否可以给予下一个订阅3天免费试用?

安装

安装包

$ composer require creatydev/plans

如果您的Laravel版本不支持包发现,请在您的 config/app.php 文件中的 providers 数组中添加此行

Creatydev\Plans\PlansServiceProvider::class,

发布配置文件和迁移文件

$ php artisan vendor:publish --provider=Creatydev\Plans\PlansServiceProvider

迁移数据库

$ php artisan migrate

HasPlans 特性添加到您的Eloquent模型中

use Creatydev\Plans\Traits\HasPlans;

class User extends Model {
    use HasPlans;
    ...
}

创建计划

类似订阅的系统的基本单元是计划。您可以使用 Creatydev\Plans\Models\PlanModel 或您的模型创建它,如果已实现自己的模型。

$plan = PlanModel::create([
    'name' => 'Enterprise',
    'description' => 'The biggest plans of all.',
    'price' => 20.99,
    'currency' => 'EUR',
    'duration' => 30, // in days
    'metadata' => ['key1' => 'value1', ...],
]);

功能

每个计划都有功能。它们可以是可计数的,这些是有限或无限的,或者只是存储信息,例如特定权限。

使用以下方式标记功能类型:

  • feature,是一个不进行计数的单个字符串。例如,您可以存储权限。
  • limit,是一个数字。对于此类功能,将填充 limit 属性。它旨在测量用户从订阅中消耗了多少此类功能。例如,您可以计算用户在一个月内(或在本例中为30天的周期内)消耗了多少构建分钟。

注意:对于无限功能,将设置 limit 字段为任何负值。

要将功能附加到您的计划,您可以使用 features() 关系并将所需的 Creatydev\Plans\Models\PlanFeatureModel 实例作为参数传递。

$plan->features()->saveMany([
    new PlanFeatureModel([
        'name' => 'Vault access',
        'code' => 'vault.access',
        'description' => 'Offering access to the vault.',
        'type' => 'feature',
        'metadata' => ['key1' => 'value1', ...],
    ]),
    new PlanFeatureModel([
        'name' => 'Build minutes',
        'code' => 'build.minutes',
        'description' => 'Build minutes used for CI/CD.',
        'type' => 'limit',
        'limit' => 2000,
        'metadata' => ['key1' => 'value1', ...],
    ]),
    new PlanFeatureModel([
        'name' => 'Users amount',
        'code' => 'users.amount',
        'description' => 'The maximum amount of users that can use the app at the same time.',
        'type' => 'limit',
        'limit' => -1, // or any negative value
        'metadata' => ['key1' => 'value1', ...],
    ]),
    ...
]);

稍后,您可以直接从订阅中检索权限。

$subscription->features()->get(); // All features
$subscription->features()->code($codeId)->first(); // Feature with a specific code.
$subscription->features()->limited()->get(); // Only countable/unlimited features.
$subscription->features()->feature()->get(); // Uncountable, permission-like features.

订阅计划

您的用户可以订阅计划,持续一定天数或直到特定日期。

$subscription = $user->subscribeTo($plan, 30); // 30 days
$subscription->remainingDays(); // 29 (29 days, 23 hours, ...)

默认情况下,计划被标记为 recurring,因此它在到期后可扩展,如果您打算这样做,请参阅下面的 循环性 部分。

如果您不想进行循环订阅,可以将 false 作为第三个参数传递。

$subscription = $user->subscribeTo($plan, 30, false); // 30 days, non-recurrent

如果您计划订阅用户直到特定日期,可以传递包含日期、日期时间或Carbon实例的字符串。

如果您的订阅是循环的,循环周期的天数是到期日期和当前日期之间的差异。

$user->subscribeToUntil($plan, '2018-12-21');
$user->subscribeToUntil($plan, '2018-12-21 16:54:11');
$user->subscribeToUntil($plan, Carbon::create(2018, 12, 21, 16, 54, 11));

$user->subscribeToUntil($plan, '2018-12-21', false); // no recurrency

注意:如果用户已经订阅,则 subscribeTo() 将返回 false。为了避免这种情况,请升级或扩展订阅。

升级订阅

当前订阅计划的升级可以通过两种方式完成:它要么通过已过去的天数来延长当前订阅,要么创建一个新的订阅,作为当前订阅的扩展。

无论哪种方式,您都必须传递一个布尔值作为第三个参数。默认情况下,它将延长当前订阅。

// The current subscription got longer with 60 days.
$currentSubscription = $user->upgradeCurrentPlanTo($anotherPlan, 60, true);

// A new subscription, with 60 days valability, starting when the current one ends.
$newSubscription = $user->upgradeCurrentPlanTo($anotherPlan, 60, false);

与订阅方法一样,升级也支持日期作为第三个参数,如果您计划在当前订阅结束时创建一个新的订阅。

$user->upgradeCurrentPlanToUntil($anotherPlan, '2018-12-21', false);
$user->upgradeCurrentPlanToUntil($anotherPlan, '2018-12-21 16:54:11', false);
$user->upgradeCurrentPlanToUntil($anotherPlan, Carbon::create(2018, 12, 21, 16, 54, 11), false);

如果您的第三个参数是 false,则可以传递第四个参数。如果您想将新的订阅标记为可续订的,则应该传递它。

// Creates a new subscription that starts at the end of the current one, for 30 days and recurrent.
$newSubscription = $user->upgradeCurrentPlanTo($anotherPlan, 30, false, true);

延长当前订阅

升级使用扩展方法,因此它使用相同的参数,但是您不需要将计划模型作为第一个参数传递。

// The current subscription got extended with 60 days.
$currentSubscription = $user->extendCurrentSubscriptionWith(60, true);

// A new subscription, which starts at the end of the current one.
$newSubscrioption = $user->extendCurrentSubscriptionWith(60, false);

// A new subscription, which starts at the end of the current one and is recurring.
$newSubscrioption = $user->extendCurrentSubscriptionWith(60, false, true);

延长也支持日期。

$user->extendCurrentSubscriptionUntil('2018-12-21');

取消订阅

您可以取消订阅。如果订阅尚未完成(尚未过期),它将被标记为 待取消。当过期日期超过当前时间并且仍然被取消时,它将被完全取消。

// Returns false if there is not an active subscription.
$user->cancelCurrentSubscription();
$lastActiveSubscription = $user->lastActiveSubscription();

$lastActiveSubscription->isCancelled(); // true
$lastActiveSubscription->isPendingCancellation(); // true
$lastActiveSubscription->isActive(); // false

$lastActiveSubscription->hasStarted();
$lastActiveSubscription->hasExpired();

消费可计数的特性

要消费 limit 类型的特性,您必须在订阅实例中调用 consumeFeature() 方法。

要获取订阅实例,您可以在实现该特性的用户中调用 activeSubscription() 方法。作为预检查,不要忘记从用户实例中调用 hasActiveSubscription() 来确保它已经订阅了。

if ($user->hasActiveSubscription()) {
    $subscription = $user->activeSubscription();
    $subscription->consumeFeature('build.minutes', 10);

    $subscription->getUsageOf('build.minutes'); // 10
    $subscription->getRemainingOf('build.minutes'); // 1990
}

consumeFeature() 方法将返回

  • false 如果特性不存在,特性不是 limit 或数量超过当前特性允许量
  • true 如果消费操作成功
// Note: The remaining of build.minutes is now 1990

$subscription->consumeFeature('build.minutes', 1991); // false
$subscription->consumeFeature('build.hours', 1); // false
$subscription->consumeFeature('build.minutes', 30); // true

$subscription->getUsageOf('build.minutes'); // 40
$subscription->getRemainingOf('build.minutes'); // 1960

如果 consumeFeature() 遇到无限特性,它会消耗它,并且会像数据库中的正常记录一样跟踪使用情况,但永远不会返回 false。剩余的数量对于无限特性始终是 -1

consumeFeature() 方法的逆操作是 unconsumeFeature()。它的工作方式相同,但方向相反。

// Note: The remaining of build.minutes is 1960

$subscription->consumeFeature('build.minutes', 60); // true

$subscription->getUsageOf('build.minutes'); // 100
$subscription->getRemainingOf('build.minutes'); // 1900

$subscription->unconsumeFeature('build.minutes', 100); // true
$subscription->unconsumeFeature('build.hours', 1); // false

$subscription->getUsageOf('build.minutes'); // 0
$subscription->getRemainingOf('build.minutes'); // 2000

在无限特性上使用 unconsumeFeature() 方法也会减少使用量,但它永远不会达到负值。

支付

即使没有明确使用集成的支付,此包也能很好地工作。这是很好的,因为文档中之前解释的特性可以在不使用集成支付系统的情况下使用。如果您有自己的支付系统,您可以使用它。确保您检查下面的 周期性 部分,以了解您如何根据用户的最后订阅来收费以及如何处理周期性。

配置Stripe

此包包含一个Stripe计费功能,可以帮助您在订阅时或按需处理 周期性(如下文所述)时对订阅者进行收费。

为了使其与Laravel Cashier一样优雅,您必须通过添加Stripe来配置您的 config/services.php 文件。

'stripe' => [
    'key' => env('STRIPE_KEY'),
    'secret' => env('STRIPE_SECRET'),
],

使用Stripe

如果您现在已经很熟悉订阅、延长、升级或取消订阅而不需要主动传递支付方式,还有一些额外的功能可以给您支付控制权。

  • 计划的费用从您的 plans 表中获取。如果您想进行一些处理并设置另一个计费价格,您可以这样做。稍后会有解释。

  • 延长或升级不会向用户收费,只有订阅方法会自动为您这样做,如果您告诉包这样做的话。您希望在订阅开始时向用户收费,因此您必须遍历所有订阅者并检查他们的订阅是否已过期,并在cron命令中自动续订,例如。

  • 您必须传递一个Stripe令牌。每次您想要进行支付时,都需要传递一个Stripe令牌。此包通过具有本地Stripe客户表的表来帮助您跟踪您的客户。

  • 事件会在支付成功或失败时触发。无需设置webhooks。事件由Stripe Charge触发,无论成功或失败。

使用Stripe Charge进行订阅

要使用Stripe Token订阅您的用户,您必须明确传递一个Stripe Token

$user->withStripe()->withStripeToken('tok_...')->subscribeTo($plan, 53); // 53 days

默认情况下,计费金额从plans表检索。但是,您可以在过程中更改价格,由您自行决定

$user->withStripe()->setChargingPriceTo(10, 'USD')->withStripeToken('tok_...')->subscribeTo($plan, 30);

无论计划价格是多少,计费价格将为10美元,因为我们覆盖了计费价格。

由于计费不支持extendCurrentSubscriptionWith()extendCurrentSubscriptionUntil()upgradeCurrentPlanTo()upgradeCurrentPlanToUntil(),使用withStripe()将没有任何效果,除非您告诉它们创建一个新的计划,作为当前计划的扩展

// This will create a new upgraded plan that starts at the end of the current one, which is recurring and will be needed to be paid to be active.
$user->withStripe()->upgradeCurrentPlanTo($plan, 30, false, true);

请注意,即使是这样的,该方法也不会向您的用户收费,因为新的订阅没有开始。由于这个新的订阅将在当前订阅结束后开始,您必须手动收费,如下所述。

周期性

此包不支持Cashier支持的内容:Stripe计划与Stripe优惠券。此包可以让您成为主人,无需使用第三方来处理订阅和周期性。主要优势是您可以定义自己的周期性天数,而Stripe受限于每天、每周、每月和每年。

要处理周期性,有一个名为renewSubscription的方法可以为您完成这项工作。您必须遍历所有您的订阅者。最好运行一个cron命令,该命令将调用每个订阅者的方法。

此方法将在需要时为用户续订(订阅)。

foreach(User::all() as $user) {
    $user->renewSubscription();
}

如果您使用集成的Stripe Charge功能,您必须传递一个Stripe Token以从该用户处收费。由于Stripe Tokens是一次性的(一次性使用),您必须管理从您的用户那里获取令牌。

$user->renewSubscription('tok...');

像往常一样,如果支付已处理,将触发Creatydev\Plans\Stripe\ChargeSuccessful事件,如果支付失败,将触发Creatydev\Plans\Stripe\ChargeFailed事件。

到期订阅

不使用本地Stripe Charge功能的订阅永远不会标记为Due,因为它们默认都是已支付的。

如果您的应用程序使用自己的支付方式,您可以传递一个闭包给以下chargeForLastDueSubscription()方法,该方法将帮助您控制到期订阅

$user->chargeForLastDueSubscription(function($subscription) {
    // process the payment here

    if($paymentSuccessful) {
        $subscription->update([
            'is_paid' => true,
            'starts_on' => Carbon::now(),
            'expires_on' => Carbon::now()->addDays($subscription->recurring_each_days),
        ]);
        
        return $subscription;
    }
    
    return null;
});

在支付失败时,它们将被标记为Due。它们需要支付,并且每个动作,如订阅、升级或扩展,总是会尝试通过删除最后一个、创建在上述动作中提到的预期之一并尝试支付它来重新支付订阅。

为此,chargeForLastDueSubscription()将帮助您为最后一个未支付的订阅向用户收费。您必须明确传递一个Stripe Token

$user->withStripe()->withStripeToken('tok_...')->chargeForLastDueSubscription();

对于此方法,在成功收费或失败收费时,将抛出\Creatydev\Plans\Events\Stripe\DueSubscriptionChargeSuccess\Creatydev\Plans\Events\Stripe\DueSubscriptionChargeFailed

模型扩展

您也可以扩展计划模型

注意 $table$fillable$castRelationships将被继承

PlanModel

<?php
namespace App\Models;
use Creatydev\Plans\Models\PlanModel;
class Plan extends PlanModel {
    //
}

PlanFeatureModel

<?php
namespace App\Models;
use Creatydev\Plans\Models\PlanFeatureModel;
class PlanFeature extends PlanFeatureModel {
    //
}

PlanSubscriptionModel

<?php
namespace App\Models;
use Creatydev\Plans\Models\PlanSubscriptionModel;
class PlanSubscription extends PlanSubscriptionModel {
    //
}

PlanSubscriptionUsageModel

<?php
namespace App\Models;
use Creatydev\Plans\Models\PlanSubscriptionUsageModel;
class PlanSubscriptionUsage extends PlanSubscriptionUsageModel {
    //
}

StripteCustomerModel

<?php
namespace App\Models;
use Creatydev\Plans\Models\StripteCustomerModel;
class StripeCustomer extends StripteCustomerModel {
    //
}

事件

当使用订阅计划时,您想监听事件以自动运行可能会更改您的应用程序的代码。

事件易于使用。如果您不熟悉,可以查看Laravel官方文档中的事件

您只需在 EventServiceProvider.php 文件中实现以下事件。每个事件都将有自己的成员,您可以通过监听器中 handle() 方法内的 $event 变量来访问这些成员。

$listen = [
    ...
    \Creatydev\Plans\Events\CancelSubscription::class => [
        // $event->model = The model that cancelled the subscription.
        // $event->subscription = The subscription that was cancelled.
    ],
    \Creatydev\Plans\Events\NewSubscription::class => [
        // $event->model = The model that was subscribed.
        // $event->subscription = The subscription that was created.
    ],
     \Creatydev\Plans\Events\NewSubscriptionUntil::class => [
        // $event->model = The model that was subscribed.
        // $event->subscription = The subscription that was created.
    ],
    \Creatydev\Plans\Events\ExtendSubscription::class => [
        // $event->model = The model that extended the subscription.
        // $event->subscription = The subscription that was extended.
        // $event->startFromNow = If the subscription is exteded now or is created a new subscription, in the future.
        // $event->newSubscription = If the startFromNow is false, here will be sent the new subscription that starts after the current one ends.
    ],
    \Creatydev\Plans\Events\ExtendSubscriptionUntil::class => [
        // $event->model = The model that extended the subscription.
        // $event->subscription = The subscription that was extended.
        // $event->expiresOn = The Carbon instance of the date when the subscription will expire.
        // $event->startFromNow = If the subscription is exteded now or is created a new subscription, in the future.
        // $event->newSubscription = If the startFromNow is false, here will be sent the new subscription that starts after the current one ends.
    ],
    \Creatydev\Plans\Events\UpgradeSubscription::class => [
        // $event->model = The model that upgraded the subscription.
        // $event->subscription = The current subscription.
        // $event->startFromNow = If the subscription is upgraded now or is created a new subscription, in the future.
        // $event->oldPlan = Here lies the current (which is now old) plan.
        // $event->newPlan = Here lies the new plan. If it's the same plan, it will match with the $event->oldPlan
    ],
    \Creatydev\Plans\Events\UpgradeSubscriptionUntil::class => [
        // $event->model = The model that upgraded the subscription.
        // $event->subscription = The current subscription.
        // $event->expiresOn = The Carbon instance of the date when the subscription will expire.
        // $event->startFromNow = If the subscription is upgraded now or is created a new subscription, in the future.
        // $event->oldPlan = Here lies the current (which is now old) plan.
        // $event->newPlan = Here lies the new plan. If it's the same plan, it will match with the $event->oldPlan
    ],
    \Creatydev\Plans\Events\FeatureConsumed::class => [
        // $event->subscription = The current subscription.
        // $event->feature = The feature that was used.
        // $event->used = The amount used.
        // $event->remaining = The total amount remaining. If the feature is unlimited, will return -1
    ],
     \Creatydev\Plans\Events\FeatureUnconsumed::class => [
        // $event->subscription = The current subscription.
        // $event->feature = The feature that was used.
        // $event->used = The amount reverted.
        // $event->remaining = The total amount remaining. If the feature is unlimited, will return -1
    ],
    \Creatydev\Plans\Events\Stripe\ChargeFailed::class => [
        // $event->model = The model for which the payment failed.
        // $event->subscription = The subscription.
        // $event->exception = The exception thrown by the Stripe API wrapper.
    ],
    \Creatydev\Plans\Events\Stripe\ChargeSuccessful::class => [
        // $event->model = The model for which the payment succeded.
        // $event->subscription = The subscription which was updated as paid.
        // $event->stripeCharge = The response coming from the Stripe API wrapper.
    ],
    \Creatydev\Plans\Events\Stripe\DueSubscriptionChargeFailed::class => [
        // $event->model = The model for which the payment failed.
        // $event->subscription = The due subscription that cannot be paid.
        // $event->exception = The exception thrown by the Stripe API wrapper.
    ],
    \Creatydev\Plans\Events\Stripe\DueSubscriptionChargeSuccess::class => [
        // $event->model = The model for which the payment succeded.
        // $event->subscription = The due subscription that was paid.
        // $event->stripeCharge = The response coming from the Stripe API wrapper.
    ],
];

作者

  • Georgescu Alexandru - 初始工作
  • Dukens Thelemaque - Laravel 5.8 - 6.2 支持 - Creatydev

还可以查看参与此项目的贡献者列表

许可证

本项目采用 MIT 许可证 - 详细信息请参阅 LICENSE.md 文件。