'client', 'services.paypal.secret' => 'secret', 'services.paypal.sandbox' => true, 'services.paypal.webhook_id' => 'wh_123', ]); Http::fake([ 'https://api-m.sandbox.paypal.com/v1/oauth2/token' => Http::response([ 'access_token' => 'token', 'expires_in' => 3600, ]), 'https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature' => Http::response([ 'verification_status' => 'SUCCESS', ]), ]); [$tenant, $package, $session, $coupon] = $this->prepareSession(withCoupon: true); $session->forceFill(['paypal_order_id' => 'ORDER-123'])->save(); $payload = [ 'id' => 'WH-1', 'event_type' => 'PAYMENT.CAPTURE.COMPLETED', 'resource' => [ 'id' => 'CAPTURE-1', 'status' => 'COMPLETED', 'amount' => [ 'value' => '99.00', 'currency_code' => 'EUR', ], 'supplementary_data' => [ 'related_ids' => [ 'order_id' => 'ORDER-123', ], ], ], ]; $response = $this->withHeaders($this->paypalHeaders()) ->postJson('/paypal/webhook', $payload); $response->assertOk()->assertJson(['status' => 'processed']); $this->assertDatabaseHas('integration_webhook_events', [ 'provider' => 'paypal', 'event_id' => 'WH-1', 'event_type' => 'PAYMENT.CAPTURE.COMPLETED', 'status' => IntegrationWebhookEvent::STATUS_PROCESSED, ]); $session->refresh(); $this->assertEquals(CheckoutSession::STATUS_COMPLETED, $session->status); $this->assertSame('paypal', $session->provider); $this->assertSame('ORDER-123', $session->paypal_order_id); $this->assertSame('CAPTURE-1', $session->paypal_capture_id); $this->assertTrue( TenantPackage::query() ->where('tenant_id', $tenant->id) ->where('package_id', $package->id) ->where('active', true) ->exists() ); $purchase = PackagePurchase::query() ->where('tenant_id', $tenant->id) ->where('package_id', $package->id) ->where('provider', 'paypal') ->first(); $this->assertNotNull($purchase); $this->assertSame(99.0, (float) $purchase->price); $this->assertSame('EUR', Arr::get($purchase->metadata, 'currency')); $this->assertTrue( CouponRedemption::query() ->where('coupon_id', $coupon->id) ->where('checkout_session_id', $session->id) ->where('status', CouponRedemption::STATUS_SUCCESS) ->exists() ); } public function test_rejects_invalid_signature(): void { config([ 'services.paypal.client_id' => 'client', 'services.paypal.secret' => 'secret', 'services.paypal.sandbox' => true, 'services.paypal.webhook_id' => 'wh_123', ]); Http::fake([ 'https://api-m.sandbox.paypal.com/v1/oauth2/token' => Http::response([ 'access_token' => 'token', 'expires_in' => 3600, ]), 'https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature' => Http::response([ 'verification_status' => 'FAILURE', ]), ]); $response = $this->withHeaders($this->paypalHeaders()) ->postJson('/paypal/webhook', [ 'id' => 'WH-FAIL', 'event_type' => 'PAYMENT.CAPTURE.COMPLETED', 'resource' => [ 'id' => 'CAPTURE-FAIL', 'status' => 'COMPLETED', ], ]); $response->assertStatus(400)->assertJson(['status' => 'invalid']); } /** * @return array{ * 0: \App\Models\Tenant, * 1: \App\Models\Package, * 2: \App\Models\CheckoutSession, * 3: \App\Models\Coupon * } */ protected function prepareSession(bool $withCoupon = false): array { $user = User::factory()->create(['email_verified_at' => now()]); $tenant = Tenant::factory()->create(['user_id' => $user->id]); $user->forceFill(['tenant_id' => $tenant->id])->save(); $package = Package::factory()->create([ 'type' => 'endcustomer', 'price' => 99, ]); /** @var CheckoutSessionService $sessions */ $sessions = app(CheckoutSessionService::class); $session = $sessions->createOrResume($user, $package, ['tenant' => $tenant]); $sessions->selectProvider($session, CheckoutSession::PROVIDER_PAYPAL); $coupon = Coupon::factory()->create([ 'lemonsqueezy_discount_id' => null, ]); if ($withCoupon) { /** @var CouponService $coupons */ $coupons = app(CouponService::class); $preview = $coupons->preview($coupon->code, $package, $tenant, CheckoutSession::PROVIDER_PAYPAL); $sessions->applyCoupon($session, $preview['coupon'], $preview['pricing']); } return [$tenant, $package, $session, $coupon]; } /** * @return array */ protected function paypalHeaders(): array { return [ 'PAYPAL-AUTH-ALGO' => 'SHA256withRSA', 'PAYPAL-CERT-URL' => 'https://example.test/cert', 'PAYPAL-TRANSMISSION-ID' => 'transmission-1', 'PAYPAL-TRANSMISSION-SIG' => 'signature', 'PAYPAL-TRANSMISSION-TIME' => '2026-02-04T12:00:00Z', ]; } }