171 lines
4.4 KiB
PHP
Executable File
171 lines
4.4 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Modules\Paddle\Services;
|
|
|
|
use Illuminate\Http\Client\PendingRequest;
|
|
use Illuminate\Http\Client\Response;
|
|
use Illuminate\Support\Facades\Http;
|
|
use RuntimeException;
|
|
|
|
class PaddleClient
|
|
{
|
|
public function customer(?string $customerID): ?array
|
|
{
|
|
if (! $customerID || ! config('paddle.api_key')) {
|
|
return null;
|
|
}
|
|
|
|
$response = $this->request()->get('/customers/'.$customerID);
|
|
|
|
if (! $response->successful()) {
|
|
return null;
|
|
}
|
|
|
|
$data = $response->json('data');
|
|
|
|
return is_array($data) ? $data : null;
|
|
}
|
|
|
|
public function subscription(string $subscriptionID): array
|
|
{
|
|
$this->requireWritableConfiguration();
|
|
|
|
$response = $this->request()
|
|
->get('/subscriptions/'.$subscriptionID);
|
|
|
|
return $this->subscriptionData(
|
|
$response,
|
|
'Paddle could not retrieve the subscription.'
|
|
);
|
|
}
|
|
|
|
public function updateSubscriptionPrice(
|
|
string $subscriptionID,
|
|
string $priceID
|
|
): array {
|
|
$this->requireWritableConfiguration();
|
|
|
|
/*
|
|
* Paddle restricts subscription item changes while a subscription
|
|
* is trialing. Existing item quantities may only be updated using
|
|
* do_not_bill during the trial.
|
|
*
|
|
* Once the subscription is outside its trial we can use normal
|
|
* immediate proration.
|
|
*/
|
|
$subscription = $this->subscription($subscriptionID);
|
|
|
|
$status = (string) ($subscription['status'] ?? '');
|
|
|
|
$prorationBillingMode = $status === 'trialing'
|
|
? 'do_not_bill'
|
|
: 'prorated_immediately';
|
|
|
|
/*
|
|
* Preserve the existing subscription item and replace its price.
|
|
*
|
|
* Paddle treats the submitted items array as the desired
|
|
* subscription items, so quantity is retained where possible.
|
|
*/
|
|
$existingItems = is_array($subscription['items'] ?? null)
|
|
? $subscription['items']
|
|
: [];
|
|
|
|
$existingItem = $existingItems[0] ?? [];
|
|
|
|
$quantity = max(
|
|
1,
|
|
(int) ($existingItem['quantity'] ?? 1)
|
|
);
|
|
|
|
$response = $this->request()->patch(
|
|
'/subscriptions/'.$subscriptionID,
|
|
[
|
|
'proration_billing_mode' => $prorationBillingMode,
|
|
'items' => [
|
|
[
|
|
'price_id' => $priceID,
|
|
'quantity' => $quantity,
|
|
],
|
|
],
|
|
]
|
|
);
|
|
|
|
return $this->subscriptionData(
|
|
$response,
|
|
'Paddle could not update the subscription.'
|
|
);
|
|
}
|
|
|
|
public function cancelSubscriptionImmediately(
|
|
string $subscriptionID
|
|
): array {
|
|
$this->requireWritableConfiguration();
|
|
|
|
$response = $this->request()->post(
|
|
'/subscriptions/'.$subscriptionID.'/cancel',
|
|
[
|
|
'effective_from' => 'immediately',
|
|
]
|
|
);
|
|
|
|
return $this->subscriptionData(
|
|
$response,
|
|
'Paddle could not cancel the subscription.'
|
|
);
|
|
}
|
|
|
|
private function request(): PendingRequest
|
|
{
|
|
return Http::baseUrl(
|
|
(string) config('paddle.api_url')
|
|
)
|
|
->withToken(
|
|
(string) config('paddle.api_key')
|
|
)
|
|
->acceptJson()
|
|
->timeout(15);
|
|
}
|
|
|
|
private function requireWritableConfiguration(): void
|
|
{
|
|
if (
|
|
trim(
|
|
(string) config('paddle.api_key')
|
|
) === ''
|
|
) {
|
|
throw new RuntimeException(
|
|
'Paddle subscription management is not configured.'
|
|
);
|
|
}
|
|
}
|
|
|
|
private function subscriptionData(
|
|
Response $response,
|
|
string $fallback
|
|
): array {
|
|
if (! $response->successful()) {
|
|
$detail = trim(
|
|
(string) (
|
|
$response->json('error.detail')
|
|
?? ''
|
|
)
|
|
);
|
|
|
|
throw new RuntimeException(
|
|
$detail !== ''
|
|
? $detail
|
|
: $fallback
|
|
);
|
|
}
|
|
|
|
$data = $response->json('data');
|
|
|
|
if (! is_array($data)) {
|
|
throw new RuntimeException($fallback);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
}
|