tipo: modificación del cpanel y gitignore de la carpeta vendor
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Taylor Otwell
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<p align="center"><img width="355" height="62" src="/art/logo.svg" alt="Logo Laravel Cashier Paddle"></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/cashier-paddle/actions"><img src="https://github.com/laravel/cashier-paddle/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/cashier-paddle"><img src="https://img.shields.io/packagist/dt/laravel/cashier-paddle" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/cashier-paddle"><img src="https://img.shields.io/packagist/v/laravel/cashier-paddle" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/cashier-paddle"><img src="https://img.shields.io/packagist/l/laravel/cashier-paddle" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## Introduction
|
||||
|
||||
Laravel Cashier Paddle provides an expressive, fluent interface to [Paddle's](https://paddle.com) subscription billing services. It handles almost all of the boilerplate subscription billing code you are dreading writing. In addition to basic subscription management, Cashier can handle swapping subscription, subscription "quantities", subscription pausing, cancellation grace periods and much more.
|
||||
|
||||
## Official Documentation
|
||||
|
||||
Documentation for Cashier Paddle can be found on the [Laravel website](https://laravel.com/docs/cashier-paddle).
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to Cashier Paddle! You can read the contribution guide [here](.github/CONTRIBUTING.md).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
Please review [our security policy](https://github.com/laravel/cashier-paddle/security/policy) on how to report security vulnerabilities.
|
||||
|
||||
## License
|
||||
|
||||
Laravel Cashier Paddle is open-sourced software licensed under the [MIT license](LICENSE.md).
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
# Release Instructions
|
||||
|
||||
1. Update the `VERSION` constant in [`Cashier.php`](./src/Cashier.php) and commit it
|
||||
2. [Create a new GitHub release](https://github.com/laravel/cashier-paddle/releases/new) for this version with the release notes
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "laravel/cashier-paddle",
|
||||
"description": "Cashier Paddle provides an expressive, fluent interface to Paddle's subscription billing services.",
|
||||
"keywords": ["laravel", "paddle", "billing"],
|
||||
"license": "MIT",
|
||||
"support": {
|
||||
"issues": "https://github.com/laravel/cashier-paddle/issues",
|
||||
"source": "https://github.com/laravel/cashier-paddle"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Taylor Otwell",
|
||||
"email": "taylor@laravel.com"
|
||||
},
|
||||
{
|
||||
"name": "Dries Vints",
|
||||
"email": "dries@laravel.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"guzzlehttp/guzzle": "^7.4.5",
|
||||
"illuminate/contracts": "^10.0|^11.0|^12.0",
|
||||
"illuminate/database": "^10.0|^11.0|^12.0",
|
||||
"illuminate/http": "^10.0|^11.0|^12.0",
|
||||
"illuminate/routing": "^10.0|^11.0|^12.0",
|
||||
"illuminate/support": "^10.0|^11.0|^12.0",
|
||||
"illuminate/view": "^10.0|^11.0|^12.0",
|
||||
"moneyphp/money": "^3.2|^4.0",
|
||||
"nesbot/carbon": "^2.67|^3.0",
|
||||
"spatie/url": "^1.3.5|^2.0",
|
||||
"symfony/http-kernel": "^6.2|^7.0",
|
||||
"symfony/polyfill-intl-icu": "^1.22.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"orchestra/testbench": "^8.36|^9.15|^10.8",
|
||||
"phpstan/phpstan": "^1.10"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "Allows for more locales besides the default \"en\" when formatting money values."
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Laravel\\Paddle\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/",
|
||||
"Workbench\\App\\": "workbench/app/",
|
||||
"Workbench\\Database\\Factories\\": "workbench/database/factories/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.x-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Laravel\\Paddle\\CashierServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": "@prepare",
|
||||
"prepare": "@php vendor/bin/testbench package:discover --ansi"
|
||||
},
|
||||
"config": {
|
||||
"audit": {
|
||||
"block-insecure": false
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Paddle Keys
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The Paddle seller ID and API key will allow your application to call
|
||||
| the Paddle API. The seller key is typically used when interacting
|
||||
| with Paddle.js, while the "API" key accesses private endpoints.
|
||||
|
|
||||
*/
|
||||
|
||||
'seller_id' => env('PADDLE_SELLER_ID'),
|
||||
|
||||
'client_side_token' => env('PADDLE_CLIENT_SIDE_TOKEN'),
|
||||
|
||||
'api_key' => env('PADDLE_AUTH_CODE') ?? env('PADDLE_API_KEY'),
|
||||
|
||||
'retain_key' => env('PADDLE_RETAIN_KEY'),
|
||||
|
||||
'webhook_secret' => env('PADDLE_WEBHOOK_SECRET'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cashier Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the base URI path where Cashier's views, such as the webhook
|
||||
| route, will be available. You're free to tweak this path based on
|
||||
| the needs of your particular application or design preferences.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('CASHIER_PATH', 'paddle'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cashier Webhook
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the base URI where webhooks from Paddle will be sent. The URL
|
||||
| built into Cashier Paddle is used by default; however, you can add
|
||||
| a custom URL when required for any application testing purposes.
|
||||
|
|
||||
*/
|
||||
|
||||
'webhook' => env('CASHIER_WEBHOOK'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Currency
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the default currency that will be used when generating charges
|
||||
| from your application. Of course, you are welcome to use any of the
|
||||
| various world currencies that are currently supported via Paddle.
|
||||
|
|
||||
*/
|
||||
|
||||
'currency' => env('CASHIER_CURRENCY', 'USD'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Currency Locale
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the default locale in which your money values are formatted in
|
||||
| for display. To utilize other locales besides the default en locale
|
||||
| verify you have the "intl" PHP extension installed on the system.
|
||||
|
|
||||
*/
|
||||
|
||||
'currency_locale' => env('CASHIER_CURRENCY_LOCALE', 'en'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Paddle Sandbox
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to toggle between the Paddle live environment
|
||||
| and its sandboxed environment.
|
||||
|
|
||||
*/
|
||||
|
||||
'sandbox' => env('PADDLE_SANDBOX', false),
|
||||
|
||||
];
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('customers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('billable');
|
||||
$table->string('paddle_id')->unique();
|
||||
$table->string('name');
|
||||
$table->string('email');
|
||||
$table->timestamp('trial_ends_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('customers');
|
||||
}
|
||||
};
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('subscriptions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('billable');
|
||||
$table->string('type');
|
||||
$table->string('paddle_id')->unique();
|
||||
$table->string('status');
|
||||
$table->timestamp('trial_ends_at')->nullable();
|
||||
$table->timestamp('paused_at')->nullable();
|
||||
$table->timestamp('ends_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('subscriptions');
|
||||
}
|
||||
};
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('subscription_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('subscription_id');
|
||||
$table->string('product_id');
|
||||
$table->string('price_id');
|
||||
$table->string('status');
|
||||
$table->integer('quantity');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['subscription_id', 'price_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('subscription_items');
|
||||
}
|
||||
};
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('transactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('billable');
|
||||
$table->string('paddle_id')->unique();
|
||||
$table->string('paddle_subscription_id')->nullable()->index();
|
||||
$table->string('invoice_number')->nullable();
|
||||
$table->string('status');
|
||||
$table->string('total');
|
||||
$table->string('tax');
|
||||
$table->string('currency', 3);
|
||||
$table->timestamp('billed_at');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('transactions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
$transaction = $checkout->getTransaction();
|
||||
$items = $checkout->getItems();
|
||||
$customer = $checkout->getCustomer();
|
||||
$custom = $checkout->getCustomData();
|
||||
?>
|
||||
|
||||
<a
|
||||
href='#!'
|
||||
@if ($transaction)
|
||||
data-transaction-id='{{ $transaction['id'] }}'
|
||||
@else
|
||||
data-items='{!! json_encode($items) !!}'
|
||||
@endif
|
||||
data-allow-logout='false'
|
||||
@if ($customer) data-customer-id='{{ $customer->paddle_id }}' @endif
|
||||
@if ($custom) data-custom-data='{{ json_encode($custom) }}' @endif
|
||||
@if ($returnUrl = $checkout->getReturnUrl()) data-success-url='{{ $returnUrl }}' @endif
|
||||
{{ $attributes->merge(['class' => 'paddle_button']) }}
|
||||
>
|
||||
{{ $slot }}
|
||||
</a>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div {{ $attributes->merge(['class' => $id]) }}></div>
|
||||
<script type="text/javascript">
|
||||
Paddle.Checkout.open(@json($options()));
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
$seller = array_filter([
|
||||
'pwAuth' => (int) config('cashier.retain_key'),
|
||||
]);
|
||||
|
||||
if (config('cashier.client_side_token')) {
|
||||
$seller['token'] = config('cashier.client_side_token');
|
||||
} elseif (config('cashier.seller_id')) {
|
||||
$seller['seller'] = (int) config('cashier.seller_id');
|
||||
}
|
||||
|
||||
if (isset($seller['pwAuth']) && Auth::check() && $customer = Auth::user()->customer) {
|
||||
$seller['pwCustomer'] = ['id' => $customer->paddle_id];
|
||||
}
|
||||
|
||||
$nonce = $nonce ?? '';
|
||||
?>
|
||||
|
||||
<script src="https://cdn.paddle.com/paddle/v2/paddle.js" @if ($nonce) nonce="{{ $nonce }}" @endif></script>
|
||||
|
||||
@if (config('cashier.sandbox'))
|
||||
<script type="text/javascript" @if ($nonce) nonce="{{ $nonce }}" @endif>
|
||||
Paddle.Environment.set('sandbox');
|
||||
</script>
|
||||
@endif
|
||||
|
||||
<script type="text/javascript" @if ($nonce) nonce="{{ $nonce }}" @endif>
|
||||
Paddle.Initialize(@json($seller));
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::post('webhook', 'WebhookController')->name('webhook');
|
||||
+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'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
providers:
|
||||
- Laravel\Paddle\CashierServiceProvider
|
||||
|
||||
migrations:
|
||||
- database/migrations
|
||||
|
||||
workbench:
|
||||
install: true
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Workbench\App\Models;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as Authenticable;
|
||||
use Laravel\Paddle\Billable;
|
||||
|
||||
class User extends Authenticable
|
||||
{
|
||||
use Billable;
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Workbench\Database\Factories;
|
||||
|
||||
use Workbench\App\Models\User;
|
||||
|
||||
/**
|
||||
* @template TModel of \Workbench\App\Models\User
|
||||
*
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<TModel>
|
||||
*/
|
||||
class UserFactory extends \Orchestra\Testbench\Factories\UserFactory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var class-string<TModel>
|
||||
*/
|
||||
protected $model = User::class;
|
||||
}
|
||||
Reference in New Issue
Block a user