Migrate billing from Paddle to Lemon Squeezy
This commit is contained in:
@@ -17,26 +17,21 @@ class EventAddonCheckoutTest extends TenantTestCase
|
||||
|
||||
Config::set('package-addons.extra_photos_small', [
|
||||
'label' => 'Extra photos (500)',
|
||||
'price_id' => 'pri_addon_photos',
|
||||
'variant_id' => 'var_addon_photos',
|
||||
'increments' => ['extra_photos' => 500],
|
||||
]);
|
||||
|
||||
Config::set('paddle.api_key', 'test_key');
|
||||
Config::set('paddle.base_url', 'https://paddle.test');
|
||||
Config::set('paddle.environment', 'sandbox');
|
||||
Config::set('lemonsqueezy.api_key', 'test_key');
|
||||
Config::set('lemonsqueezy.base_url', 'https://lemonsqueezy.test');
|
||||
Config::set('lemonsqueezy.store_id', 'store_123');
|
||||
|
||||
// Fake Paddle response
|
||||
// Fake Lemon Squeezy response
|
||||
Http::fake([
|
||||
'*/customers' => Http::response([
|
||||
'https://lemonsqueezy.test/checkouts' => Http::response([
|
||||
'data' => [
|
||||
'id' => 'ctm_addon_123',
|
||||
],
|
||||
], 200),
|
||||
'*/transactions' => Http::response([
|
||||
'data' => [
|
||||
'id' => 'txn_addon_123',
|
||||
'checkout' => [
|
||||
'url' => 'https://checkout.paddle.test/abcd',
|
||||
'id' => 'chk_addon_123',
|
||||
'attributes' => [
|
||||
'url' => 'https://checkout.lemonsqueezy.test/abcd',
|
||||
],
|
||||
],
|
||||
], 200),
|
||||
@@ -73,14 +68,14 @@ class EventAddonCheckoutTest extends TenantTestCase
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('checkout_id', 'txn_addon_123');
|
||||
$response->assertJsonPath('checkout_id', 'chk_addon_123');
|
||||
|
||||
$this->assertDatabaseHas('event_package_addons', [
|
||||
'event_package_id' => $eventPackage->id,
|
||||
'addon_key' => 'extra_photos_small',
|
||||
'status' => 'pending',
|
||||
'quantity' => 2,
|
||||
'transaction_id' => 'txn_addon_123',
|
||||
'checkout_id' => 'chk_addon_123',
|
||||
]);
|
||||
|
||||
$addon = EventPackageAddon::where('event_package_id', $eventPackage->id)->latest()->first();
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
namespace Tests\Feature\Tenant;
|
||||
|
||||
use App\Services\Paddle\Exceptions\PaddleException;
|
||||
use App\Services\Paddle\PaddleCustomerPortalService;
|
||||
use App\Services\Paddle\PaddleCustomerService;
|
||||
use App\Models\Package;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Services\LemonSqueezy\Exceptions\LemonSqueezyException;
|
||||
use App\Services\LemonSqueezy\LemonSqueezySubscriptionService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mockery;
|
||||
|
||||
@@ -17,7 +18,7 @@ class TenantBillingPortalTest extends TenantTestCase
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_portal_logs_paddle_error_context(): void
|
||||
public function test_portal_logs_lemonsqueezy_error_context(): void
|
||||
{
|
||||
$logged = [];
|
||||
Log::listen(function ($event) use (&$logged): void {
|
||||
@@ -28,39 +29,44 @@ class TenantBillingPortalTest extends TenantTestCase
|
||||
];
|
||||
});
|
||||
|
||||
$customerService = Mockery::mock(PaddleCustomerService::class);
|
||||
$customerService->shouldReceive('ensureCustomerId')
|
||||
->once()
|
||||
->withArgs(fn ($tenant) => $tenant->is($this->tenant))
|
||||
->andReturn('ctm_test_123');
|
||||
$this->instance(PaddleCustomerService::class, $customerService);
|
||||
$package = Package::factory()->reseller()->create();
|
||||
TenantPackage::factory()->create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'lemonsqueezy_subscription_id' => 'sub_123',
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$portalService = Mockery::mock(PaddleCustomerPortalService::class);
|
||||
$portalService->shouldReceive('createSession')
|
||||
$subscriptions = Mockery::mock(LemonSqueezySubscriptionService::class);
|
||||
$subscriptions->shouldReceive('retrieve')
|
||||
->once()
|
||||
->with('ctm_test_123')
|
||||
->andThrow(new PaddleException('Paddle request failed with status 404', 404, [
|
||||
'error' => [
|
||||
'code' => 'entity_not_found',
|
||||
'message' => 'Not found',
|
||||
->with('sub_123')
|
||||
->andThrow(new LemonSqueezyException('Not found', 404, [
|
||||
'errors' => [
|
||||
[
|
||||
'code' => 'entity_not_found',
|
||||
'detail' => 'Not found',
|
||||
],
|
||||
],
|
||||
'meta' => [
|
||||
'request_id' => 'req_123',
|
||||
],
|
||||
]));
|
||||
$this->instance(PaddleCustomerPortalService::class, $portalService);
|
||||
$this->instance(LemonSqueezySubscriptionService::class, $subscriptions);
|
||||
|
||||
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/billing/portal');
|
||||
|
||||
$response->assertStatus(502)
|
||||
->assertJson([
|
||||
'message' => 'Failed to create Paddle customer portal session.',
|
||||
'message' => 'Failed to fetch Lemon Squeezy subscription portal URL.',
|
||||
]);
|
||||
|
||||
$matched = collect($logged)->contains(function (array $entry): bool {
|
||||
return $entry['level'] === 'warning'
|
||||
&& $entry['message'] === 'Failed to create Paddle customer portal session'
|
||||
&& $entry['message'] === 'Failed to fetch Lemon Squeezy subscription portal URL'
|
||||
&& ($entry['context']['tenant_id'] ?? null) === $this->tenant->id
|
||||
&& ($entry['context']['paddle_customer_id'] ?? null) === 'ctm_test_123'
|
||||
&& ($entry['context']['paddle_status'] ?? null) === 404
|
||||
&& ($entry['context']['paddle_error_code'] ?? null) === 'entity_not_found';
|
||||
&& ($entry['context']['lemonsqueezy_status'] ?? null) === 404
|
||||
&& (($entry['context']['lemonsqueezy_error']['code'] ?? null) === 'entity_not_found');
|
||||
});
|
||||
|
||||
$this->assertTrue($matched);
|
||||
|
||||
@@ -20,14 +20,14 @@ class TenantCheckoutSessionStatusTest extends TenantTestCase
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'status' => CheckoutSession::STATUS_FAILED,
|
||||
'provider' => CheckoutSession::PROVIDER_PADDLE,
|
||||
'provider' => CheckoutSession::PROVIDER_LEMONSQUEEZY,
|
||||
'provider_metadata' => [
|
||||
'paddle_checkout_url' => 'https://checkout.paddle.test/checkout/123',
|
||||
'lemonsqueezy_checkout_url' => 'https://checkout.lemonsqueezy.test/checkout/123',
|
||||
],
|
||||
'status_history' => [
|
||||
[
|
||||
'status' => CheckoutSession::STATUS_FAILED,
|
||||
'reason' => 'paddle_failed',
|
||||
'reason' => 'lemonsqueezy_failed',
|
||||
'at' => now()->toIso8601String(),
|
||||
],
|
||||
],
|
||||
@@ -40,7 +40,7 @@ class TenantCheckoutSessionStatusTest extends TenantTestCase
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('status', CheckoutSession::STATUS_FAILED)
|
||||
->assertJsonPath('reason', 'paddle_failed')
|
||||
->assertJsonPath('checkout_url', 'https://checkout.paddle.test/checkout/123');
|
||||
->assertJsonPath('reason', 'lemonsqueezy_failed')
|
||||
->assertJsonPath('checkout_url', 'https://checkout.lemonsqueezy.test/checkout/123');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace Tests\Feature\Tenant;
|
||||
|
||||
use App\Models\Package;
|
||||
use App\Services\Paddle\PaddleCheckoutService;
|
||||
use App\Services\LemonSqueezy\LemonSqueezyCheckoutService;
|
||||
use Mockery;
|
||||
|
||||
class TenantPaddleCheckoutTest extends TenantTestCase
|
||||
class TenantLemonSqueezyCheckoutTest extends TenantTestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
@@ -15,14 +15,14 @@ class TenantPaddleCheckoutTest extends TenantTestCase
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_tenant_can_create_paddle_checkout(): void
|
||||
public function test_tenant_can_create_lemonsqueezy_checkout(): void
|
||||
{
|
||||
$package = Package::factory()->create([
|
||||
'paddle_price_id' => 'pri_test_123',
|
||||
'lemonsqueezy_variant_id' => 'pri_test_123',
|
||||
'price' => 129,
|
||||
]);
|
||||
|
||||
$checkoutService = Mockery::mock(PaddleCheckoutService::class);
|
||||
$checkoutService = Mockery::mock(LemonSqueezyCheckoutService::class);
|
||||
$checkoutService->shouldReceive('createCheckout')
|
||||
->once()
|
||||
->withArgs(function ($tenant, $payloadPackage, array $payload) use ($package) {
|
||||
@@ -35,28 +35,28 @@ class TenantPaddleCheckoutTest extends TenantTestCase
|
||||
&& ! empty($payload['metadata']['checkout_session_id']);
|
||||
})
|
||||
->andReturn([
|
||||
'checkout_url' => 'https://checkout.paddle.test/checkout/123',
|
||||
'checkout_url' => 'https://checkout.lemonsqueezy.test/checkout/123',
|
||||
'id' => 'chk_test_123',
|
||||
]);
|
||||
$this->instance(PaddleCheckoutService::class, $checkoutService);
|
||||
$this->instance(LemonSqueezyCheckoutService::class, $checkoutService);
|
||||
|
||||
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/packages/paddle-checkout', [
|
||||
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/packages/lemonsqueezy-checkout', [
|
||||
'package_id' => $package->id,
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('checkout_url', 'https://checkout.paddle.test/checkout/123')
|
||||
->assertJsonPath('checkout_url', 'https://checkout.lemonsqueezy.test/checkout/123')
|
||||
->assertJsonStructure(['checkout_session_id']);
|
||||
}
|
||||
|
||||
public function test_paddle_checkout_requires_paddle_price_id(): void
|
||||
public function test_lemonsqueezy_checkout_requires_lemonsqueezy_variant_id(): void
|
||||
{
|
||||
$package = Package::factory()->create([
|
||||
'paddle_price_id' => null,
|
||||
'lemonsqueezy_variant_id' => null,
|
||||
'price' => 129,
|
||||
]);
|
||||
|
||||
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/packages/paddle-checkout', [
|
||||
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/packages/lemonsqueezy-checkout', [
|
||||
'package_id' => $package->id,
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user