Files
2026-10-02 10:21:54 +01:00

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;
}
}