Migrate billing from Paddle to Lemon Squeezy
This commit is contained in:
@@ -19,7 +19,7 @@ class EventAddonCatalog
|
||||
->mapWithKeys(function (PackageAddon $addon) {
|
||||
return [$addon->key => [
|
||||
'label' => $addon->label,
|
||||
'price_id' => $addon->price_id,
|
||||
'variant_id' => $addon->variant_id,
|
||||
'increments' => $addon->increments,
|
||||
]];
|
||||
})
|
||||
@@ -39,11 +39,11 @@ class EventAddonCatalog
|
||||
return $this->all()[$key] ?? null;
|
||||
}
|
||||
|
||||
public function resolvePriceId(string $key): ?string
|
||||
public function resolveVariantId(string $key): ?string
|
||||
{
|
||||
$addon = $this->find($key);
|
||||
|
||||
return $addon['price_id'] ?? null;
|
||||
return $addon['variant_id'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,20 +5,16 @@ namespace App\Services\Addons;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventPackageAddon;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Paddle\PaddleClient;
|
||||
use App\Services\Paddle\PaddleCustomerService;
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Services\LemonSqueezy\LemonSqueezyCheckoutService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
|
||||
class EventAddonCheckoutService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EventAddonCatalog $catalog,
|
||||
private readonly PaddleClient $paddle,
|
||||
private readonly PaddleCustomerService $customers,
|
||||
private readonly LemonSqueezyCheckoutService $checkout,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -32,25 +28,17 @@ class EventAddonCheckoutService
|
||||
$acceptedWaiver = (bool) ($payload['accepted_waiver'] ?? false);
|
||||
$acceptedTerms = (bool) ($payload['accepted_terms'] ?? false);
|
||||
|
||||
try {
|
||||
$customerId = $this->customers->ensureCustomerId($tenant);
|
||||
} catch (Throwable $exception) {
|
||||
throw ValidationException::withMessages([
|
||||
'customer' => __('Konnte Paddle-Kundenkonto nicht anlegen: :message', ['message' => $exception->getMessage()]),
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $addonKey || ! $this->catalog->find($addonKey)) {
|
||||
throw ValidationException::withMessages([
|
||||
'addon_key' => __('Unbekanntes Add-on.'),
|
||||
]);
|
||||
}
|
||||
|
||||
$priceId = $this->catalog->resolvePriceId($addonKey);
|
||||
$variantId = $this->catalog->resolveVariantId($addonKey);
|
||||
|
||||
if (! $priceId) {
|
||||
if (! $variantId) {
|
||||
throw ValidationException::withMessages([
|
||||
'addon_key' => __('Für dieses Add-on ist kein Paddle-Preis hinterlegt.'),
|
||||
'addon_key' => __('Für dieses Add-on ist kein Lemon Squeezy Variant hinterlegt.'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -73,6 +61,7 @@ class EventAddonCheckoutService
|
||||
'addon_key' => $addonKey,
|
||||
'addon_intent' => $addonIntent,
|
||||
'quantity' => $quantity,
|
||||
'lemonsqueezy_variant_id' => $variantId,
|
||||
'legal_version' => $this->resolveLegalVersion(),
|
||||
'accepted_terms' => $acceptedTerms ? '1' : '0',
|
||||
'accepted_waiver' => $acceptedWaiver ? '1' : '0',
|
||||
@@ -80,31 +69,18 @@ class EventAddonCheckoutService
|
||||
'cancel_url' => $payload['cancel_url'] ?? null,
|
||||
], static fn ($value) => $value !== null && $value !== '');
|
||||
|
||||
$requestPayload = array_filter([
|
||||
'customer_id' => $customerId,
|
||||
'items' => [
|
||||
[
|
||||
'price_id' => $priceId,
|
||||
'quantity' => $quantity,
|
||||
],
|
||||
],
|
||||
'custom_data' => $metadata,
|
||||
], static fn ($value) => $value !== null && $value !== '');
|
||||
$response = $this->checkout->createVariantCheckout($variantId, $metadata, [
|
||||
'success_url' => $payload['success_url'] ?? null,
|
||||
'return_url' => $payload['cancel_url'] ?? null,
|
||||
'customer_email' => $tenant->contact_email ?? $tenant->user?->email,
|
||||
]);
|
||||
|
||||
$response = $this->paddle->post('/transactions', $requestPayload);
|
||||
|
||||
$checkoutUrl = Arr::get($response, 'data.checkout.url')
|
||||
?? Arr::get($response, 'checkout.url')
|
||||
?? Arr::get($response, 'data.url')
|
||||
?? Arr::get($response, 'url');
|
||||
$checkoutId = Arr::get($response, 'data.checkout_id')
|
||||
?? Arr::get($response, 'data.checkout.id')
|
||||
?? Arr::get($response, 'checkout_id')
|
||||
?? Arr::get($response, 'checkout.id');
|
||||
$transactionId = Arr::get($response, 'data.id') ?? Arr::get($response, 'id');
|
||||
$checkoutUrl = $response['checkout_url'] ?? null;
|
||||
$checkoutId = $response['id'] ?? null;
|
||||
$transactionId = null;
|
||||
|
||||
if (! $checkoutUrl) {
|
||||
Log::warning('Paddle addon checkout response missing url', ['response' => $response]);
|
||||
Log::warning('Lemon Squeezy addon checkout response missing url', ['response' => $response]);
|
||||
}
|
||||
|
||||
EventPackageAddon::create([
|
||||
@@ -113,7 +89,7 @@ class EventAddonCheckoutService
|
||||
'tenant_id' => $tenant->id,
|
||||
'addon_key' => $addonKey,
|
||||
'quantity' => $quantity,
|
||||
'price_id' => $priceId,
|
||||
'variant_id' => $variantId,
|
||||
'checkout_id' => $checkoutId,
|
||||
'transaction_id' => $transactionId,
|
||||
'status' => 'pending',
|
||||
@@ -133,10 +109,8 @@ class EventAddonCheckoutService
|
||||
|
||||
return [
|
||||
'checkout_url' => $checkoutUrl,
|
||||
'expires_at' => Arr::get($response, 'data.checkout.expires_at')
|
||||
?? Arr::get($response, 'data.expires_at')
|
||||
?? Arr::get($response, 'expires_at'),
|
||||
'id' => $transactionId ?? $checkoutId,
|
||||
'expires_at' => $response['expires_at'] ?? null,
|
||||
'id' => $checkoutId,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -17,14 +17,19 @@ class EventAddonWebhookService
|
||||
|
||||
public function handle(array $payload): bool
|
||||
{
|
||||
$eventType = $payload['event_type'] ?? null;
|
||||
$eventType = $payload['meta']['event_name'] ?? null;
|
||||
$data = $payload['data'] ?? [];
|
||||
|
||||
if ($eventType !== 'transaction.completed') {
|
||||
if (! in_array($eventType, ['order_created', 'order_updated'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$metadata = $this->extractMetadata($data);
|
||||
$status = strtolower((string) data_get($data, 'attributes.status', ''));
|
||||
if ($status !== 'paid') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$metadata = $this->extractMetadata($payload);
|
||||
$intentId = $metadata['addon_intent'] ?? null;
|
||||
$addonKey = $metadata['addon_key'] ?? null;
|
||||
|
||||
@@ -32,8 +37,8 @@ class EventAddonWebhookService
|
||||
return false;
|
||||
}
|
||||
|
||||
$transactionId = $data['id'] ?? $data['transaction_id'] ?? null;
|
||||
$checkoutId = $data['checkout_id'] ?? null;
|
||||
$transactionId = $data['id'] ?? null;
|
||||
$checkoutId = data_get($data, 'attributes.checkout_id') ?? null;
|
||||
|
||||
$addon = EventPackageAddon::query()
|
||||
->where('addon_key', $addonKey)
|
||||
@@ -66,10 +71,12 @@ class EventAddonWebhookService
|
||||
'transaction_id' => $transactionId,
|
||||
'checkout_id' => $addon->checkout_id ?: $checkoutId,
|
||||
'status' => 'completed',
|
||||
'amount' => Arr::get($data, 'totals.grand_total') ?? Arr::get($data, 'amount'),
|
||||
'currency' => Arr::get($data, 'currency_code') ?? Arr::get($data, 'currency'),
|
||||
'amount' => $this->resolveAmount($data),
|
||||
'currency' => Arr::get($data, 'attributes.currency') ?? Arr::get($data, 'currency'),
|
||||
'metadata' => array_merge($addon->metadata ?? [], ['webhook_payload' => $data]),
|
||||
'receipt_payload' => Arr::get($data, 'receipt_url') ? ['receipt_url' => Arr::get($data, 'receipt_url')] : null,
|
||||
'receipt_payload' => Arr::get($data, 'attributes.urls.receipt')
|
||||
? ['receipt_url' => Arr::get($data, 'attributes.urls.receipt')]
|
||||
: null,
|
||||
'purchased_at' => now(),
|
||||
])->save();
|
||||
|
||||
@@ -118,17 +125,36 @@ class EventAddonWebhookService
|
||||
{
|
||||
$metadata = [];
|
||||
|
||||
if (isset($data['metadata']) && is_array($data['metadata'])) {
|
||||
$metadata = $data['metadata'];
|
||||
if (isset($data['meta']['custom_data']) && is_array($data['meta']['custom_data'])) {
|
||||
$metadata = $data['meta']['custom_data'];
|
||||
}
|
||||
|
||||
if (isset($data['custom_data']) && is_array($data['custom_data'])) {
|
||||
$metadata = array_merge($metadata, $data['custom_data']);
|
||||
if (isset($data['metadata']) && is_array($data['metadata'])) {
|
||||
$metadata = array_merge($metadata, $data['metadata']);
|
||||
}
|
||||
|
||||
if (isset($data['attributes']['custom_data']) && is_array($data['attributes']['custom_data'])) {
|
||||
$metadata = array_merge($metadata, $data['attributes']['custom_data']);
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
private function resolveAmount(array $data): ?float
|
||||
{
|
||||
$total = Arr::get($data, 'attributes.total');
|
||||
|
||||
if ($total === null || $total === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! is_numeric($total)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return round(((float) $total) / 100, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int>
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user