Migrate billing from Paddle to Lemon Squeezy
This commit is contained in:
@@ -29,7 +29,7 @@ class CouponRedemptionServiceTest extends TestCase
|
||||
]);
|
||||
$coupon = Coupon::factory()->create([
|
||||
'code' => 'SAVE10',
|
||||
'paddle_discount_id' => 'dsc_123',
|
||||
'lemonsqueezy_discount_id' => 'dsc_123',
|
||||
]);
|
||||
$coupon->packages()->attach($package);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Services\GiftVouchers\GiftVoucherCheckoutService;
|
||||
use App\Services\Paddle\PaddleClient;
|
||||
use App\Services\LemonSqueezy\LemonSqueezyCheckoutService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
@@ -15,8 +15,8 @@ class GiftVoucherCheckoutServiceTest extends TestCase
|
||||
public function test_it_lists_tiers_with_checkout_flag(): void
|
||||
{
|
||||
config()->set('gift-vouchers.tiers', [
|
||||
['key' => 'gift-a', 'label' => 'A', 'amount' => 10, 'currency' => 'EUR', 'paddle_price_id' => 'pri_a'],
|
||||
['key' => 'gift-b', 'label' => 'B', 'amount' => 20, 'currency' => 'EUR', 'paddle_price_id' => null],
|
||||
['key' => 'gift-a', 'label' => 'A', 'amount' => 10, 'currency' => 'EUR', 'lemonsqueezy_variant_id' => 'pri_a'],
|
||||
['key' => 'gift-b', 'label' => 'B', 'amount' => 20, 'currency' => 'EUR', 'lemonsqueezy_variant_id' => null],
|
||||
]);
|
||||
|
||||
$service = $this->app->make(GiftVoucherCheckoutService::class);
|
||||
@@ -31,26 +31,23 @@ class GiftVoucherCheckoutServiceTest extends TestCase
|
||||
public function test_it_creates_checkout_link_with_metadata(): void
|
||||
{
|
||||
config()->set('gift-vouchers.tiers', [
|
||||
['key' => 'gift-a', 'label' => 'A', 'amount' => 10, 'currency' => 'EUR', 'paddle_price_id' => 'pri_a'],
|
||||
['key' => 'gift-a', 'label' => 'A', 'amount' => 10, 'currency' => 'EUR', 'lemonsqueezy_variant_id' => 'pri_a'],
|
||||
]);
|
||||
|
||||
$client = Mockery::mock(PaddleClient::class);
|
||||
$client->shouldReceive('post')
|
||||
$checkoutService = Mockery::mock(LemonSqueezyCheckoutService::class);
|
||||
$checkoutService->shouldReceive('createVariantCheckout')
|
||||
->once()
|
||||
->with('/customers', Mockery::on(function ($payload) {
|
||||
return $payload['email'] === 'buyer@example.com';
|
||||
}))
|
||||
->andReturn(['data' => ['id' => 'ctm_123']]);
|
||||
$client->shouldReceive('post')
|
||||
->once()
|
||||
->with('/transactions', Mockery::on(function ($payload) {
|
||||
return $payload['items'][0]['price_id'] === 'pri_a'
|
||||
&& $payload['customer_id'] === 'ctm_123'
|
||||
&& $payload['custom_data']['type'] === 'gift_voucher';
|
||||
}))
|
||||
->andReturn(['data' => ['checkout' => ['url' => 'https://paddle.test/checkout/123'], 'id' => 'txn_123']]);
|
||||
->with('pri_a', Mockery::on(function (array $customData) {
|
||||
return ($customData['type'] ?? null) === 'gift_voucher'
|
||||
&& ($customData['tier_key'] ?? null) === 'gift-a'
|
||||
&& ($customData['purchaser_email'] ?? null) === 'buyer@example.com'
|
||||
&& ($customData['recipient_email'] ?? null) === 'friend@example.com'
|
||||
&& ($customData['recipient_name'] ?? null) === 'Friend'
|
||||
&& ($customData['message'] ?? null) === 'Hi';
|
||||
}), Mockery::type('array'))
|
||||
->andReturn(['checkout_url' => 'https://lemonsqueezy.test/checkout/123', 'id' => 'chk_123']);
|
||||
|
||||
$this->app->instance(PaddleClient::class, $client);
|
||||
$this->app->instance(LemonSqueezyCheckoutService::class, $checkoutService);
|
||||
|
||||
$service = $this->app->make(GiftVoucherCheckoutService::class);
|
||||
|
||||
@@ -62,7 +59,7 @@ class GiftVoucherCheckoutServiceTest extends TestCase
|
||||
'message' => 'Hi',
|
||||
]);
|
||||
|
||||
$this->assertSame('https://paddle.test/checkout/123', $checkout['checkout_url']);
|
||||
$this->assertSame('txn_123', $checkout['id']);
|
||||
$this->assertSame('https://lemonsqueezy.test/checkout/123', $checkout['checkout_url']);
|
||||
$this->assertSame('chk_123', $checkout['id']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Enums\CouponType;
|
||||
use App\Jobs\SyncCouponToPaddle;
|
||||
use App\Mail\GiftVoucherIssued;
|
||||
use App\Models\Coupon;
|
||||
use App\Models\GiftVoucher;
|
||||
use App\Models\Package;
|
||||
use App\Services\GiftVouchers\GiftVoucherService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -18,37 +16,38 @@ class GiftVoucherServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_issues_voucher_and_coupon_from_paddle_payload(): void
|
||||
public function test_it_issues_voucher_and_coupon_from_lemonsqueezy_payload(): void
|
||||
{
|
||||
$package = Package::factory()->create([
|
||||
'type' => 'endcustomer',
|
||||
'paddle_price_id' => 'pri_pkg_001',
|
||||
'lemonsqueezy_variant_id' => 'pri_pkg_001',
|
||||
'price' => 59,
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'id' => 'txn_123',
|
||||
'event_type' => 'transaction.completed',
|
||||
'currency_code' => 'EUR',
|
||||
'totals' => [
|
||||
'grand_total' => [
|
||||
'amount' => 5900,
|
||||
'data' => [
|
||||
'id' => 'ord_123',
|
||||
'attributes' => [
|
||||
'total' => 5900,
|
||||
'currency' => 'EUR',
|
||||
'checkout_id' => 'chk_abc',
|
||||
'variant_id' => 'pri_pkg_001',
|
||||
'user_email' => 'buyer@example.com',
|
||||
],
|
||||
],
|
||||
'custom_data' => [
|
||||
'type' => 'gift_card',
|
||||
'purchaser_email' => 'buyer@example.com',
|
||||
'recipient_email' => 'friend@example.com',
|
||||
'recipient_name' => 'Friend',
|
||||
'message' => 'Happy Day',
|
||||
'meta' => [
|
||||
'custom_data' => [
|
||||
'type' => 'gift_card',
|
||||
'purchaser_email' => 'buyer@example.com',
|
||||
'recipient_email' => 'friend@example.com',
|
||||
'recipient_name' => 'Friend',
|
||||
'message' => 'Happy Day',
|
||||
],
|
||||
],
|
||||
'checkout_id' => 'chk_abc',
|
||||
];
|
||||
|
||||
Bus::fake([SyncCouponToPaddle::class]);
|
||||
|
||||
$service = $this->app->make(GiftVoucherService::class);
|
||||
$voucher = $service->issueFromPaddle($payload);
|
||||
$voucher = $service->issueFromLemonSqueezy($payload);
|
||||
|
||||
$this->assertInstanceOf(GiftVoucher::class, $voucher);
|
||||
$this->assertSame(59.00, (float) $voucher->amount);
|
||||
@@ -56,7 +55,6 @@ class GiftVoucherServiceTest extends TestCase
|
||||
$this->assertSame($voucher->code, $voucher->coupon->code);
|
||||
$this->assertTrue($voucher->expires_at->greaterThan(now()->addYears(4)));
|
||||
$this->assertTrue($voucher->coupon->packages()->whereKey($package->id)->exists());
|
||||
Bus::assertDispatched(SyncCouponToPaddle::class);
|
||||
}
|
||||
|
||||
public function test_redeeming_coupon_marks_voucher_redeemed(): void
|
||||
@@ -71,7 +69,7 @@ class GiftVoucherServiceTest extends TestCase
|
||||
'type' => CouponType::FLAT,
|
||||
'amount' => 29,
|
||||
'currency' => 'EUR',
|
||||
'paddle_discount_id' => null,
|
||||
'lemonsqueezy_discount_id' => null,
|
||||
]);
|
||||
|
||||
$voucher->coupon()->associate($coupon)->save();
|
||||
@@ -86,45 +84,48 @@ class GiftVoucherServiceTest extends TestCase
|
||||
public function test_it_sends_notifications_to_purchaser_and_recipient_once(): void
|
||||
{
|
||||
Mail::fake();
|
||||
Bus::fake([SyncCouponToPaddle::class]);
|
||||
config()->set('gift-vouchers.reminder_days', 0);
|
||||
config()->set('gift-vouchers.expiry_reminder_days', 0);
|
||||
|
||||
Package::factory()->create([
|
||||
'type' => 'endcustomer',
|
||||
'paddle_price_id' => 'pri_pkg_001',
|
||||
'lemonsqueezy_variant_id' => 'pri_pkg_001',
|
||||
'price' => 29,
|
||||
]);
|
||||
|
||||
$payload = [
|
||||
'id' => 'txn_456',
|
||||
'currency_code' => 'EUR',
|
||||
'totals' => [
|
||||
'grand_total' => [
|
||||
'amount' => 2900,
|
||||
'data' => [
|
||||
'id' => 'ord_456',
|
||||
'attributes' => [
|
||||
'total' => 2900,
|
||||
'currency' => 'EUR',
|
||||
'checkout_id' => 'chk_notif',
|
||||
'variant_id' => 'pri_pkg_001',
|
||||
'user_email' => 'buyer@example.com',
|
||||
],
|
||||
],
|
||||
'custom_data' => [
|
||||
'type' => 'gift_voucher',
|
||||
'purchaser_email' => 'buyer@example.com',
|
||||
'recipient_email' => 'friend@example.com',
|
||||
'app_locale' => 'de',
|
||||
'meta' => [
|
||||
'custom_data' => [
|
||||
'type' => 'gift_voucher',
|
||||
'purchaser_email' => 'buyer@example.com',
|
||||
'recipient_email' => 'friend@example.com',
|
||||
'app_locale' => 'de',
|
||||
],
|
||||
],
|
||||
'checkout_id' => 'chk_notif',
|
||||
];
|
||||
|
||||
$service = $this->app->make(GiftVoucherService::class);
|
||||
$voucher = $service->issueFromPaddle($payload);
|
||||
$voucher = $service->issueFromLemonSqueezy($payload);
|
||||
|
||||
Mail::assertQueued(GiftVoucherIssued::class, 2);
|
||||
$this->assertTrue((bool) ($voucher->metadata['notifications_sent'] ?? false));
|
||||
|
||||
// Second call (duplicate webhook) should not resend
|
||||
$service->issueFromPaddle($payload);
|
||||
$service->issueFromLemonSqueezy($payload);
|
||||
Mail::assertQueued(GiftVoucherIssued::class, 2);
|
||||
}
|
||||
|
||||
public function test_it_resolves_amount_from_tier_by_price_id(): void
|
||||
public function test_it_resolves_amount_from_tier_by_variant_id(): void
|
||||
{
|
||||
config()->set('gift-vouchers.tiers', [
|
||||
[
|
||||
@@ -132,21 +133,25 @@ class GiftVoucherServiceTest extends TestCase
|
||||
'label' => 'Gift Classic (USD)',
|
||||
'amount' => 65.00,
|
||||
'currency' => 'USD',
|
||||
'paddle_price_id' => 'pri_usd_123',
|
||||
'lemonsqueezy_variant_id' => 'pri_usd_123',
|
||||
],
|
||||
]);
|
||||
|
||||
Bus::fake([SyncCouponToPaddle::class]);
|
||||
Mail::fake();
|
||||
|
||||
$payload = [
|
||||
'id' => 'txn_usd',
|
||||
'price_id' => 'pri_usd_123',
|
||||
'currency_code' => 'USD',
|
||||
'data' => [
|
||||
'id' => 'ord_usd',
|
||||
'attributes' => [
|
||||
'variant_id' => 'pri_usd_123',
|
||||
'currency' => 'USD',
|
||||
'total' => 6500,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$service = $this->app->make(GiftVoucherService::class);
|
||||
$voucher = $service->issueFromPaddle($payload);
|
||||
$voucher = $service->issueFromLemonSqueezy($payload);
|
||||
|
||||
$this->assertSame(65.00, (float) $voucher->amount);
|
||||
$this->assertSame('USD', $voucher->currency);
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
namespace Tests\Unit\Jobs;
|
||||
|
||||
use App\Jobs\SyncPackageAddonToPaddle;
|
||||
use App\Jobs\SyncPackageAddonToLemonSqueezy;
|
||||
use App\Models\PackageAddon;
|
||||
use App\Services\Paddle\PaddleAddonCatalogService;
|
||||
use App\Services\LemonSqueezy\LemonSqueezyAddonCatalogService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SyncPackageAddonToPaddleTest extends TestCase
|
||||
class SyncPackageAddonToLemonSqueezyTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
@@ -22,7 +22,7 @@ class SyncPackageAddonToPaddleTest extends TestCase
|
||||
'metadata' => ['price_eur' => 5],
|
||||
]);
|
||||
|
||||
$service = Mockery::mock(PaddleAddonCatalogService::class);
|
||||
$service = Mockery::mock(LemonSqueezyAddonCatalogService::class);
|
||||
$service->shouldReceive('createProduct')
|
||||
->once()
|
||||
->andReturn(['id' => 'pro_addon_1']);
|
||||
@@ -30,13 +30,13 @@ class SyncPackageAddonToPaddleTest extends TestCase
|
||||
->once()
|
||||
->andReturn(['id' => 'pri_addon_1']);
|
||||
|
||||
$job = new SyncPackageAddonToPaddle($addon->id);
|
||||
$job = new SyncPackageAddonToLemonSqueezy($addon->id);
|
||||
$job->handle($service);
|
||||
|
||||
$addon->refresh();
|
||||
|
||||
$this->assertSame('pri_addon_1', $addon->price_id);
|
||||
$this->assertEquals('pro_addon_1', $addon->metadata['paddle_product_id']);
|
||||
$this->assertEquals('synced', $addon->metadata['paddle_sync_status']);
|
||||
$this->assertSame('pri_addon_1', $addon->variant_id);
|
||||
$this->assertEquals('pro_addon_1', $addon->metadata['lemonsqueezy_product_id']);
|
||||
$this->assertEquals('synced', $addon->metadata['lemonsqueezy_sync_status']);
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Package;
|
||||
use App\Services\Paddle\PaddleCatalogService;
|
||||
use App\Services\Paddle\PaddleClient;
|
||||
use App\Services\LemonSqueezy\LemonSqueezyCatalogService;
|
||||
use App\Services\LemonSqueezy\LemonSqueezyClient;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PaddleCatalogServiceTest extends TestCase
|
||||
class LemonSqueezyCatalogServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
@@ -40,7 +40,7 @@ class PaddleCatalogServiceTest extends TestCase
|
||||
],
|
||||
]);
|
||||
|
||||
$service = new PaddleCatalogService(Mockery::mock(PaddleClient::class));
|
||||
$service = new LemonSqueezyCatalogService(Mockery::mock(LemonSqueezyClient::class));
|
||||
|
||||
$payload = $service->buildProductPayload($package);
|
||||
|
||||
@@ -63,7 +63,7 @@ class PaddleCatalogServiceTest extends TestCase
|
||||
'name' => 'Silver Plan',
|
||||
]);
|
||||
|
||||
$service = new PaddleCatalogService(Mockery::mock(PaddleClient::class));
|
||||
$service = new LemonSqueezyCatalogService(Mockery::mock(LemonSqueezyClient::class));
|
||||
|
||||
$payload = $service->buildPricePayload($package, 'pro_123');
|
||||
|
||||
61
tests/Unit/LemonSqueezyCheckoutServiceTest.php
Normal file
61
tests/Unit/LemonSqueezyCheckoutServiceTest.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Package;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\LemonSqueezy\LemonSqueezyCheckoutService;
|
||||
use App\Services\LemonSqueezy\LemonSqueezyClient;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LemonSqueezyCheckoutServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_create_checkout_sends_custom_data_payload(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create([
|
||||
'contact_email' => 'buyer@example.com',
|
||||
]);
|
||||
|
||||
$package = Package::factory()->create([
|
||||
'lemonsqueezy_variant_id' => 'pri_123',
|
||||
]);
|
||||
|
||||
config()->set('lemonsqueezy.store_id', 'store_123');
|
||||
|
||||
$client = Mockery::mock(LemonSqueezyClient::class);
|
||||
$client->shouldReceive('post')
|
||||
->once()
|
||||
->with('/checkouts', Mockery::on(function (array $payload) use ($tenant, $package) {
|
||||
$data = $payload['data'] ?? [];
|
||||
$attributes = $data['attributes'] ?? [];
|
||||
$custom = $attributes['checkout_data']['custom'] ?? [];
|
||||
|
||||
return ($data['type'] ?? null) === 'checkouts'
|
||||
&& ($data['relationships']['variant']['data']['id'] ?? null) === 'pri_123'
|
||||
&& ($data['relationships']['store']['data']['id'] ?? null) === 'store_123'
|
||||
&& ($custom['tenant_id'] ?? null) === (string) $tenant->id
|
||||
&& ($custom['package_id'] ?? null) === (string) $package->id
|
||||
&& ($custom['source'] ?? null) === 'test'
|
||||
&& ($custom['success_url'] ?? null) === 'https://example.test/success'
|
||||
&& ($custom['return_url'] ?? null) === 'https://example.test/cancel';
|
||||
}))
|
||||
->andReturn(['data' => ['attributes' => ['url' => 'https://lemonsqueezy.test/checkout/123'], 'id' => 'chk_123']]);
|
||||
|
||||
$this->app->instance(LemonSqueezyClient::class, $client);
|
||||
|
||||
$service = $this->app->make(LemonSqueezyCheckoutService::class);
|
||||
|
||||
$checkout = $service->createCheckout($tenant, $package, [
|
||||
'success_url' => 'https://example.test/success',
|
||||
'return_url' => 'https://example.test/cancel',
|
||||
'metadata' => ['source' => 'test'],
|
||||
]);
|
||||
|
||||
$this->assertSame('https://lemonsqueezy.test/checkout/123', $checkout['checkout_url']);
|
||||
$this->assertSame('chk_123', $checkout['id']);
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,13 @@ namespace Tests\Unit;
|
||||
|
||||
use App\Enums\CouponType;
|
||||
use App\Models\Coupon;
|
||||
use App\Services\Paddle\PaddleClient;
|
||||
use App\Services\Paddle\PaddleDiscountService;
|
||||
use App\Services\LemonSqueezy\LemonSqueezyClient;
|
||||
use App\Services\LemonSqueezy\LemonSqueezyDiscountService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PaddleDiscountServiceTest extends TestCase
|
||||
class LemonSqueezyDiscountServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
@@ -32,7 +32,7 @@ class PaddleDiscountServiceTest extends TestCase
|
||||
'amount' => 10,
|
||||
]);
|
||||
|
||||
$service = new TestablePaddleDiscountService(Mockery::mock(PaddleClient::class));
|
||||
$service = new TestableLemonSqueezyDiscountService(Mockery::mock(LemonSqueezyClient::class));
|
||||
|
||||
$payload = $service->buildPayload($coupon);
|
||||
|
||||
@@ -50,7 +50,7 @@ class PaddleDiscountServiceTest extends TestCase
|
||||
'description' => 'Flat discount',
|
||||
]);
|
||||
|
||||
$service = new TestablePaddleDiscountService(Mockery::mock(PaddleClient::class));
|
||||
$service = new TestableLemonSqueezyDiscountService(Mockery::mock(LemonSqueezyClient::class));
|
||||
|
||||
$payload = $service->buildPayload($coupon);
|
||||
|
||||
@@ -67,7 +67,7 @@ class PaddleDiscountServiceTest extends TestCase
|
||||
'description' => 'Percent discount',
|
||||
]);
|
||||
|
||||
$service = new TestablePaddleDiscountService(Mockery::mock(PaddleClient::class));
|
||||
$service = new TestableLemonSqueezyDiscountService(Mockery::mock(LemonSqueezyClient::class));
|
||||
|
||||
$payload = $service->buildPayload($coupon);
|
||||
|
||||
@@ -76,7 +76,7 @@ class PaddleDiscountServiceTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
class TestablePaddleDiscountService extends PaddleDiscountService
|
||||
class TestableLemonSqueezyDiscountService extends LemonSqueezyDiscountService
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
52
tests/Unit/LemonSqueezyOrderServiceTest.php
Normal file
52
tests/Unit/LemonSqueezyOrderServiceTest.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Services\LemonSqueezy\LemonSqueezyClient;
|
||||
use App\Services\LemonSqueezy\LemonSqueezyOrderService;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LemonSqueezyOrderServiceTest extends TestCase
|
||||
{
|
||||
public function test_list_for_customer_uses_expected_filters(): void
|
||||
{
|
||||
$client = Mockery::mock(LemonSqueezyClient::class);
|
||||
$client->shouldReceive('get')
|
||||
->once()
|
||||
->with('/orders', Mockery::on(function (array $payload) {
|
||||
return $payload['filter[customer_id]'] === 'ctm_123'
|
||||
&& $payload['sort'] === '-created_at';
|
||||
}))
|
||||
->andReturn(['data' => [], 'meta' => []]);
|
||||
|
||||
$this->app->instance(LemonSqueezyClient::class, $client);
|
||||
|
||||
$service = $this->app->make(LemonSqueezyOrderService::class);
|
||||
$service->listForCustomer('ctm_123');
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_find_by_checkout_id_uses_checkout_then_order_lookup(): void
|
||||
{
|
||||
$client = Mockery::mock(LemonSqueezyClient::class);
|
||||
$client->shouldReceive('get')
|
||||
->once()
|
||||
->with('/checkouts/chk_123', [])
|
||||
->andReturn(['data' => ['attributes' => ['order_id' => 'ord_123']]]);
|
||||
|
||||
$client->shouldReceive('get')
|
||||
->once()
|
||||
->with('/orders/ord_123', [])
|
||||
->andReturn(['data' => ['id' => 'ord_123', 'attributes' => ['status' => 'paid']]]);
|
||||
|
||||
$this->app->instance(LemonSqueezyClient::class, $client);
|
||||
|
||||
$service = $this->app->make(LemonSqueezyOrderService::class);
|
||||
$order = $service->findByCheckoutId('chk_123');
|
||||
|
||||
$this->assertIsArray($order);
|
||||
$this->assertSame('ord_123', $order['id'] ?? null);
|
||||
}
|
||||
}
|
||||
26
tests/Unit/LemonSqueezySyncLoggingConfigTest.php
Normal file
26
tests/Unit/LemonSqueezySyncLoggingConfigTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
class LemonSqueezySyncLoggingConfigTest extends TestCase
|
||||
{
|
||||
public function test_lemonsqueezy_sync_channel_is_configured(): void
|
||||
{
|
||||
$channel = config('logging.channels.lemonsqueezy-sync');
|
||||
|
||||
$this->assertIsArray($channel);
|
||||
$this->assertSame('stack', $channel['driver'] ?? null);
|
||||
$this->assertNotEmpty($channel['channels'] ?? null);
|
||||
}
|
||||
|
||||
public function test_lemonsqueezy_sync_file_channel_is_configured(): void
|
||||
{
|
||||
$channel = config('logging.channels.lemonsqueezy-sync-file');
|
||||
|
||||
$this->assertIsArray($channel);
|
||||
$this->assertSame('daily', $channel['driver'] ?? null);
|
||||
$this->assertSame('lemonsqueezy-sync.log', basename((string) ($channel['path'] ?? '')));
|
||||
}
|
||||
}
|
||||
31
tests/Unit/PackageLemonSqueezyLinkTest.php
Normal file
31
tests/Unit/PackageLemonSqueezyLinkTest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Package;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PackageLemonSqueezyLinkTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_link_lemonsqueezy_ids_updates_fields(): void
|
||||
{
|
||||
$package = Package::factory()->create([
|
||||
'lemonsqueezy_product_id' => null,
|
||||
'lemonsqueezy_variant_id' => null,
|
||||
'lemonsqueezy_sync_status' => null,
|
||||
'lemonsqueezy_synced_at' => null,
|
||||
]);
|
||||
|
||||
$package->linkLemonSqueezyIds('pro_123', 'pri_123');
|
||||
|
||||
$package->refresh();
|
||||
|
||||
$this->assertSame('pro_123', $package->lemonsqueezy_product_id);
|
||||
$this->assertSame('pri_123', $package->lemonsqueezy_variant_id);
|
||||
$this->assertSame('linked', $package->lemonsqueezy_sync_status);
|
||||
$this->assertNotNull($package->lemonsqueezy_synced_at);
|
||||
}
|
||||
}
|
||||
@@ -6,33 +6,33 @@ use App\Models\Package;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PackagePaddleSyncErrorTest extends TestCase
|
||||
class PackageLemonSqueezySyncErrorTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_paddle_sync_error_message_returns_message_when_present(): void
|
||||
public function test_lemonsqueezy_sync_error_message_returns_message_when_present(): void
|
||||
{
|
||||
$package = Package::factory()->create([
|
||||
'paddle_snapshot' => [
|
||||
'lemonsqueezy_snapshot' => [
|
||||
'error' => [
|
||||
'message' => 'Sync failed.',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSame('Sync failed.', $package->paddle_sync_error_message);
|
||||
$this->assertSame('Sync failed.', $package->lemonsqueezy_sync_error_message);
|
||||
}
|
||||
|
||||
public function test_paddle_sync_error_message_returns_null_when_missing(): void
|
||||
public function test_lemonsqueezy_sync_error_message_returns_null_when_missing(): void
|
||||
{
|
||||
$package = Package::factory()->create([
|
||||
'paddle_snapshot' => [
|
||||
'lemonsqueezy_snapshot' => [
|
||||
'product' => [
|
||||
'id' => 'pro_123',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertNull($package->paddle_sync_error_message);
|
||||
$this->assertNull($package->lemonsqueezy_sync_error_message);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Package;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PackagePaddleLinkTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_link_paddle_ids_updates_fields(): void
|
||||
{
|
||||
$package = Package::factory()->create([
|
||||
'paddle_product_id' => null,
|
||||
'paddle_price_id' => null,
|
||||
'paddle_sync_status' => null,
|
||||
'paddle_synced_at' => null,
|
||||
]);
|
||||
|
||||
$package->linkPaddleIds('pro_123', 'pri_123');
|
||||
|
||||
$package->refresh();
|
||||
|
||||
$this->assertSame('pro_123', $package->paddle_product_id);
|
||||
$this->assertSame('pri_123', $package->paddle_price_id);
|
||||
$this->assertSame('linked', $package->paddle_sync_status);
|
||||
$this->assertNotNull($package->paddle_synced_at);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Package;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Paddle\PaddleCheckoutService;
|
||||
use App\Services\Paddle\PaddleClient;
|
||||
use App\Services\Paddle\PaddleCustomerService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PaddleCheckoutServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_create_checkout_sends_custom_data_payload(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create([
|
||||
'contact_email' => 'buyer@example.com',
|
||||
]);
|
||||
|
||||
$package = Package::factory()->create([
|
||||
'paddle_price_id' => 'pri_123',
|
||||
]);
|
||||
|
||||
$client = Mockery::mock(PaddleClient::class);
|
||||
$customers = Mockery::mock(PaddleCustomerService::class);
|
||||
|
||||
$customers->shouldReceive('ensureCustomerId')
|
||||
->once()
|
||||
->with($tenant)
|
||||
->andReturn('ctm_123');
|
||||
|
||||
$client->shouldReceive('post')
|
||||
->once()
|
||||
->with('/transactions', Mockery::on(function (array $payload) use ($tenant, $package) {
|
||||
return $payload['items'][0]['price_id'] === 'pri_123'
|
||||
&& $payload['customer_id'] === 'ctm_123'
|
||||
&& ($payload['custom_data']['tenant_id'] ?? null) === (string) $tenant->id
|
||||
&& ($payload['custom_data']['package_id'] ?? null) === (string) $package->id
|
||||
&& ($payload['custom_data']['source'] ?? null) === 'test'
|
||||
&& ($payload['custom_data']['success_url'] ?? null) === 'https://example.test/success'
|
||||
&& ($payload['custom_data']['cancel_url'] ?? null) === 'https://example.test/cancel'
|
||||
&& ! isset($payload['metadata'])
|
||||
&& ! isset($payload['success_url'])
|
||||
&& ! isset($payload['cancel_url'])
|
||||
&& ! isset($payload['customer_email']);
|
||||
}))
|
||||
->andReturn(['data' => ['checkout' => ['url' => 'https://paddle.test/checkout/123'], 'id' => 'txn_123']]);
|
||||
|
||||
$this->app->instance(PaddleClient::class, $client);
|
||||
$this->app->instance(PaddleCustomerService::class, $customers);
|
||||
|
||||
$service = $this->app->make(PaddleCheckoutService::class);
|
||||
|
||||
$checkout = $service->createCheckout($tenant, $package, [
|
||||
'success_url' => 'https://example.test/success',
|
||||
'return_url' => 'https://example.test/cancel',
|
||||
'metadata' => ['source' => 'test'],
|
||||
]);
|
||||
|
||||
$this->assertSame('https://paddle.test/checkout/123', $checkout['checkout_url']);
|
||||
$this->assertSame('txn_123', $checkout['id']);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
class PaddleSyncLoggingConfigTest extends TestCase
|
||||
{
|
||||
public function test_paddle_sync_channel_is_configured(): void
|
||||
{
|
||||
$channel = config('logging.channels.paddle-sync');
|
||||
|
||||
$this->assertIsArray($channel);
|
||||
$this->assertSame('stack', $channel['driver'] ?? null);
|
||||
$this->assertNotEmpty($channel['channels'] ?? null);
|
||||
}
|
||||
|
||||
public function test_paddle_sync_file_channel_is_configured(): void
|
||||
{
|
||||
$channel = config('logging.channels.paddle-sync-file');
|
||||
|
||||
$this->assertIsArray($channel);
|
||||
$this->assertSame('daily', $channel['driver'] ?? null);
|
||||
$this->assertSame('paddle-sync.log', basename((string) ($channel['path'] ?? '')));
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Services\Paddle\PaddleClient;
|
||||
use App\Services\Paddle\PaddleTransactionService;
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PaddleTransactionServiceTest extends TestCase
|
||||
{
|
||||
public function test_list_for_customer_uses_expected_order_by_format(): void
|
||||
{
|
||||
$client = Mockery::mock(PaddleClient::class);
|
||||
$client->shouldReceive('get')
|
||||
->once()
|
||||
->with('/transactions', Mockery::on(function (array $payload) {
|
||||
return $payload['customer_id'] === 'ctm_123'
|
||||
&& $payload['order_by'] === 'created_at[desc]';
|
||||
}))
|
||||
->andReturn(['data' => [], 'meta' => ['pagination' => []]]);
|
||||
|
||||
$this->app->instance(PaddleClient::class, $client);
|
||||
|
||||
$service = $this->app->make(PaddleTransactionService::class);
|
||||
$service->listForCustomer('ctm_123');
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
public function test_find_by_checkout_id_uses_expected_order_by_format(): void
|
||||
{
|
||||
$client = Mockery::mock(PaddleClient::class);
|
||||
$client->shouldReceive('get')
|
||||
->once()
|
||||
->with('/transactions', Mockery::on(function (array $payload) {
|
||||
return $payload['checkout_id'] === 'chk_123'
|
||||
&& $payload['order_by'] === 'created_at[desc]';
|
||||
}))
|
||||
->andReturn(['data' => []]);
|
||||
|
||||
$this->app->instance(PaddleClient::class, $client);
|
||||
|
||||
$service = $this->app->make(PaddleTransactionService::class);
|
||||
$this->assertNull($service->findByCheckoutId('chk_123'));
|
||||
}
|
||||
|
||||
public function test_find_by_custom_data_uses_expected_order_by_format(): void
|
||||
{
|
||||
$client = Mockery::mock(PaddleClient::class);
|
||||
$client->shouldReceive('get')
|
||||
->once()
|
||||
->with('/transactions', Mockery::on(function (array $payload) {
|
||||
return $payload['order_by'] === 'created_at[desc]'
|
||||
&& $payload['per_page'] === 20;
|
||||
}))
|
||||
->andReturn(['data' => []]);
|
||||
|
||||
$this->app->instance(PaddleClient::class, $client);
|
||||
|
||||
$service = $this->app->make(PaddleTransactionService::class);
|
||||
$this->assertNull($service->findByCustomData([
|
||||
'checkout_session_id' => 'sess_123',
|
||||
]));
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ class SuperAdminNavigationGroupsTest extends TestCase
|
||||
\App\Filament\Resources\PackageResource::class => 'admin.nav.commercial',
|
||||
\App\Filament\Resources\PhotoboothSettings\PhotoboothSettingResource::class => 'admin.nav.storage',
|
||||
\App\Filament\Resources\PurchaseResource::class => 'admin.nav.billing',
|
||||
\App\Filament\Clusters\DailyOps\Resources\TenantPaddleHealths\TenantPaddleHealthResource::class => 'admin.nav.billing',
|
||||
\App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths\TenantLemonSqueezyHealthResource::class => 'admin.nav.billing',
|
||||
\App\Filament\Resources\PurchaseHistoryResource::class => 'admin.nav.commercial',
|
||||
\App\Filament\Resources\EventPurchaseResource::class => 'admin.nav.commercial',
|
||||
\App\Filament\Resources\TenantPackageResource::class => 'admin.nav.commercial',
|
||||
@@ -57,7 +57,7 @@ class SuperAdminNavigationGroupsTest extends TestCase
|
||||
\App\Filament\Resources\PhotoResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\Resources\TenantResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\Resources\PurchaseResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\Clusters\DailyOps\Resources\TenantPaddleHealths\TenantPaddleHealthResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\Clusters\DailyOps\Resources\TenantLemonSqueezyHealths\TenantLemonSqueezyHealthResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\Resources\TenantFeedbackResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\SuperAdmin\Pages\OpsHealthDashboard::class => DailyOpsCluster::class,
|
||||
\App\Filament\SuperAdmin\Pages\IntegrationsHealthDashboard::class => DailyOpsCluster::class,
|
||||
|
||||
Reference in New Issue
Block a user