Migrate billing from Paddle to Lemon Squeezy
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-03 10:59:54 +01:00
parent c96a73d884
commit 10c99de1e2
228 changed files with 4369 additions and 4067 deletions

View File

@@ -15,8 +15,8 @@ use App\Models\Tenant;
use App\Models\User;
use App\Services\Checkout\CheckoutAssignmentService;
use App\Services\Checkout\CheckoutSessionService;
use App\Services\Paddle\Exceptions\PaddleException;
use App\Services\Paddle\PaddleTransactionService;
use App\Services\LemonSqueezy\Exceptions\LemonSqueezyException;
use App\Services\LemonSqueezy\LemonSqueezyOrderService;
use App\Support\CheckoutRequestContext;
use App\Support\CheckoutRoutes;
use App\Support\Concerns\PresentsPackages;
@@ -74,9 +74,9 @@ class CheckoutController extends Controller
'error' => $facebookError,
'profile' => $facebookProfile,
],
'paddle' => [
'environment' => config('paddle.environment'),
'client_token' => config('paddle.client_token'),
'lemonsqueezy' => [
'store_id' => config('lemonsqueezy.store_id'),
'test_mode' => config('lemonsqueezy.test_mode', false),
],
]);
}
@@ -271,9 +271,9 @@ class CheckoutController extends Controller
CheckoutSession $session,
CheckoutSessionService $sessions,
CheckoutAssignmentService $assignment,
PaddleTransactionService $transactions,
LemonSqueezyOrderService $orders,
): JsonResponse {
$this->attemptPaddleRecovery($session, $sessions, $assignment, $transactions);
$this->attemptLemonSqueezyRecovery($session, $sessions, $assignment, $orders);
$session->refresh();
@@ -288,56 +288,56 @@ class CheckoutController extends Controller
CheckoutSession $session,
CheckoutSessionService $sessions,
CheckoutAssignmentService $assignment,
PaddleTransactionService $transactions,
LemonSqueezyOrderService $orders,
): JsonResponse {
$validated = $request->validated();
$transactionId = $validated['transaction_id'] ?? null;
$orderId = $validated['order_id'] ?? null;
$checkoutId = $validated['checkout_id'] ?? null;
$metadata = $session->provider_metadata ?? [];
$metadataUpdated = false;
if ($transactionId) {
$session->paddle_transaction_id = $transactionId;
$metadata['paddle_transaction_id'] = $transactionId;
if ($orderId) {
$session->lemonsqueezy_order_id = $orderId;
$metadata['lemonsqueezy_order_id'] = $orderId;
$metadataUpdated = true;
}
if ($checkoutId) {
$metadata['paddle_checkout_id'] = $checkoutId;
$metadata['lemonsqueezy_checkout_id'] = $checkoutId;
$metadataUpdated = true;
}
if ($metadataUpdated) {
$metadata['paddle_client_event_at'] = now()->toIso8601String();
$metadata['lemonsqueezy_client_event_at'] = now()->toIso8601String();
$session->provider_metadata = $metadata;
$session->save();
}
if (app()->environment('local')
&& $session->provider === CheckoutSession::PROVIDER_PADDLE
&& $session->provider === CheckoutSession::PROVIDER_LEMONSQUEEZY
&& ! in_array($session->status, [
CheckoutSession::STATUS_COMPLETED,
CheckoutSession::STATUS_FAILED,
CheckoutSession::STATUS_CANCELLED,
], true)
&& ($transactionId || $checkoutId)
&& ($orderId || $checkoutId)
) {
$sessions->markProcessing($session, array_filter([
'paddle_status' => 'completed',
'paddle_transaction_id' => $transactionId,
'paddle_local_confirmed_at' => now()->toIso8601String(),
'lemonsqueezy_status' => 'paid',
'lemonsqueezy_order_id' => $orderId,
'lemonsqueezy_local_confirmed_at' => now()->toIso8601String(),
]));
$assignment->finalise($session, [
'source' => 'paddle_local',
'provider' => CheckoutSession::PROVIDER_PADDLE,
'provider_reference' => $transactionId ?? $checkoutId,
'source' => 'lemonsqueezy_local',
'provider' => CheckoutSession::PROVIDER_LEMONSQUEEZY,
'provider_reference' => $orderId ?? $checkoutId,
]);
$sessions->markCompleted($session);
} else {
$this->attemptPaddleRecovery($session, $sessions, $assignment, $transactions);
$this->attemptLemonSqueezyRecovery($session, $sessions, $assignment, $orders);
}
$session->refresh();
@@ -419,13 +419,13 @@ class CheckoutController extends Controller
return $price <= 0;
}
private function attemptPaddleRecovery(
private function attemptLemonSqueezyRecovery(
CheckoutSession $session,
CheckoutSessionService $sessions,
CheckoutAssignmentService $assignment,
PaddleTransactionService $transactions
LemonSqueezyOrderService $orders
): void {
if ($session->provider !== CheckoutSession::PROVIDER_PADDLE) {
if ($session->provider !== CheckoutSession::PROVIDER_LEMONSQUEEZY) {
return;
}
@@ -438,7 +438,7 @@ class CheckoutController extends Controller
}
$metadata = $session->provider_metadata ?? [];
$lastPollAt = $metadata['paddle_poll_at'] ?? null;
$lastPollAt = $metadata['lemonsqueezy_poll_at'] ?? null;
$now = now();
if ($lastPollAt) {
@@ -452,39 +452,31 @@ class CheckoutController extends Controller
}
}
$checkoutId = $metadata['paddle_checkout_id'] ?? $session->paddle_checkout_id ?? null;
$transactionId = $metadata['paddle_transaction_id'] ?? $session->paddle_transaction_id ?? null;
$checkoutId = $metadata['lemonsqueezy_checkout_id'] ?? $session->lemonsqueezy_checkout_id ?? null;
$orderId = $metadata['lemonsqueezy_order_id'] ?? $session->lemonsqueezy_order_id ?? null;
if (! $checkoutId && ! $transactionId) {
Log::info('[Checkout] Paddle recovery missing checkout reference, falling back to custom data scan', [
if (! $checkoutId && ! $orderId) {
Log::info('[Checkout] Lemon Squeezy recovery missing checkout reference', [
'session_id' => $session->id,
]);
}
$metadata['paddle_poll_at'] = $now->toIso8601String();
$metadata['lemonsqueezy_poll_at'] = $now->toIso8601String();
$session->forceFill([
'provider_metadata' => $metadata,
])->save();
try {
$transaction = $transactionId ? $transactions->retrieve($transactionId) : null;
$order = $orderId ? $orders->retrieve($orderId) : null;
if (! $transaction && $checkoutId) {
$transaction = $transactions->findByCheckoutId($checkoutId);
if (! $order && $checkoutId) {
$order = $orders->findByCheckoutId($checkoutId);
}
if (! $transaction) {
$transaction = $transactions->findByCustomData([
'checkout_session_id' => $session->id,
'package_id' => (string) $session->package_id,
'tenant_id' => (string) $session->tenant_id,
]);
}
} catch (PaddleException $exception) {
Log::warning('[Checkout] Paddle recovery failed', [
} catch (LemonSqueezyException $exception) {
Log::warning('[Checkout] Lemon Squeezy recovery failed', [
'session_id' => $session->id,
'checkout_id' => $checkoutId,
'transaction_id' => $transactionId,
'order_id' => $orderId,
'status' => $exception->status(),
'message' => $exception->getMessage(),
'context' => $exception->context(),
@@ -492,77 +484,77 @@ class CheckoutController extends Controller
return;
} catch (\Throwable $exception) {
Log::warning('[Checkout] Paddle recovery failed', [
Log::warning('[Checkout] Lemon Squeezy recovery failed', [
'session_id' => $session->id,
'checkout_id' => $checkoutId,
'transaction_id' => $transactionId,
'order_id' => $orderId,
'message' => $exception->getMessage(),
]);
return;
}
if (! $transaction) {
Log::info('[Checkout] Paddle recovery: transaction not found', [
if (! $order) {
Log::info('[Checkout] Lemon Squeezy recovery: order not found', [
'session_id' => $session->id,
'checkout_id' => $checkoutId,
'transaction_id' => $transactionId,
'order_id' => $orderId,
]);
return;
}
$status = strtolower((string) ($transaction['status'] ?? ''));
$transactionId = $transactionId ?: ($transaction['id'] ?? null);
$status = strtolower((string) data_get($order, 'attributes.status', ''));
$resolvedOrderId = $orderId ?: data_get($order, 'id');
if ($transactionId && $session->paddle_transaction_id !== $transactionId) {
if ($resolvedOrderId && $session->lemonsqueezy_order_id !== $resolvedOrderId) {
$session->forceFill([
'paddle_transaction_id' => $transactionId,
'lemonsqueezy_order_id' => $resolvedOrderId,
])->save();
}
if ($status === 'completed') {
if (in_array($status, ['paid', 'completed'], true)) {
$sessions->markProcessing($session, [
'paddle_status' => $status,
'paddle_transaction_id' => $transactionId,
'paddle_recovered_at' => $now->toIso8601String(),
'lemonsqueezy_status' => $status,
'lemonsqueezy_order_id' => $resolvedOrderId,
'lemonsqueezy_recovered_at' => $now->toIso8601String(),
]);
$assignment->finalise($session, [
'source' => 'paddle_poll',
'provider' => CheckoutSession::PROVIDER_PADDLE,
'provider_reference' => $transactionId,
'payload' => $transaction,
'source' => 'lemonsqueezy_poll',
'provider' => CheckoutSession::PROVIDER_LEMONSQUEEZY,
'provider_reference' => $resolvedOrderId,
'payload' => $order,
]);
$sessions->markCompleted($session, $now);
Log::info('[Checkout] Paddle session recovered via API', [
Log::info('[Checkout] Lemon Squeezy session recovered via API', [
'session_id' => $session->id,
'checkout_id' => $checkoutId,
'transaction_id' => $transactionId,
'order_id' => $resolvedOrderId,
]);
return;
}
if (in_array($status, ['failed', 'cancelled', 'canceled'], true)) {
$sessions->markFailed($session, 'paddle_'.$status);
if (in_array($status, ['failed', 'cancelled', 'canceled', 'refunded', 'voided'], true)) {
$sessions->markFailed($session, 'lemonsqueezy_'.$status);
Log::info('[Checkout] Paddle transaction failed', [
Log::info('[Checkout] Lemon Squeezy order failed', [
'session_id' => $session->id,
'checkout_id' => $checkoutId,
'transaction_id' => $transactionId,
'order_id' => $resolvedOrderId,
'status' => $status,
]);
return;
}
Log::info('[Checkout] Paddle transaction pending', [
Log::info('[Checkout] Lemon Squeezy order pending', [
'session_id' => $session->id,
'checkout_id' => $checkoutId,
'transaction_id' => $transactionId,
'order_id' => $resolvedOrderId,
'status' => $status,
]);
}