tipo: modificación del cpanel y gitignore de la carpeta vendor
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
use Laravel\Paddle\Concerns\ManagesCustomer;
|
||||
use Laravel\Paddle\Concerns\ManagesSubscriptions;
|
||||
use Laravel\Paddle\Concerns\ManagesTransactions;
|
||||
use Laravel\Paddle\Concerns\PerformsCharges;
|
||||
|
||||
trait Billable
|
||||
{
|
||||
use ManagesCustomer;
|
||||
use ManagesSubscriptions;
|
||||
use ManagesTransactions;
|
||||
use PerformsCharges;
|
||||
}
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Laravel\Paddle\Exceptions\PaddleException;
|
||||
use Money\Currencies\ISOCurrencies;
|
||||
use Money\Currency;
|
||||
use Money\Formatter\IntlMoneyFormatter;
|
||||
use Money\Money;
|
||||
use NumberFormatter;
|
||||
|
||||
class Cashier
|
||||
{
|
||||
const VERSION = '2.6.3';
|
||||
|
||||
/**
|
||||
* The custom currency formatter.
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
protected static $formatCurrencyUsing;
|
||||
|
||||
/**
|
||||
* Indicates if Cashier routes will be registered.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $registersRoutes = true;
|
||||
|
||||
/**
|
||||
* Indicates if Cashier will mark past due subscriptions as invalid.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $deactivatePastDue = true;
|
||||
|
||||
/**
|
||||
* The customer model class name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $customerModel = Customer::class;
|
||||
|
||||
/**
|
||||
* The subscription model class name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $subscriptionModel = Subscription::class;
|
||||
|
||||
/**
|
||||
* The subscription item model class name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $subscriptionItemModel = SubscriptionItem::class;
|
||||
|
||||
/**
|
||||
* The transaction model class name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $transactionModel = Transaction::class;
|
||||
|
||||
/**
|
||||
* Preview prices for a given set of items.
|
||||
*
|
||||
* @param array|string $items
|
||||
* @param array $options
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public static function previewPrices($items, array $options = [])
|
||||
{
|
||||
$items = static::api('POST', 'pricing-preview', array_merge([
|
||||
'items' => static::normalizeItems($items),
|
||||
], $options))['data']['details']['line_items'];
|
||||
|
||||
return collect($items)->map(function (array $item) {
|
||||
return new PricePreview($item);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer instance by its Paddle customer ID.
|
||||
*
|
||||
* @param string $customerId
|
||||
* @return \Laravel\Paddle\Billable|null
|
||||
*/
|
||||
public static function findBillable($customerId)
|
||||
{
|
||||
return (new static::$customerModel)->where('paddle_id', $customerId)->first()?->billable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Paddle webhook url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function webhookUrl()
|
||||
{
|
||||
return config('cashier.webhook') ?? route('cashier.webhook');
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a Paddle API call.
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $uri
|
||||
* @param array|null $payload
|
||||
* @return \Illuminate\Http\Client\Response
|
||||
*
|
||||
* @throws \Laravel\Paddle\Exceptions\PaddleException
|
||||
*/
|
||||
public static function api($method, $uri, ?array $payload = null)
|
||||
{
|
||||
if (empty($apiKey = config('cashier.api_key', config('cashier.auth_code')))) {
|
||||
throw new Exception('Paddle API key not set.');
|
||||
}
|
||||
|
||||
$host = static::apiUrl();
|
||||
|
||||
/** @var \Illuminate\Http\Client\Response $response */
|
||||
$response = Http::withToken($apiKey)
|
||||
->withUserAgent('Laravel\Paddle/'.static::VERSION)
|
||||
->withHeaders(['Paddle-Version' => 1])
|
||||
->$method("{$host}/{$uri}", $payload);
|
||||
|
||||
if (isset($response['error'])) {
|
||||
$message = "Paddle API error '{$response['error']['detail']}' occurred";
|
||||
|
||||
if (isset($response['error']['errors'])) {
|
||||
$message .= ' with validation errors ('.json_encode($response['error']['errors']).')';
|
||||
}
|
||||
|
||||
throw (new PaddleException($message))->setError($response['error']);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Paddle API url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function apiUrl()
|
||||
{
|
||||
return 'https://'.(config('cashier.sandbox') ? 'sandbox-' : '').'api.paddle.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the given items to a Paddle accepted format.
|
||||
*
|
||||
* @param array|string $items
|
||||
* @param string $priceKey
|
||||
* @return array
|
||||
*/
|
||||
public static function normalizeItems($items, string $priceKey = 'price_id'): array
|
||||
{
|
||||
return collect($items)->map(function ($item, $key) use ($priceKey) {
|
||||
if (is_array($item)) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
if (is_string($key)) {
|
||||
return [
|
||||
$priceKey => $key,
|
||||
'quantity' => $item,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
$priceKey => $item,
|
||||
'quantity' => 1,
|
||||
];
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom currency formatter.
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function formatCurrencyUsing(callable $callback)
|
||||
{
|
||||
static::$formatCurrencyUsing = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given amount into a displayable currency.
|
||||
*
|
||||
* @param int $amount
|
||||
* @param string $currency
|
||||
* @param string|null $locale
|
||||
* @param array $options
|
||||
* @return string
|
||||
*/
|
||||
public static function formatAmount($amount, $currency, $locale = null, array $options = [])
|
||||
{
|
||||
if (static::$formatCurrencyUsing) {
|
||||
return call_user_func(static::$formatCurrencyUsing, $amount, $currency, $locale, $options);
|
||||
}
|
||||
|
||||
$money = new Money($amount, new Currency(strtoupper($currency)));
|
||||
|
||||
$locale = $locale ?? config('cashier.currency_locale');
|
||||
|
||||
$numberFormatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
|
||||
|
||||
if (isset($options['min_fraction_digits'])) {
|
||||
$numberFormatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $options['min_fraction_digits']);
|
||||
}
|
||||
|
||||
$moneyFormatter = new IntlMoneyFormatter($numberFormatter, new ISOCurrencies());
|
||||
|
||||
return $moneyFormatter->format($money);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given currency uses cents.
|
||||
*
|
||||
* @param \Money\Currency $currency
|
||||
* @return bool
|
||||
*/
|
||||
public static function currencyUsesCents(Currency $currency)
|
||||
{
|
||||
return ! in_array($currency->getCode(), ['JPY', 'KRW'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Cashier to not register its routes.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function ignoreRoutes()
|
||||
{
|
||||
static::$registersRoutes = false;
|
||||
|
||||
return new static;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Cashier to maintain past due subscriptions as active.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function keepPastDueSubscriptionsActive()
|
||||
{
|
||||
static::$deactivatePastDue = false;
|
||||
|
||||
return new static;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the customer model class name.
|
||||
*
|
||||
* @param string $customerModel
|
||||
* @return void
|
||||
*/
|
||||
public static function useCustomerModel($customerModel)
|
||||
{
|
||||
static::$customerModel = $customerModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the subscription model class name.
|
||||
*
|
||||
* @param string $subscriptionModel
|
||||
* @return void
|
||||
*/
|
||||
public static function useSubscriptionModel($subscriptionModel)
|
||||
{
|
||||
static::$subscriptionModel = $subscriptionModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the subscription item model class name.
|
||||
*
|
||||
* @param string $subscriptionItemModel
|
||||
* @return void
|
||||
*/
|
||||
public static function useSubscriptionItemModel($subscriptionItemModel)
|
||||
{
|
||||
static::$subscriptionItemModel = $subscriptionItemModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the transaction model class name.
|
||||
*
|
||||
* @param string $transactionModel
|
||||
* @return void
|
||||
*/
|
||||
public static function useTransactionModel($transactionModel)
|
||||
{
|
||||
static::$transactionModel = $transactionModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fake Cashier instance.
|
||||
*
|
||||
* @return \Laravel\Paddle\CashierFake
|
||||
*/
|
||||
public static function fake(...$arguments)
|
||||
{
|
||||
return CashierFake::fake(...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass-thru to the CashierFake method of the same name.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertCustomerUpdated($callback = null)
|
||||
{
|
||||
CashierFake::assertCustomerUpdated($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass-thru to the CashierFake method of the same name.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertTransactionCompleted($callback = null)
|
||||
{
|
||||
CashierFake::assertTransactionCompleted($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass-thru to the CashierFake method of the same name.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertTransactionUpdated($callback = null)
|
||||
{
|
||||
CashierFake::assertTransactionUpdated($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass-thru to the CashierFake method of the same name.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertSubscriptionCreated($callback = null)
|
||||
{
|
||||
CashierFake::assertSubscriptionCreated($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass-thru to the CashierFake method of the same name.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertSubscriptionNotCreated($callback = null)
|
||||
{
|
||||
CashierFake::assertSubscriptionNotCreated($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass-thru to the CashierFake method of the same name.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertSubscriptionUpdated($callback = null)
|
||||
{
|
||||
CashierFake::assertSubscriptionUpdated($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass-thru to the CashierFake method of the same name.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertSubscriptionCanceled($callback = null)
|
||||
{
|
||||
CashierFake::assertSubscriptionCanceled($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass-thru to the CashierFake method of the same name.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertSubscriptionPaused($callback = null)
|
||||
{
|
||||
CashierFake::assertSubscriptionPaused($callback);
|
||||
}
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Paddle\Events\CustomerUpdated;
|
||||
use Laravel\Paddle\Events\SubscriptionCanceled;
|
||||
use Laravel\Paddle\Events\SubscriptionCreated;
|
||||
use Laravel\Paddle\Events\SubscriptionPaused;
|
||||
use Laravel\Paddle\Events\SubscriptionUpdated;
|
||||
use Laravel\Paddle\Events\TransactionCompleted;
|
||||
use Laravel\Paddle\Events\TransactionUpdated;
|
||||
|
||||
class CashierFake
|
||||
{
|
||||
/**
|
||||
* The array of callbacks for each response.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $responses = [];
|
||||
|
||||
/**
|
||||
* Initialize the fake instance and fake Cashier's events and API calls.
|
||||
*
|
||||
* @param array $endpoints
|
||||
* @param string|array $events
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(array $endpoints = [], $events = [])
|
||||
{
|
||||
foreach ($endpoints as $endpoint => $response) {
|
||||
if (! Arr::isAssoc($endpoints)) {
|
||||
$endpoint = $response;
|
||||
$response = null;
|
||||
}
|
||||
|
||||
$this->fakeHttpResponse($endpoint, Arr::wrap($response));
|
||||
}
|
||||
|
||||
Event::fake(array_merge([
|
||||
CustomerUpdated::class,
|
||||
TransactionCompleted::class,
|
||||
TransactionUpdated::class,
|
||||
SubscriptionCreated::class,
|
||||
SubscriptionUpdated::class,
|
||||
SubscriptionCanceled::class,
|
||||
SubscriptionPaused::class,
|
||||
], Arr::wrap($events)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntactic sugar for the constructor.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function fake(...$arguments)
|
||||
{
|
||||
return new static(...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the successful response for a given endpoint.
|
||||
*
|
||||
* @param string $endpoint
|
||||
* @param array $data
|
||||
* @return self
|
||||
*/
|
||||
public function response(string $endpoint, array $data)
|
||||
{
|
||||
$this->fakeHttpResponse($endpoint, [
|
||||
'data' => $data,
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an error response for a given endpoint.
|
||||
*
|
||||
* @param string $endpoint
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @return self
|
||||
*
|
||||
* @see https://developer.paddle.com/api-reference/ZG9jOjI1MzUzOTkw-api-error-codes
|
||||
*/
|
||||
public function error(string $endpoint, $message = '', $code = 0)
|
||||
{
|
||||
$this->fakeHttpResponse($endpoint, [
|
||||
'error' => ['detail' => $message],
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake the given endpoint with the provided response.
|
||||
*
|
||||
* @param string $endpoint
|
||||
* @param mixed $response
|
||||
* @return void
|
||||
*/
|
||||
protected function fakeHttpResponse(string $endpoint, $response)
|
||||
{
|
||||
$notFaked = ! Arr::exists($this->responses, $endpoint);
|
||||
|
||||
$this->responses[$endpoint] = $response;
|
||||
|
||||
if ($notFaked) {
|
||||
Http::fake([static::getFormattedApiUrl($endpoint) => function () use ($endpoint) {
|
||||
return $this->responses[$endpoint];
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given path into a full API url.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
public static function getFormattedApiUrl(string $path): string
|
||||
{
|
||||
return Cashier::apiUrl().Str::start($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert if the CustomerUpdated event was dispatched based on a truth-test callback.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertCustomerUpdated($callback = null)
|
||||
{
|
||||
Event::assertDispatched(CustomerUpdated::class, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert if the TransactionCompleted event was dispatched based on a truth-test callback.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertTransactionCompleted($callback = null)
|
||||
{
|
||||
Event::assertDispatched(TransactionCompleted::class, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert if the TransactionUpdated event was dispatched based on a truth-test callback.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertTransactionUpdated($callback = null)
|
||||
{
|
||||
Event::assertDispatched(TransactionUpdated::class, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert if the SubscriptionCreated event was dispatched based on a truth-test callback.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertSubscriptionCreated($callback = null)
|
||||
{
|
||||
Event::assertDispatched(SubscriptionCreated::class, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert if the SubscriptionCreated event was not dispatched based on a truth-test callback.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertSubscriptionNotCreated($callback = null)
|
||||
{
|
||||
Event::assertNotDispatched(SubscriptionCreated::class, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert if the SubscriptionUpdated event was dispatched based on a truth-test callback.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertSubscriptionUpdated($callback = null)
|
||||
{
|
||||
Event::assertDispatched(SubscriptionUpdated::class, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert if the SubscriptionCanceled event was dispatched based on a truth-test callback.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertSubscriptionCanceled($callback = null)
|
||||
{
|
||||
Event::assertDispatched(SubscriptionCanceled::class, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert if the SubscriptionPaused event was dispatched based on a truth-test callback.
|
||||
*
|
||||
* @param callable|int|null $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function assertSubscriptionPaused($callback = null)
|
||||
{
|
||||
Event::assertDispatched(SubscriptionPaused::class, $callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Paddle\Components\Button;
|
||||
use Laravel\Paddle\Components\Checkout;
|
||||
|
||||
class CashierServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->mergeConfigFrom(
|
||||
__DIR__.'/../config/cashier.php', 'cashier'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any package services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$this->bootRoutes();
|
||||
$this->bootResources();
|
||||
$this->bootPublishing();
|
||||
$this->bootDirectives();
|
||||
$this->bootComponents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the package routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function bootRoutes()
|
||||
{
|
||||
if (Cashier::$registersRoutes) {
|
||||
Route::group([
|
||||
'prefix' => config('cashier.path'),
|
||||
'namespace' => 'Laravel\Paddle\Http\Controllers',
|
||||
'as' => 'cashier.',
|
||||
], function () {
|
||||
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the package resources.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function bootResources()
|
||||
{
|
||||
$this->loadViewsFrom(__DIR__.'/../resources/views', 'cashier');
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the package's publishable resources.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function bootPublishing()
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->publishes([
|
||||
__DIR__.'/../config/cashier.php' => $this->app->configPath('cashier.php'),
|
||||
], 'cashier-config');
|
||||
|
||||
$this->publishes([
|
||||
__DIR__.'/../database/migrations' => $this->app->databasePath('migrations'),
|
||||
], 'cashier-migrations');
|
||||
|
||||
$this->publishes([
|
||||
__DIR__.'/../resources/views' => $this->app->resourcePath('views/vendor/cashier'),
|
||||
], 'cashier-views');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the package directives.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function bootDirectives()
|
||||
{
|
||||
Blade::directive('paddleJS', function ($expression) {
|
||||
$expression = $expression ?: '[]';
|
||||
|
||||
return '<?php echo view("cashier::js", ["nonce" => '.$expression.'["nonce"] ?? ""]); ?>';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the package components.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function bootComponents()
|
||||
{
|
||||
Blade::component(Button::class, 'paddle-button');
|
||||
Blade::component(Checkout::class, 'paddle-checkout');
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JsonSerializable;
|
||||
use LogicException;
|
||||
|
||||
class Checkout implements Arrayable, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* The custom data for the checkout.
|
||||
*/
|
||||
protected array $custom = [];
|
||||
|
||||
/**
|
||||
* The URL which the customer will be returned to after starting the subscription.
|
||||
*/
|
||||
protected ?string $returnTo = null;
|
||||
|
||||
/**
|
||||
* Create a new checkout instance.
|
||||
*/
|
||||
public function __construct(
|
||||
protected ?Customer $customer,
|
||||
protected array $items = [],
|
||||
protected array $transaction = []
|
||||
) {
|
||||
$this->items = Cashier::normalizeItems($items, 'priceId');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new checkout instance for a guest.
|
||||
*/
|
||||
public static function guest(array $items = []): self
|
||||
{
|
||||
return new static(null, $items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new checkout instance for an existing customer.
|
||||
*/
|
||||
public static function customer(Customer $customer, array $items = []): self
|
||||
{
|
||||
return new static($customer, $items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new transaction on Paddle and return a new checkout instance.
|
||||
*/
|
||||
public static function transaction(array $transaction, ?Customer $customer = null): self
|
||||
{
|
||||
return new static($customer, [], $transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom data to the checkout.
|
||||
*/
|
||||
public function customData(array $custom): self
|
||||
{
|
||||
// Make sure subscription_type doesn't gets unset.
|
||||
if (isset($this->custom['subscription_type']) && isset($custom['subscription_type'])) {
|
||||
throw new LogicException('The subscription_type can not be overwritten.');
|
||||
}
|
||||
|
||||
$this->custom = $custom;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the checkout to an array compatible with `Paddle.Checkout.open`.
|
||||
*/
|
||||
public function options(): array
|
||||
{
|
||||
$options = [
|
||||
'settings' => array_filter([
|
||||
'displayMode' => 'inline',
|
||||
'frameStyle' => 'width: 100%; background-color: transparent; border: none;',
|
||||
'successUrl' => $this->returnTo,
|
||||
'allowLogout' => false,
|
||||
]),
|
||||
'items' => $this->items,
|
||||
];
|
||||
|
||||
if ($customer = $this->customer) {
|
||||
$options['customer'] = ['id' => $customer->paddle_id];
|
||||
}
|
||||
|
||||
if ($custom = $this->custom) {
|
||||
$options['customData'] = $custom;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL the customer should be returned to after a successful checkout.
|
||||
*/
|
||||
public function returnTo(string $returnTo): self
|
||||
{
|
||||
$this->returnTo = $returnTo;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer for the checkout.
|
||||
*/
|
||||
public function getCustomer(): ?Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the items for the checkout.
|
||||
*/
|
||||
public function getItems(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Paddle transaction data.
|
||||
*/
|
||||
public function getTransaction(): array
|
||||
{
|
||||
return $this->transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the custom data for the checkout.
|
||||
*/
|
||||
public function getCustomData(): array
|
||||
{
|
||||
return $this->custom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL the customer should be returned to after a successful checkout.
|
||||
*/
|
||||
public function getReturnUrl(): ?string
|
||||
{
|
||||
return $this->returnTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the checkout's JSON serializable attributes.
|
||||
*/
|
||||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return $this->options();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the checkout to its array representation.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return $this->options();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
use Laravel\Paddle\Checkout as PaddleCheckout;
|
||||
|
||||
class Button extends Component
|
||||
{
|
||||
/**
|
||||
* Initialise the Button component class.
|
||||
*/
|
||||
public function __construct(public PaddleCheckout $checkout)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / view contents that represent the component.
|
||||
*
|
||||
* @return \Illuminate\View\View|string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('cashier::components.button');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
use Laravel\Paddle\Checkout as PaddleCheckout;
|
||||
|
||||
class Checkout extends Component
|
||||
{
|
||||
/**
|
||||
* Initialise the Checkout component class.
|
||||
*/
|
||||
public function __construct(
|
||||
protected PaddleCheckout $checkout,
|
||||
public string $id = 'paddle-checkout-container',
|
||||
protected int $height = 366,
|
||||
protected array $settings = []
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view / view contents that represent the component.
|
||||
*
|
||||
* @return \Illuminate\View\View|string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return view('cashier::components.checkout');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the options for the inline Paddle Checkout script.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function options()
|
||||
{
|
||||
$options = $this->checkout->options();
|
||||
|
||||
$options['settings']['frameTarget'] = $this->id;
|
||||
$options['settings']['frameInitialHeight'] = $this->height;
|
||||
|
||||
$options['settings'] = array_filter(
|
||||
array_merge($options['settings'], $this->settings),
|
||||
fn ($option) => ! is_null($option)
|
||||
);
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Concerns;
|
||||
|
||||
use Laravel\Paddle\Cashier;
|
||||
use LogicException;
|
||||
|
||||
trait ManagesCustomer
|
||||
{
|
||||
/**
|
||||
* Create a Paddle customer for the given model.
|
||||
*
|
||||
* @return \Laravel\Paddle\Customer
|
||||
*/
|
||||
public function createAsCustomer(array $options = [])
|
||||
{
|
||||
if ($customer = $this->customer) {
|
||||
return $customer;
|
||||
}
|
||||
|
||||
if (! array_key_exists('name', $options) && $name = $this->paddleName()) {
|
||||
$options['name'] = $name;
|
||||
}
|
||||
|
||||
if (! array_key_exists('email', $options) && $email = $this->paddleEmail()) {
|
||||
$options['email'] = $email;
|
||||
}
|
||||
|
||||
if (! isset($options['email'])) {
|
||||
throw new LogicException('Unable to create Paddle customer without an email.');
|
||||
}
|
||||
|
||||
$trialEndsAt = $options['trial_ends_at'] ?? null;
|
||||
|
||||
unset($options['trial_ends_at']);
|
||||
|
||||
// Attempt to find the customer by email address first...
|
||||
$response = Cashier::api('GET', 'customers', [
|
||||
'status' => 'active,archived',
|
||||
'email' => $options['email'],
|
||||
])['data'][0] ?? null;
|
||||
|
||||
// If we can't find the customer by email, we'll create them on Paddle...
|
||||
if (is_null($response)) {
|
||||
$response = Cashier::api('POST', 'customers', $options)['data'];
|
||||
}
|
||||
|
||||
if (Cashier::$customerModel::where('paddle_id', $response['id'])->exists()) {
|
||||
throw new LogicException("The Paddle customer [{$response['id']}] already exists in the database.");
|
||||
}
|
||||
|
||||
$customer = $this->customer()->make();
|
||||
$customer->paddle_id = $response['id'];
|
||||
$customer->name = $response['name'] ?? '';
|
||||
$customer->email = $response['email'];
|
||||
$customer->trial_ends_at = $trialEndsAt;
|
||||
$customer->save();
|
||||
|
||||
$this->refresh();
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer related to the billable model.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
|
||||
*/
|
||||
public function customer()
|
||||
{
|
||||
return $this->morphOne(Cashier::$customerModel, 'billable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price previews for a set of price ids for this billable model.
|
||||
*
|
||||
* @param array|string $items
|
||||
* @param array $options
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function previewPrices($items, array $options = [])
|
||||
{
|
||||
if ($customer = $this->customer) {
|
||||
$options['customer_id'] = $customer->paddle_id;
|
||||
}
|
||||
|
||||
return Cashier::previewPrices($items, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the billable model's name to associate with Paddle.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function paddleName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the billable model's email address to associate with Paddle.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function paddleEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Concerns;
|
||||
|
||||
use Laravel\Paddle\Cashier;
|
||||
use Laravel\Paddle\Subscription;
|
||||
|
||||
trait ManagesSubscriptions
|
||||
{
|
||||
/**
|
||||
* Get all of the subscriptions for the Billable model.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function subscriptions()
|
||||
{
|
||||
return $this->morphMany(Cashier::$subscriptionModel, 'billable')->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a subscription instance by type.
|
||||
*
|
||||
* @param string $type
|
||||
* @return \Laravel\Paddle\Subscription|null
|
||||
*/
|
||||
public function subscription($type = 'default')
|
||||
{
|
||||
return $this->subscriptions->where('type', $type)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the Billable model is on trial.
|
||||
*
|
||||
* @param string $type
|
||||
* @param int|null $price
|
||||
* @return bool
|
||||
*/
|
||||
public function onTrial($type = 'default', $price = null)
|
||||
{
|
||||
if (func_num_args() === 0 && $this->onGenericTrial()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$subscription = $this->subscription($type);
|
||||
|
||||
if (! $subscription || ! $subscription->onTrial()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $price ? $subscription->hasPrice($price) : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the Billable model's trial has ended.
|
||||
*
|
||||
* @param string $type
|
||||
* @param int|null $price
|
||||
* @return bool
|
||||
*/
|
||||
public function hasExpiredTrial($type = 'default', $price = null)
|
||||
{
|
||||
if (func_num_args() === 0 && $this->hasExpiredGenericTrial()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$subscription = $this->subscription($type);
|
||||
|
||||
if (! $subscription || ! $subscription->hasExpiredTrial()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $price ? $subscription->hasPrice($price) : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the Billable model is on a "generic" trial at the model level.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function onGenericTrial()
|
||||
{
|
||||
if (is_null($this->customer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->customer->onGenericTrial();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the Billable model's "generic" trial at the model level has expired.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasExpiredGenericTrial()
|
||||
{
|
||||
if (is_null($this->customer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->customer->hasExpiredGenericTrial();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ending date of the trial.
|
||||
*
|
||||
* @param string $type
|
||||
* @return \Illuminate\Support\Carbon|null
|
||||
*/
|
||||
public function trialEndsAt($type = 'default')
|
||||
{
|
||||
if (is_null($this->customer)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (func_num_args() === 0 && $this->onGenericTrial()) {
|
||||
return $this->customer->trial_ends_at;
|
||||
}
|
||||
|
||||
if ($subscription = $this->subscription($type)) {
|
||||
return $subscription->trial_ends_at;
|
||||
}
|
||||
|
||||
return $this->customer->trial_ends_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the customer has a given subscription.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string|null $price
|
||||
* @return bool
|
||||
*/
|
||||
public function subscribed($type = 'default', $price = null)
|
||||
{
|
||||
$subscription = $this->subscription($type);
|
||||
|
||||
if (! $subscription || ! $subscription->valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $price ? $subscription->hasPrice($price) : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the customer is actively subscribed to one of the given products.
|
||||
*
|
||||
* @param string|string[] $products
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
public function subscribedToProduct($products, $type = 'default')
|
||||
{
|
||||
$subscription = $this->subscription($type);
|
||||
|
||||
if (! $subscription || ! $subscription->valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ((array) $products as $product) {
|
||||
if ($subscription->hasProduct($product)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the customer is actively subscribed to one of the given prices.
|
||||
*
|
||||
* @param string|string[] $prices
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
public function subscribedToPrice($prices, $type = 'default')
|
||||
{
|
||||
$subscription = $this->subscription($type);
|
||||
|
||||
if (! $subscription || ! $subscription->valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ((array) $prices as $price) {
|
||||
if ($subscription->hasPrice($price)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the customer has a valid subscription on the given product.
|
||||
*
|
||||
* @param string $product
|
||||
* @return bool
|
||||
*/
|
||||
public function onProduct($product)
|
||||
{
|
||||
return ! is_null($this->subscriptions->first(function (Subscription $subscription) use ($product) {
|
||||
return $subscription->valid() && $subscription->hasProduct($product);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the customer has a valid subscription on the given price.
|
||||
*
|
||||
* @param string $price
|
||||
* @return bool
|
||||
*/
|
||||
public function onPrice($price)
|
||||
{
|
||||
return ! is_null($this->subscriptions->first(function (Subscription $subscription) use ($price) {
|
||||
return $subscription->valid() && $subscription->hasPrice($price);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Concerns;
|
||||
|
||||
use Laravel\Paddle\Cashier;
|
||||
|
||||
trait ManagesTransactions
|
||||
{
|
||||
/**
|
||||
* Get all of the transactions for the Billable model.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||
*/
|
||||
public function transactions()
|
||||
{
|
||||
return $this->morphMany(Cashier::$transactionModel, 'billable')->orderByDesc('billed_at');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Concerns;
|
||||
|
||||
use Laravel\Paddle\Cashier;
|
||||
use Laravel\Paddle\Checkout;
|
||||
use Laravel\Paddle\Subscription;
|
||||
use Laravel\Paddle\SubscriptionBuilder;
|
||||
|
||||
trait PerformsCharges
|
||||
{
|
||||
/**
|
||||
* Get a checkout instance for a given list of prices.
|
||||
*
|
||||
* @param string|array $prices
|
||||
* @param int $quantity
|
||||
* @return \Laravel\Paddle\Checkout
|
||||
*/
|
||||
public function checkout($prices, int $quantity = 1)
|
||||
{
|
||||
$customer = $this->createAsCustomer();
|
||||
|
||||
return Checkout::customer($customer, is_array($prices) ? $prices : [$prices => $quantity]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe the customer to a new plan variant.
|
||||
*
|
||||
* @param string|array $prices
|
||||
* @param string $type
|
||||
* @return \Laravel\Paddle\Checkout
|
||||
*/
|
||||
public function subscribe($prices, string $type = Subscription::DEFAULT_TYPE)
|
||||
{
|
||||
return $this->checkout($prices)->customData(['subscription_type' => $type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe the customer to a new product.
|
||||
*
|
||||
* @param int $amount
|
||||
* @param string $name
|
||||
* @param string $type
|
||||
* @return \Laravel\Paddle\SubscriptionBuilder
|
||||
*/
|
||||
public function newSubscription(int $amount, string $name, string $type = Subscription::DEFAULT_TYPE)
|
||||
{
|
||||
return new SubscriptionBuilder($this, $amount, $name, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transaction for a "one off" charge for the given amount and returns a checkout instance.
|
||||
*
|
||||
* @param int $amount
|
||||
* @param string $name
|
||||
* @param array $options
|
||||
* @return \Laravel\Paddle\Checkout
|
||||
*/
|
||||
public function charge(int $amount, string $name, array $options = [])
|
||||
{
|
||||
return $this->chargeMany([array_replace_recursive([
|
||||
'price' => [
|
||||
'description' => "$name Custom Price",
|
||||
'unit_price' => [
|
||||
'amount' => (string) $amount,
|
||||
'currency_code' => config('cashier.currency'),
|
||||
],
|
||||
'product' => [
|
||||
'name' => $name,
|
||||
'tax_category' => 'standard',
|
||||
],
|
||||
],
|
||||
'quantity' => 1,
|
||||
], $options)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transaction for a "one off" charge for the given items and returns a checkout instance.
|
||||
*
|
||||
* @param array $items
|
||||
* @return \Laravel\Paddle\Checkout
|
||||
*/
|
||||
public function chargeMany(array $items)
|
||||
{
|
||||
$customer = $this->createAsCustomer();
|
||||
|
||||
$transaction = Cashier::api('POST', 'transactions', ['items' => $items])->json()['data'];
|
||||
|
||||
return Checkout::transaction($transaction, $customer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Concerns;
|
||||
|
||||
/**
|
||||
* @link https://developer.paddle.com/concepts/subscriptions/proration
|
||||
*/
|
||||
trait Prorates
|
||||
{
|
||||
/**
|
||||
* The current proration behavior.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $prorationBehavior = 'prorated_next_billing_period';
|
||||
|
||||
/**
|
||||
* Indicate that the buyer is billed the prorated amount on their next renewal.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function prorate()
|
||||
{
|
||||
$this->prorationBehavior = 'prorated_next_billing_period';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the buyer is billed for the full amount on their next renewal.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function noProrate()
|
||||
{
|
||||
$this->prorationBehavior = 'full_next_billing_period';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the buyer is billed the prorated amount now.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function prorateImmediately()
|
||||
{
|
||||
$this->prorationBehavior = 'prorated_immediately';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the buyer is billed the full amount now.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function immediatelyWithoutProrate()
|
||||
{
|
||||
$this->prorationBehavior = 'full_immediately';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the buyer is not billed for the prorated amount or the full amount.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function doNotBill()
|
||||
{
|
||||
$this->prorationBehavior = 'do_not_bill';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the proration behavior.
|
||||
*
|
||||
* @param string $prorationBehavior
|
||||
* @return $this
|
||||
*/
|
||||
public function setProrationBehavior($prorationBehavior)
|
||||
{
|
||||
$this->prorationBehavior = $prorationBehavior;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property \Laravel\Paddle\Billable $billable
|
||||
*/
|
||||
class Customer extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are not mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'trial_ends_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the billable model related to the customer.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function billable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the Paddle model is on a "generic" trial at the model level.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function onGenericTrial()
|
||||
{
|
||||
return $this->trial_ends_at && $this->trial_ends_at->isFuture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the Paddle model has an expired "generic" trial at the model level.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasExpiredGenericTrial()
|
||||
{
|
||||
return $this->trial_ends_at && $this->trial_ends_at->isPast();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Events;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Laravel\Paddle\Customer;
|
||||
|
||||
class CustomerUpdated
|
||||
{
|
||||
use Dispatchable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The billable entity.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public $billable;
|
||||
|
||||
/**
|
||||
* The customer instance.
|
||||
*
|
||||
* @var \Laravel\Paddle\Customer
|
||||
*/
|
||||
public $customer;
|
||||
|
||||
/**
|
||||
* The webhook payload.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $payload;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $billable
|
||||
* @param \Laravel\Paddle\Customer $customer
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Model $billable, Customer $customer, array $payload)
|
||||
{
|
||||
$this->billable = $billable;
|
||||
$this->customer = $customer;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Laravel\Paddle\Subscription;
|
||||
|
||||
class SubscriptionCanceled
|
||||
{
|
||||
use Dispatchable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The subscription instance.
|
||||
*
|
||||
* @var \Laravel\Paddle\Subscription
|
||||
*/
|
||||
public $subscription;
|
||||
|
||||
/**
|
||||
* The webhook payload.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $payload;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param \Laravel\Paddle\Subscription $subscription
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Subscription $subscription, array $payload)
|
||||
{
|
||||
$this->subscription = $subscription;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Events;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Laravel\Paddle\Subscription;
|
||||
|
||||
class SubscriptionCreated
|
||||
{
|
||||
use Dispatchable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The billable entity.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public $billable;
|
||||
|
||||
/**
|
||||
* The subscription instance.
|
||||
*
|
||||
* @var \Laravel\Paddle\Subscription
|
||||
*/
|
||||
public $subscription;
|
||||
|
||||
/**
|
||||
* The payload array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $payload;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $billable
|
||||
* @param \Laravel\Paddle\Subscription $subscription
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Model $billable, Subscription $subscription, array $payload)
|
||||
{
|
||||
$this->billable = $billable;
|
||||
$this->subscription = $subscription;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Laravel\Paddle\Subscription;
|
||||
|
||||
class SubscriptionPaused
|
||||
{
|
||||
use Dispatchable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The subscription instance.
|
||||
*
|
||||
* @var \Laravel\Paddle\Subscription
|
||||
*/
|
||||
public $subscription;
|
||||
|
||||
/**
|
||||
* The webhook payload.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $payload;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param \Laravel\Paddle\Subscription $subscription
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Subscription $subscription, array $payload)
|
||||
{
|
||||
$this->subscription = $subscription;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Laravel\Paddle\Subscription;
|
||||
|
||||
class SubscriptionUpdated
|
||||
{
|
||||
use Dispatchable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The subscription instance.
|
||||
*
|
||||
* @var \Laravel\Paddle\Subscription
|
||||
*/
|
||||
public $subscription;
|
||||
|
||||
/**
|
||||
* The webhook payload.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $payload;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param \Laravel\Paddle\Subscription $subscription
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Subscription $subscription, array $payload)
|
||||
{
|
||||
$this->subscription = $subscription;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Events;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Laravel\Paddle\Transaction;
|
||||
|
||||
class TransactionCompleted
|
||||
{
|
||||
use Dispatchable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The billable entity.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public $billable;
|
||||
|
||||
/**
|
||||
* The transaction instance.
|
||||
*
|
||||
* @var \Laravel\Paddle\Transaction
|
||||
*/
|
||||
public $transaction;
|
||||
|
||||
/**
|
||||
* The webhook payload.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $payload;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $billable
|
||||
* @param \Laravel\Paddle\Transaction $transaction
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Model $billable, Transaction $transaction, array $payload)
|
||||
{
|
||||
$this->billable = $billable;
|
||||
$this->transaction = $transaction;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Events;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Laravel\Paddle\Transaction;
|
||||
|
||||
class TransactionUpdated
|
||||
{
|
||||
use Dispatchable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The billable entity.
|
||||
*
|
||||
* @var \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public $billable;
|
||||
|
||||
/**
|
||||
* The transaction instance.
|
||||
*
|
||||
* @var \Laravel\Paddle\Transaction
|
||||
*/
|
||||
public $transaction;
|
||||
|
||||
/**
|
||||
* The webhook payload.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $payload;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Model $billable
|
||||
* @param \Laravel\Paddle\Transaction $transaction
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Model $billable, Transaction $transaction, array $payload)
|
||||
{
|
||||
$this->billable = $billable;
|
||||
$this->transaction = $transaction;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class WebhookHandled
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $payload;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(array $payload)
|
||||
{
|
||||
$this->payload = $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Events;
|
||||
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class WebhookReceived
|
||||
{
|
||||
use Dispatchable;
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $payload;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(array $payload)
|
||||
{
|
||||
$this->payload = $payload;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PaddleException extends Exception
|
||||
{
|
||||
/**
|
||||
* The error response from Paddle.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $error = [];
|
||||
|
||||
/**
|
||||
* Get the error response from Paddle.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getError(): array
|
||||
{
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error response from Paddle.
|
||||
*
|
||||
* @param array $error
|
||||
* @return self
|
||||
*/
|
||||
public function setError(array $error): self
|
||||
{
|
||||
$this->error = $error;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Http\Controllers;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Paddle\Cashier;
|
||||
use Laravel\Paddle\Events\CustomerUpdated;
|
||||
use Laravel\Paddle\Events\SubscriptionCanceled;
|
||||
use Laravel\Paddle\Events\SubscriptionCreated;
|
||||
use Laravel\Paddle\Events\SubscriptionPaused;
|
||||
use Laravel\Paddle\Events\SubscriptionUpdated;
|
||||
use Laravel\Paddle\Events\TransactionCompleted;
|
||||
use Laravel\Paddle\Events\TransactionUpdated;
|
||||
use Laravel\Paddle\Events\WebhookHandled;
|
||||
use Laravel\Paddle\Events\WebhookReceived;
|
||||
use Laravel\Paddle\Http\Middleware\VerifyWebhookSignature;
|
||||
use Laravel\Paddle\Subscription;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class WebhookController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new WebhookController instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
if (config('cashier.webhook_secret')) {
|
||||
$this->middleware(VerifyWebhookSignature::class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a Paddle webhook call.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$payload = $request->all();
|
||||
|
||||
$method = 'handle'.Str::studly(Str::replace('.', ' ', $payload['event_type']));
|
||||
|
||||
WebhookReceived::dispatch($payload);
|
||||
|
||||
if (method_exists($this, $method)) {
|
||||
$this->{$method}($payload);
|
||||
|
||||
WebhookHandled::dispatch($payload);
|
||||
|
||||
return new Response('Webhook Handled');
|
||||
}
|
||||
|
||||
return new Response();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle customer updated.
|
||||
*
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
protected function handleCustomerUpdated(array $payload)
|
||||
{
|
||||
$data = $payload['data'];
|
||||
|
||||
if (! $customer = $this->findCustomer($data['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$customer->update([
|
||||
'name' => $data['name'] ?? '',
|
||||
'email' => $data['email'],
|
||||
]);
|
||||
|
||||
CustomerUpdated::dispatch($customer->billable, $customer, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle transaction completed.
|
||||
*
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
protected function handleTransactionCompleted(array $payload)
|
||||
{
|
||||
$data = $payload['data'];
|
||||
|
||||
if ($this->transactionExists($data['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $billable = $this->findBillable($data['customer_id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transaction = $billable->transactions()->create([
|
||||
'paddle_id' => $data['id'],
|
||||
'paddle_subscription_id' => $data['subscription_id'],
|
||||
'invoice_number' => $data['invoice_number'],
|
||||
'status' => $data['status'],
|
||||
'total' => $data['details']['totals']['total'],
|
||||
'tax' => $data['details']['totals']['tax'],
|
||||
'currency' => $data['currency_code'],
|
||||
'billed_at' => Carbon::parse($data['billed_at'], 'UTC'),
|
||||
]);
|
||||
|
||||
TransactionCompleted::dispatch($billable, $transaction, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle transaction updated.
|
||||
*
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
protected function handleTransactionUpdated(array $payload)
|
||||
{
|
||||
$data = $payload['data'];
|
||||
|
||||
if (! $transaction = $this->findTransaction($data['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transaction->update([
|
||||
'invoice_number' => $data['invoice_number'],
|
||||
'status' => $data['status'],
|
||||
'total' => $data['details']['totals']['total'],
|
||||
'tax' => $data['details']['totals']['tax'],
|
||||
'billed_at' => Carbon::parse($data['billed_at'], 'UTC'),
|
||||
]);
|
||||
|
||||
TransactionUpdated::dispatch($transaction->billable, $transaction, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle subscription created.
|
||||
*
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
protected function handleSubscriptionCreated(array $payload)
|
||||
{
|
||||
$data = $payload['data'];
|
||||
|
||||
if ($this->subscriptionExists($data['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $billable = $this->findBillable($data['customer_id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscription = $billable->subscriptions()->create([
|
||||
'type' => $data['custom_data']['subscription_type'] ?? Subscription::DEFAULT_TYPE,
|
||||
'paddle_id' => $data['id'],
|
||||
'status' => $data['status'],
|
||||
'trial_ends_at' => $data['status'] === Subscription::STATUS_TRIALING
|
||||
? Carbon::parse($data['next_billed_at'], 'UTC')
|
||||
: null,
|
||||
]);
|
||||
|
||||
foreach ($data['items'] as $item) {
|
||||
$subscription->items()->create([
|
||||
'product_id' => $item['price']['product_id'],
|
||||
'price_id' => $item['price']['id'],
|
||||
'status' => $item['status'],
|
||||
'quantity' => $item['quantity'] ?? 1,
|
||||
]);
|
||||
}
|
||||
|
||||
$billable->customer->update(['trial_ends_at' => null]);
|
||||
|
||||
SubscriptionCreated::dispatch($billable, $subscription, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle subscription updated.
|
||||
*
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
protected function handleSubscriptionUpdated(array $payload)
|
||||
{
|
||||
$data = $payload['data'];
|
||||
|
||||
if (! $subscription = $this->findSubscription($data['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscription->status = $data['status'];
|
||||
|
||||
if ($data['status'] === Subscription::STATUS_TRIALING) {
|
||||
$subscription->trial_ends_at = Carbon::parse($data['next_billed_at'], 'UTC');
|
||||
} else {
|
||||
$subscription->trial_ends_at = null;
|
||||
}
|
||||
|
||||
if (isset($data['paused_at'])) {
|
||||
$subscription->paused_at = Carbon::parse($data['paused_at'], 'UTC');
|
||||
} elseif (isset($data['scheduled_change']) && $data['scheduled_change']['action'] === 'pause') {
|
||||
$subscription->paused_at = Carbon::parse($data['scheduled_change']['effective_at'], 'UTC');
|
||||
} else {
|
||||
$subscription->paused_at = null;
|
||||
}
|
||||
|
||||
if (isset($data['canceled_at'])) {
|
||||
$subscription->ends_at = Carbon::parse($data['canceled_at'], 'UTC');
|
||||
} elseif (isset($data['scheduled_change']) && $data['scheduled_change']['action'] === 'cancel') {
|
||||
$subscription->ends_at = Carbon::parse($data['scheduled_change']['effective_at'], 'UTC');
|
||||
} else {
|
||||
$subscription->ends_at = null;
|
||||
}
|
||||
|
||||
$subscription->save();
|
||||
|
||||
$prices = [];
|
||||
|
||||
foreach ($data['items'] as $item) {
|
||||
$prices[] = $item['price']['id'];
|
||||
|
||||
$subscription->items()->updateOrCreate([
|
||||
'price_id' => $item['price']['id'],
|
||||
], [
|
||||
'product_id' => $item['price']['product_id'],
|
||||
'status' => $item['status'],
|
||||
'quantity' => $item['quantity'] ?? 1,
|
||||
]);
|
||||
}
|
||||
|
||||
// Delete items that aren't attached to the subscription anymore...
|
||||
$subscription->items()->whereNotIn('price_id', $prices)->delete();
|
||||
|
||||
SubscriptionUpdated::dispatch($subscription, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle subscription paused.
|
||||
*
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
protected function handleSubscriptionPaused(array $payload)
|
||||
{
|
||||
$data = $payload['data'];
|
||||
|
||||
if (! $subscription = $this->findSubscription($data['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscription->status = $data['status'];
|
||||
|
||||
$subscription->paused_at = Carbon::parse($data['paused_at'], 'UTC');
|
||||
|
||||
$subscription->ends_at = null;
|
||||
|
||||
$subscription->save();
|
||||
|
||||
SubscriptionPaused::dispatch($subscription, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle subscription canceled.
|
||||
*
|
||||
* @param array $payload
|
||||
* @return void
|
||||
*/
|
||||
protected function handleSubscriptionCanceled(array $payload)
|
||||
{
|
||||
$data = $payload['data'];
|
||||
|
||||
if (! $subscription = $this->findSubscription($data['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscription->status = $data['status'];
|
||||
|
||||
$subscription->ends_at = Carbon::parse($data['canceled_at'], 'UTC');
|
||||
|
||||
$subscription->paused_at = null;
|
||||
|
||||
$subscription->save();
|
||||
|
||||
SubscriptionCanceled::dispatch($subscription, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the customer instance by its Paddle customer ID.
|
||||
*
|
||||
* @param string $customerId
|
||||
* @return \Laravel\Paddle\Billable|null
|
||||
*/
|
||||
protected function findBillable($customerId)
|
||||
{
|
||||
return Cashier::findBillable($customerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first customer matching a Paddle customer ID.
|
||||
*
|
||||
* @param string $customerId
|
||||
* @return \Laravel\Paddle\Customer|null
|
||||
*/
|
||||
protected function findCustomer(string $customerId)
|
||||
{
|
||||
return Cashier::$customerModel::firstWhere('paddle_id', $customerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first subscription matching a Paddle subscription ID.
|
||||
*
|
||||
* @param string $subscriptionId
|
||||
* @return \Laravel\Paddle\Subscription|null
|
||||
*/
|
||||
protected function findSubscription(string $subscriptionId)
|
||||
{
|
||||
return Cashier::$subscriptionModel::firstWhere('paddle_id', $subscriptionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a subscription with a given Paddle ID already exists.
|
||||
*
|
||||
* @param string $subscriptionId
|
||||
* @return bool
|
||||
*/
|
||||
protected function subscriptionExists(string $subscriptionId)
|
||||
{
|
||||
return Cashier::$subscriptionModel::where('paddle_id', $subscriptionId)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first transaction matching a Paddle transaction ID.
|
||||
*
|
||||
* @param string $transactionId
|
||||
* @return \Laravel\Paddle\Transaction|null
|
||||
*/
|
||||
protected function findTransaction(string $transactionId)
|
||||
{
|
||||
return Cashier::$transactionModel::firstWhere('paddle_id', $transactionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a transaction with a given ID already exists.
|
||||
*
|
||||
* @param string $transactionId
|
||||
* @return bool
|
||||
*/
|
||||
protected function transactionExists(string $transactionId)
|
||||
{
|
||||
return Cashier::$transactionModel::where('paddle_id', $transactionId)->count() > 0;
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
/**
|
||||
* @see https://developer.paddle.com/webhook-reference/verifying-webhooks
|
||||
*/
|
||||
class VerifyWebhookSignature
|
||||
{
|
||||
public const SIGNATURE_HEADER = 'Paddle-Signature';
|
||||
public const HASH_ALGORITHM_1 = 'h1';
|
||||
|
||||
protected ?int $maximumVariance = 5;
|
||||
|
||||
/**
|
||||
* Handle the incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return \Illuminate\Http\Response
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$signature = $request->header(self::SIGNATURE_HEADER);
|
||||
|
||||
if ($this->isInvalidSignature($request, $signature)) {
|
||||
throw new AccessDeniedHttpException('Invalid webhook signature.');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate signature.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param string $signature
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
//the signature is not $signature[0] it's $signature
|
||||
//the true it's false and false it's true when if ($this->isInvalidSignature($request, $signature)) { throw new AccessDeniedHttpException('Invalid webhook signature.'); }
|
||||
protected function isInvalidSignature(Request $request, $signature)
|
||||
{
|
||||
if (empty($signature)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
[$timestamp, $hashes] = $this->parseSignature($signature);
|
||||
|
||||
if ($this->maximumVariance > 0 && time() > $timestamp + $this->maximumVariance) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$secret = config('cashier.webhook_secret');
|
||||
$data = $request->getContent();
|
||||
|
||||
foreach ($hashes as $hashAlgorithm => $possibleHashes) {
|
||||
$hash = match ($hashAlgorithm) {
|
||||
'h1' => hash_hmac('sha256', "{$timestamp}:{$data}", $secret),
|
||||
};
|
||||
|
||||
foreach ($possibleHashes as $possibleHash) {
|
||||
if (hash_equals($hash, $possibleHash)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the signature header.
|
||||
*
|
||||
* @param string $header
|
||||
* @return array
|
||||
*/
|
||||
public function parseSignature(string $header): array
|
||||
{
|
||||
$components = [
|
||||
'ts' => 0,
|
||||
'hashes' => [],
|
||||
];
|
||||
|
||||
foreach (explode(';', $header) as $part) {
|
||||
if (str_contains($part, '=')) {
|
||||
[$key, $value] = explode('=', $part, 2);
|
||||
|
||||
match ($key) {
|
||||
'ts' => $components['ts'] = (int) $value,
|
||||
'h1' => $components['hashes']['h1'][] = $value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
$components['ts'],
|
||||
$components['hashes'],
|
||||
];
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Contracts\Support\Jsonable;
|
||||
use JsonSerializable;
|
||||
use Money\Currency;
|
||||
|
||||
class Payment implements Arrayable, Jsonable, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* The amount of the payment.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $amount;
|
||||
|
||||
/**
|
||||
* The currency of the payment.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $currency;
|
||||
|
||||
/**
|
||||
* The payment date.
|
||||
*
|
||||
* @var \Carbon\Carbon
|
||||
*/
|
||||
public $date;
|
||||
|
||||
/**
|
||||
* Create a new Payment instance.
|
||||
*
|
||||
* @param string $amount
|
||||
* @param string $currency
|
||||
* @param \Carbon\Carbon $date
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($amount, $currency, $date)
|
||||
{
|
||||
$this->amount = $amount;
|
||||
$this->currency = $currency;
|
||||
$this->date = $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total amount of the payment.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function amount()
|
||||
{
|
||||
return Cashier::formatAmount($this->amount, $this->currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw total of the payment.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rawAmount()
|
||||
{
|
||||
return $this->amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currency used for the payment.
|
||||
*
|
||||
* @return \Money\Currency
|
||||
*/
|
||||
public function currency(): Currency
|
||||
{
|
||||
return new Currency($this->currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the date of the payment as a Carbon instance.
|
||||
*
|
||||
* @return \Carbon\Carbon
|
||||
*/
|
||||
public function date()
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance as an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return [
|
||||
'amount' => $this->amount(),
|
||||
'currency' => $this->currency,
|
||||
'date' => $this->date()->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the object to its JSON representation.
|
||||
*
|
||||
* @param int $options
|
||||
* @return string
|
||||
*/
|
||||
public function toJson($options = 0)
|
||||
{
|
||||
return json_encode($this->jsonSerialize(), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the object into something JSON serializable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
use Money\Currency;
|
||||
|
||||
class Price
|
||||
{
|
||||
/**
|
||||
* The price attributes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $price;
|
||||
|
||||
/**
|
||||
* Create a new Price instance.
|
||||
*
|
||||
* @param array $price
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(array $price)
|
||||
{
|
||||
$this->price = $price;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the amount.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function amount()
|
||||
{
|
||||
return Cashier::formatAmount($this->rawAmount(), $this->currency());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw amount.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rawAmount()
|
||||
{
|
||||
return $this->price['unit_price']['amount'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the interval for the price.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function interval()
|
||||
{
|
||||
return $this->price['billing_cycle']['interval'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frequency for the price.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function frequency()
|
||||
{
|
||||
return $this->price['billing_cycle']['frequency'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the used currency for the price.
|
||||
*
|
||||
* @return \Money\Currency
|
||||
*/
|
||||
public function currency(): Currency
|
||||
{
|
||||
return new Currency($this->price['unit_price']['currency_code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically get values from the Paddle price.
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
return $this->price[$key];
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Contracts\Support\Jsonable;
|
||||
use JsonSerializable;
|
||||
use Money\Currency;
|
||||
|
||||
class PricePreview implements Arrayable, Jsonable, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* The price preview attributes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $item;
|
||||
|
||||
/**
|
||||
* Create a new PricePreview instance.
|
||||
*
|
||||
* @param array $item
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(array $item)
|
||||
{
|
||||
$this->item = $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the price object for the preview.
|
||||
*
|
||||
* @return \Laravel\Paddle\Price
|
||||
*/
|
||||
public function price()
|
||||
{
|
||||
return new Price($this->item['price']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total amount.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function total()
|
||||
{
|
||||
return $this->item['formatted_totals']['total'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw total amount.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rawTotal()
|
||||
{
|
||||
return $this->item['totals']['total'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subtotal amount.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function subtotal()
|
||||
{
|
||||
return $this->item['formatted_totals']['subtotal'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw subtotal amount.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rawSubtotal()
|
||||
{
|
||||
return $this->item['totals']['subtotal'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tax amount.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function tax()
|
||||
{
|
||||
return $this->item['formatted_totals']['tax'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the price has tax.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTax()
|
||||
{
|
||||
return $this->rawTax() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw tax amount.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rawTax()
|
||||
{
|
||||
return $this->item['totals']['tax'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the used currency for the price preview.
|
||||
*
|
||||
* @return \Money\Currency
|
||||
*/
|
||||
public function currency(): Currency
|
||||
{
|
||||
return $this->price()->currency();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance as an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return $this->item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the object to its JSON representation.
|
||||
*
|
||||
* @param int $options
|
||||
* @return string
|
||||
*/
|
||||
public function toJson($options = 0)
|
||||
{
|
||||
return json_encode($this->jsonSerialize(), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the object into something JSON serializable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically get values from the price preview.
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
return $this->item[$key];
|
||||
}
|
||||
}
|
||||
+967
@@ -0,0 +1,967 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use DateTimeInterface;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use InvalidArgumentException;
|
||||
use Laravel\Paddle\Concerns\Prorates;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* @property \Laravel\Paddle\Billable $billable
|
||||
*/
|
||||
class Subscription extends Model
|
||||
{
|
||||
use Prorates;
|
||||
|
||||
const STATUS_ACTIVE = 'active';
|
||||
const STATUS_TRIALING = 'trialing';
|
||||
const STATUS_PAST_DUE = 'past_due';
|
||||
const STATUS_PAUSED = 'paused';
|
||||
const STATUS_CANCELED = 'canceled';
|
||||
|
||||
const INTERVAL_DAY = 'day';
|
||||
const INTERVAL_WEEK = 'week';
|
||||
const INTERVAL_MONTH = 'month';
|
||||
const INTERVAL_YEAR = 'year';
|
||||
|
||||
const DEFAULT_TYPE = 'default';
|
||||
|
||||
/**
|
||||
* The attributes that are not mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* The relations to eager load on every query.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $with = ['items'];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'trial_ends_at' => 'datetime',
|
||||
'paused_at' => 'datetime',
|
||||
'ends_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the billable model related to the subscription.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function billable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subscription items related to the subscription.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function items()
|
||||
{
|
||||
return $this->hasMany(Cashier::$subscriptionItemModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subscription item for the given price.
|
||||
*
|
||||
* @param string $price
|
||||
* @return \Laravel\Paddle\SubscriptionItem
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
|
||||
*/
|
||||
public function findItemOrFail($price)
|
||||
{
|
||||
return $this->items()->where('price_id', $price)->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a specific item by price or the single item on a subscription.
|
||||
*
|
||||
* @param string|null $price
|
||||
* @return \Laravel\Paddle\SubscriptionItem
|
||||
*
|
||||
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function singleItemOrFail($price = null)
|
||||
{
|
||||
if ($this->items()->count() > 1 && is_null($price)) {
|
||||
throw new InvalidArgumentException(
|
||||
'Please provide a price when retrieving an item of a subscription with multiple prices.'
|
||||
);
|
||||
}
|
||||
|
||||
return $price ? $this->findItemOrFail($price) : $this->items()->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the transactions for the Billable model.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function transactions()
|
||||
{
|
||||
return $this->hasMany(Cashier::$transactionModel, 'paddle_subscription_id', 'paddle_id')
|
||||
->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription has multiple prices.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasMultiplePrices()
|
||||
{
|
||||
return $this->items->count() > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription has a single price.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasSinglePrice()
|
||||
{
|
||||
return ! $this->hasMultiplePrices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription has a specific product.
|
||||
*
|
||||
* @param string $product
|
||||
* @return bool
|
||||
*/
|
||||
public function hasProduct($product)
|
||||
{
|
||||
return $this->items->contains(function (SubscriptionItem $item) use ($product) {
|
||||
return $item->product_id === $product;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription has a specific price.
|
||||
*
|
||||
* @param string $price
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPrice($price)
|
||||
{
|
||||
return $this->items->contains(function (SubscriptionItem $item) use ($price) {
|
||||
return $item->price_id === $price;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription is active, on trial, or within its grace period.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return $this->onTrial() || $this->active() || (! Cashier::$deactivatePastDue && $this->pastDue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by valid.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeValid($query)
|
||||
{
|
||||
$query->where('status', self::STATUS_TRIALING)
|
||||
->orWhere('status', self::STATUS_ACTIVE);
|
||||
|
||||
if (! Cashier::$deactivatePastDue) {
|
||||
$query->orWhere('status', self::STATUS_PAST_DUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription is within its trial period.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function onTrial()
|
||||
{
|
||||
return $this->status === self::STATUS_TRIALING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by on trial.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeOnTrial($query)
|
||||
{
|
||||
$query->where('status', self::STATUS_TRIALING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription's trial has expired.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasExpiredTrial()
|
||||
{
|
||||
return $this->trial_ends_at && $this->trial_ends_at->isPast();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by expired trial.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeExpiredTrial($query)
|
||||
{
|
||||
$query->whereNotNull('trial_ends_at')->where('trial_ends_at', '<', Carbon::now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by not on trial.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeNotOnTrial($query)
|
||||
{
|
||||
$query->where('status', '!=', self::STATUS_TRIALING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function active()
|
||||
{
|
||||
return $this->status === self::STATUS_ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by active.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
$query->where('status', '=', self::STATUS_ACTIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription is active and not on any grace period.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function recurring()
|
||||
{
|
||||
return $this->active() && ! $this->onPausedGracePeriod() && ! $this->onGracePeriod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by recurring.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeRecurring($query)
|
||||
{
|
||||
$query->active()->notOnPausedGracePeriod()->notOnGracePeriod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription is past due.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function pastDue()
|
||||
{
|
||||
return $this->status === self::STATUS_PAST_DUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by past due.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopePastDue($query)
|
||||
{
|
||||
$query->where('status', self::STATUS_PAST_DUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription is paused.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function paused()
|
||||
{
|
||||
return $this->status === self::STATUS_PAUSED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by paused.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopePaused($query)
|
||||
{
|
||||
$query->where('status', self::STATUS_PAUSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by not paused.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeNotPaused($query)
|
||||
{
|
||||
$query->where('status', '!=', self::STATUS_PAUSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription is within its grace period after being paused.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function onPausedGracePeriod()
|
||||
{
|
||||
return $this->paused_at && $this->paused_at->isFuture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by on trial grace period.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeOnPausedGracePeriod($query)
|
||||
{
|
||||
$query->whereNotNull('paused_at')->where('paused_at', '>', Carbon::now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by not on trial grace period.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeNotOnPausedGracePeriod($query)
|
||||
{
|
||||
$query->whereNull('paused_at')->orWhere('paused_at', '<=', Carbon::now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription is no longer active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canceled()
|
||||
{
|
||||
return $this->status === self::STATUS_CANCELED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by canceled.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeCanceled($query)
|
||||
{
|
||||
$query->where('status', self::STATUS_CANCELED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by not canceled.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeNotCanceled($query)
|
||||
{
|
||||
$query->where('status', '!=', self::STATUS_CANCELED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the subscription is within its grace period after cancellation.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function onGracePeriod()
|
||||
{
|
||||
return $this->ends_at && $this->ends_at->isFuture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by on grace period.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeOnGracePeriod($query)
|
||||
{
|
||||
$query->whereNotNull('ends_at')->where('ends_at', '>', Carbon::now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter query by not on grace period.
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function scopeNotOnGracePeriod($query)
|
||||
{
|
||||
$query->whereNull('ends_at')->orWhere('ends_at', '<=', Carbon::now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Bill for one-time charges on top of the subscription.
|
||||
*
|
||||
* @param string|array $items
|
||||
* @param bool $chargeNow
|
||||
* @return $this
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function charge($items, bool $chargeNow = false)
|
||||
{
|
||||
if (empty($items = (array) $items)) {
|
||||
throw new InvalidArgumentException('Please provide at least one item when charging one-time.');
|
||||
}
|
||||
|
||||
$response = Cashier::api('POST', "subscriptions/{$this->paddle_id}/charge", [
|
||||
'effective_from' => $chargeNow ? 'immediately' : 'next_billing_period',
|
||||
'items' => Cashier::normalizeItems($items),
|
||||
])['data'];
|
||||
|
||||
$this->forceFill([
|
||||
'status' => $response['status'],
|
||||
])->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bill for one-time charges on top of the subscription, and invoice immediately.
|
||||
*
|
||||
* @param string|array $items
|
||||
* @return $this
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function chargeAndInvoice($items)
|
||||
{
|
||||
$this->setProrateAndInvoice('chargeAndInvoice');
|
||||
|
||||
return $this->charge($items, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the quantity of a subscription item.
|
||||
*
|
||||
* @param int $count
|
||||
* @param string|null $price
|
||||
* @return $this
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function incrementQuantity($count = 1, $price = null)
|
||||
{
|
||||
$item = $this->singleItemOrFail($price);
|
||||
|
||||
return $this->updateQuantity($item->quantity + $count, $item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the quantity of the subscription, and invoice immediately.
|
||||
*
|
||||
* @param int $count
|
||||
* @param string|null $price
|
||||
* @return $this
|
||||
*/
|
||||
public function incrementAndInvoice($count = 1, $price = null)
|
||||
{
|
||||
$item = $this->singleItemOrFail($price);
|
||||
|
||||
$this->setProrateAndInvoice('incrementAndInvoice');
|
||||
|
||||
return $this->updateQuantity($item->quantity + $count, $item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the quantity of a subscription item.
|
||||
*
|
||||
* @param int $count
|
||||
* @param string|null $price
|
||||
* @return $this
|
||||
*/
|
||||
public function decrementQuantity($count = 1, $price = null)
|
||||
{
|
||||
$item = $this->singleItemOrFail($price);
|
||||
|
||||
return $this->updateQuantity(max(1, $item->quantity - $count), $item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the quantity of the subscription.
|
||||
*
|
||||
* @param int $quantity
|
||||
* @param \Laravel\Paddle\SubscriptionItem|string|null $price
|
||||
* @return $this
|
||||
*/
|
||||
public function updateQuantity($quantity, $price = null)
|
||||
{
|
||||
if ($quantity < 1) {
|
||||
throw new LogicException('Quantities of zero are not allowed.');
|
||||
}
|
||||
|
||||
$itemToUpdate = $price instanceof SubscriptionItem ? $price : $this->singleItemOrFail($price);
|
||||
|
||||
$items = $this->items()
|
||||
->get(['quantity', 'price_id'])
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'price_id' => $item['price_id'],
|
||||
'quantity' => $item['quantity'],
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
foreach ($items as $key => $item) {
|
||||
if ($item['price_id'] === $itemToUpdate->price_id) {
|
||||
$items[$key]['quantity'] = $quantity;
|
||||
}
|
||||
}
|
||||
|
||||
$response = $this->updatePaddleSubscription([
|
||||
'items' => $items,
|
||||
'proration_billing_mode' => $this->prorationBehavior,
|
||||
]);
|
||||
|
||||
$this->forceFill([
|
||||
'status' => $response['status'],
|
||||
])->save();
|
||||
|
||||
$itemToUpdate->forceFill([
|
||||
'quantity' => $quantity,
|
||||
])->save();
|
||||
|
||||
$this->load('items');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the trial period of the subscription.
|
||||
*
|
||||
* @param \DateTimeInterface|string $until
|
||||
* @return $this
|
||||
*/
|
||||
public function extendTrial($until)
|
||||
{
|
||||
$response = $this->updatePaddleSubscription([
|
||||
'next_billed_at' => Carbon::parse($until)->format(DateTimeInterface::RFC3339),
|
||||
'proration_billing_mode' => 'do_not_bill',
|
||||
]);
|
||||
|
||||
$this->forceFill([
|
||||
'status' => $response['status'],
|
||||
'trial_ends_at' => Carbon::parse($response['next_billed_at'], 'UTC'),
|
||||
])->save();
|
||||
|
||||
$this->syncSubscriptionItems($response['items']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the trial to end immediately and activate the subscription.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function activate()
|
||||
{
|
||||
$response = Cashier::api('POST', "subscriptions/{$this->paddle_id}/activate")['data'];
|
||||
|
||||
$this->forceFill([
|
||||
'status' => $response['status'],
|
||||
'trial_ends_at' => null,
|
||||
])->save();
|
||||
|
||||
$this->syncSubscriptionItems($response['items']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the subscription to new Paddle items.
|
||||
*
|
||||
* @param string|array $items
|
||||
* @param array $options
|
||||
* @return $this
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function swap($items, array $options = [])
|
||||
{
|
||||
if (empty($items = (array) $items)) {
|
||||
throw new InvalidArgumentException('Please provide at least one item when swapping.');
|
||||
}
|
||||
|
||||
$items = Cashier::normalizeItems($items);
|
||||
|
||||
$response = $this->updatePaddleSubscription(array_merge($options, [
|
||||
'items' => $items,
|
||||
'proration_billing_mode' => $this->prorationBehavior,
|
||||
]));
|
||||
|
||||
$this->forceFill([
|
||||
'status' => $response['status'],
|
||||
])->save();
|
||||
|
||||
$this->syncSubscriptionItems($response['items']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the subscription to a new Paddle plan, and invoice immediately.
|
||||
*
|
||||
* @param string|array $items
|
||||
* @param array $options
|
||||
* @return $this
|
||||
*/
|
||||
public function swapAndInvoice($items, array $options = [])
|
||||
{
|
||||
$this->setProrateAndInvoice('swapAndInvoice');
|
||||
|
||||
return $this->swap($items, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the billing cycle anchor.
|
||||
*
|
||||
* @param \DateTimeInterface|string|null $date
|
||||
* @return $this
|
||||
*/
|
||||
public function anchorBillingCycleOn($date)
|
||||
{
|
||||
$this->updatePaddleSubscription([
|
||||
'next_billed_at' => Carbon::parse($date)->format(DateTimeInterface::RFC3339),
|
||||
'proration_billing_mode' => $this->prorationBehavior,
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect the user to the Paddle payment method update URL.
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function redirectToUpdatePaymentMethod()
|
||||
{
|
||||
return redirect($this->paymentMethodUpdateUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Paddle payment method update URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function paymentMethodUpdateUrl()
|
||||
{
|
||||
return Cashier::api('GET', "subscriptions/{$this->paddle_id}")['data']['management_urls']['update_payment_method'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw transaction used to update the payment method on file.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function paymentMethodUpdateTransaction()
|
||||
{
|
||||
return Cashier::api('GET', "subscriptions/{$this->paddle_id}/update-payment-method-transaction")['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect the user to the Paddle cancel URL.
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function redirectToCancel()
|
||||
{
|
||||
return redirect($this->cancelUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Paddle cancel URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function cancelUrl()
|
||||
{
|
||||
return Cashier::api('GET', "subscriptions/{$this->paddle_id}")['data']['management_urls']['cancel'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the subscription.
|
||||
*
|
||||
* @param bool $pauseNow
|
||||
* @param \DateTimeInterface|string|null $until
|
||||
* @return $this
|
||||
*/
|
||||
public function pause(bool $pauseNow = false, $until = null)
|
||||
{
|
||||
$response = Cashier::api('POST', "subscriptions/{$this->paddle_id}/pause", [
|
||||
'effective_from' => $pauseNow ? 'immediately' : 'next_billing_period',
|
||||
'resume_at' => $until ? Carbon::parse($until)->format(DateTimeInterface::RFC3339) : null,
|
||||
])['data'];
|
||||
|
||||
$pausedAt = $pauseNow ? $response['paused_at'] : $response['scheduled_change']['effective_at'];
|
||||
|
||||
$this->forceFill([
|
||||
'status' => $response['status'],
|
||||
'paused_at' => Carbon::parse($pausedAt, 'UTC'),
|
||||
])->save();
|
||||
|
||||
$this->syncSubscriptionItems($response['items']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the subscription until a certain date.
|
||||
*
|
||||
* @param \DateTimeInterface|string $until
|
||||
* @return $this
|
||||
*/
|
||||
public function pauseUntil($until)
|
||||
{
|
||||
return $this->pause(false, $until);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the subscription immediately.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function pauseNow()
|
||||
{
|
||||
return $this->pause(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the subscription immediately and until a certain date.
|
||||
*
|
||||
* @param \DateTimeInterface|string $until
|
||||
* @return $this
|
||||
*/
|
||||
public function pauseNowUntil($until)
|
||||
{
|
||||
return $this->pause(true, $until);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume a paused subscription.
|
||||
*
|
||||
* @param \DateTimeInterface|string|null $resumeAt
|
||||
* @return $this
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function resume($resumeAt = null)
|
||||
{
|
||||
if ($this->paused()) {
|
||||
$response = Cashier::api('POST', "subscriptions/{$this->paddle_id}/resume", [
|
||||
'effective_from' => $resumeAt
|
||||
? Carbon::parse($resumeAt)->format(DateTimeInterface::RFC3339)
|
||||
: 'immediately',
|
||||
])['data'];
|
||||
} elseif ($this->onPausedGracePeriod()) {
|
||||
$response = Cashier::api('PATCH', "subscriptions/{$this->paddle_id}", [
|
||||
'scheduled_change' => null,
|
||||
])['data'];
|
||||
} else {
|
||||
throw new LogicException('Cannot resume a subscription that is not paused.');
|
||||
}
|
||||
|
||||
$this->forceFill([
|
||||
'status' => $response['status'],
|
||||
'paused_at' => $response['paused_at'] ? Carbon::parse($response['paused_at'], 'UTC') : null,
|
||||
])->save();
|
||||
|
||||
$this->syncSubscriptionItems($response['items']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the underlying Paddle subscription information for the model.
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function updatePaddleSubscription(array $options)
|
||||
{
|
||||
return Cashier::api('PATCH', "subscriptions/{$this->paddle_id}", $options)['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the subscription at the end of the current billing period.
|
||||
*
|
||||
* @param bool $cancelNow
|
||||
* @return $this
|
||||
*/
|
||||
public function cancel(bool $cancelNow = false)
|
||||
{
|
||||
$response = Cashier::api('POST', "subscriptions/{$this->paddle_id}/cancel", [
|
||||
'effective_from' => $cancelNow ? 'immediately' : 'next_billing_period',
|
||||
])['data'];
|
||||
|
||||
$endsAt = $cancelNow ? $response['canceled_at'] : $response['scheduled_change']['effective_at'];
|
||||
|
||||
$this->forceFill([
|
||||
'status' => $response['status'],
|
||||
'ends_at' => Carbon::parse($endsAt, 'UTC'),
|
||||
'trial_ends_at' => $cancelNow ? null : $this->trial_ends_at,
|
||||
])->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the subscription immediately.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function cancelNow()
|
||||
{
|
||||
return $this->cancel(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the subscription from being canceled at the end of the current billing period.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function stopCancelation()
|
||||
{
|
||||
$response = $this->updatePaddleSubscription(['scheduled_change' => null]);
|
||||
|
||||
$this->forceFill([
|
||||
'status' => $response['status'],
|
||||
'ends_at' => null,
|
||||
])->save();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last payment for the subscription.
|
||||
*
|
||||
* @return \Laravel\Paddle\Payment|null
|
||||
*/
|
||||
public function lastPayment()
|
||||
{
|
||||
if ($transaction = $this->transactions()->orderByDesc('billed_at')->first()) {
|
||||
return new Payment($transaction->total, $transaction->currency, $transaction->billed_at);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next payment for the subscription.
|
||||
*
|
||||
* @return \Laravel\Paddle\Payment|null
|
||||
*/
|
||||
public function nextPayment()
|
||||
{
|
||||
if ($transaction = $this->asPaddleSubscription('next_transaction')['next_transaction'] ?? null) {
|
||||
return new Payment(
|
||||
$transaction['details']['totals']['grand_total'],
|
||||
$transaction['details']['totals']['currency_code'],
|
||||
Carbon::parse($transaction['billing_period']['starts_at'], 'UTC'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subscription as a Paddle subscription response.
|
||||
*
|
||||
* @param string|null $include
|
||||
* @return array
|
||||
*/
|
||||
public function asPaddleSubscription(?string $include = null)
|
||||
{
|
||||
$include = $include ? ['include' => $include] : [];
|
||||
|
||||
return Cashier::api('GET', "subscriptions/{$this->paddle_id}", $include)['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically set the proration behavior when invoicing immediately.
|
||||
*
|
||||
* @param string $method
|
||||
* @return void
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
protected function setProrateAndInvoice($method): void
|
||||
{
|
||||
if ($this->prorationBehavior === 'do_not_bill') {
|
||||
throw new LogicException("You cannot combine {$method} and doNotBill.");
|
||||
}
|
||||
|
||||
if ($this->prorationBehavior === 'prorated_next_billing_period') {
|
||||
$this->prorateImmediately();
|
||||
} elseif ($this->prorationBehavior === 'full_next_billing_period') {
|
||||
$this->immediatelyWithoutProrate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the subscription items with the latest data from Paddle.
|
||||
*
|
||||
* @param array $items
|
||||
* @return void
|
||||
*/
|
||||
protected function syncSubscriptionItems(array $items)
|
||||
{
|
||||
$prices = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$prices[] = $item['price']['id'];
|
||||
|
||||
$this->items()->updateOrCreate([
|
||||
'price_id' => $item['price']['id'],
|
||||
], [
|
||||
'product_id' => $item['price']['product_id'],
|
||||
'status' => $item['status'],
|
||||
'quantity' => $item['quantity'] ?? 1,
|
||||
]);
|
||||
}
|
||||
|
||||
// Delete items that aren't attached to the subscription anymore...
|
||||
$this->items()->whereNotIn('price_id', $prices)->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
class SubscriptionBuilder
|
||||
{
|
||||
/**
|
||||
* The quantity of the subscription.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $quantity = 1;
|
||||
|
||||
/**
|
||||
* The interval of the subscription.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $interval = Subscription::INTERVAL_MONTH;
|
||||
|
||||
/**
|
||||
* Create a new subscription builder instance.
|
||||
*
|
||||
* @param \Laravel\Paddle\Billable $billable
|
||||
* @param int $amount
|
||||
* @param string $name
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected $billable,
|
||||
protected int $amount,
|
||||
protected string $name,
|
||||
protected string $type = Subscription::DEFAULT_TYPE
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the quantity of the subscription.
|
||||
*
|
||||
* @param int $quantity
|
||||
* @return $this
|
||||
*/
|
||||
public function quantity($quantity)
|
||||
{
|
||||
$this->quantity = $quantity;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a daily interval for the subscription.
|
||||
*/
|
||||
public function daily()
|
||||
{
|
||||
$this->interval = Subscription::INTERVAL_DAY;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a weekly interval for the subscription.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function weekly()
|
||||
{
|
||||
$this->interval = Subscription::INTERVAL_WEEK;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a monthly interval for the subscription.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function monthly()
|
||||
{
|
||||
$this->interval = Subscription::INTERVAL_MONTH;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a yearly interval for the subscription.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function yearly()
|
||||
{
|
||||
$this->interval = Subscription::INTERVAL_YEAR;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new checkout instance for the subscription.
|
||||
*
|
||||
* @param array $options
|
||||
* @return \Laravel\Paddle\Checkout
|
||||
*/
|
||||
public function checkout(array $options = [])
|
||||
{
|
||||
return $this->billable->charge(
|
||||
$this->amount,
|
||||
$this->name,
|
||||
array_replace_recursive([
|
||||
'price' => [
|
||||
'description' => $this->interval === Subscription::INTERVAL_DAY
|
||||
? "{$this->name} Daily"
|
||||
: $this->name.' '.ucfirst($this->interval).'ly',
|
||||
'billing_cycle' => [
|
||||
'interval' => $this->interval,
|
||||
'frequency' => $options['frequency'] ?? 1,
|
||||
],
|
||||
],
|
||||
'quantity' => $this->quantity,
|
||||
], $options)
|
||||
)->customData(['subscription_type' => $this->type]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property \Laravel\Paddle\Subscription|null $subscription
|
||||
*/
|
||||
class SubscriptionItem extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are not mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'quantity' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the subscription that the item belongs to.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function subscription()
|
||||
{
|
||||
$model = Cashier::$subscriptionModel;
|
||||
|
||||
return $this->belongsTo($model, (new $model)->getForeignKey());
|
||||
}
|
||||
}
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace Laravel\Paddle;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use LogicException;
|
||||
use Money\Currency;
|
||||
|
||||
/**
|
||||
* @property \Laravel\Paddle\Billable $billable
|
||||
* @property \Laravel\Paddle\Subscription|null $subscription
|
||||
*/
|
||||
class Transaction extends Model
|
||||
{
|
||||
const STATUS_DRAFT = 'draft';
|
||||
const STATUS_READY = 'ready';
|
||||
const STATUS_BILLED = 'billed';
|
||||
const STATUS_PAID = 'paid';
|
||||
const STATUS_COMPLETED = 'completed';
|
||||
const STATUS_CANCELED = 'canceled';
|
||||
const STATUS_PAST_DUE = 'past_due';
|
||||
|
||||
/**
|
||||
* The attributes that are not mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'billed_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the billable model related to the transaction.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function billable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subscription related to the transaction.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function subscription()
|
||||
{
|
||||
return $this->belongsTo(Cashier::$subscriptionModel, 'paddle_subscription_id', 'paddle_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total amount that was paid.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function total()
|
||||
{
|
||||
return Cashier::formatAmount($this->total, $this->currency());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total tax that was paid.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function tax()
|
||||
{
|
||||
return Cashier::formatAmount($this->tax, $this->currency());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the used currency for the transaction.
|
||||
*
|
||||
* @return \Money\Currency
|
||||
*/
|
||||
public function currency(): Currency
|
||||
{
|
||||
return new Currency($this->currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL to download the invoice.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function invoicePdf()
|
||||
{
|
||||
if (! $this->invoice_number) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Cashier::api('GET', "transactions/{$this->paddle_id}/invoice")['data']['url'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL to download the invoice.
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function redirectToInvoicePdf()
|
||||
{
|
||||
if ($url = $this->invoicePdf()) {
|
||||
return redirect($url);
|
||||
}
|
||||
|
||||
throw new LogicException('The transaction does not have an invoice PDF.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Refund the transaction for a given price and optional amount.
|
||||
*
|
||||
* @param string $reason
|
||||
* @param string|array $price
|
||||
* @return array
|
||||
*/
|
||||
public function refund($reason, $prices = [])
|
||||
{
|
||||
return $this->adjust('refund', $reason, $prices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit the transaction for a given price and optional amount.
|
||||
*
|
||||
* @param string $reason
|
||||
* @param string|array $price
|
||||
* @return array
|
||||
*/
|
||||
public function credit($reason, $prices = [])
|
||||
{
|
||||
return $this->adjust('credit', $reason, $prices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the transaction for a given price and optional amount.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $reason
|
||||
* @param string|array $price
|
||||
* @return array
|
||||
*/
|
||||
public function adjust($type, $reason, $prices = [])
|
||||
{
|
||||
if ($this->status !== 'billed' && $this->status !== 'completed') {
|
||||
throw new LogicException('Only "billed" or "completed" transactions can be adjusted.');
|
||||
}
|
||||
|
||||
$lineItems = $this->asPaddleTransaction()['details']['line_items'];
|
||||
|
||||
$prices = (array) $prices;
|
||||
|
||||
$items = collect($lineItems)
|
||||
->filter(function (array $lineItem) use ($prices) {
|
||||
// If no specific prices were given, we'll refund the entire transaction...
|
||||
if (empty($prices)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($lineItem['price_id'], $prices);
|
||||
})
|
||||
->map(function (array $lineItem) use ($prices) {
|
||||
// If a specific amount was given to refund for this price, we'll use that...
|
||||
$amount = isset($prices[$lineItem['price_id']])
|
||||
? $prices[$lineItem['price_id']]
|
||||
: null;
|
||||
|
||||
return array_filter([
|
||||
'item_id' => $lineItem['id'],
|
||||
'type' => $amount ? 'partial' : 'full',
|
||||
'amount' => $amount,
|
||||
]);
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if (empty($items)) {
|
||||
$prices = implode(', ', $prices);
|
||||
|
||||
throw new Exception(
|
||||
"Cannot find line items with price ID's `{$prices}` for transaction `{$this->paddle_id}`."
|
||||
);
|
||||
}
|
||||
|
||||
return Cashier::api('POST', 'adjustments', [
|
||||
'action' => $type,
|
||||
'transaction_id' => $this->paddle_id,
|
||||
'reason' => $reason,
|
||||
'items' => $items,
|
||||
])['data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the transaction as a Paddle transaction response.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function asPaddleTransaction()
|
||||
{
|
||||
return Cashier::api('GET', "transactions/{$this->paddle_id}")['data'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user