seedTenantWithPackage(price: 10); Auth::login($tenant->user); $apiResponse = $this->apiResponse((object) [ 'id' => 'ORDER-123', 'links' => [ (object) ['rel' => 'approve', 'href' => 'https://paypal.test/approve/ORDER-123'], ], ]); $ordersController = Mockery::mock(OrdersController::class); $ordersController->shouldReceive('createOrder') ->once() ->andReturn($apiResponse); $clientMock = Mockery::mock(PaypalServerSdkClient::class); $clientMock->shouldReceive('getOrdersController')->andReturn($ordersController); $factory = Mockery::mock(PaypalClientFactory::class); $factory->shouldReceive('make')->andReturn($clientMock); $this->app->instance(PaypalClientFactory::class, $factory); $response = $this->postJson('/paypal/create-order', [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, ]); $response->assertOk() ->assertJson([ 'id' => 'ORDER-123', 'approve_url' => 'https://paypal.test/approve/ORDER-123', ]); } public function test_paypal_capture_creates_purchase_and_package(): void { [$tenant, $package] = $this->seedTenantWithPackage(price: 15); Auth::login($tenant->user); $metadata = json_encode([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'type' => 'endcustomer_event', ]); $apiResponse = $this->apiResponse((object) [ 'id' => 'ORDER-456', 'purchaseUnits' => [ (object) [ 'customId' => $metadata, 'amount' => (object) ['value' => '15.00'], ], ], ]); $ordersController = Mockery::mock(OrdersController::class); $ordersController->shouldReceive('captureOrder') ->once() ->andReturn($apiResponse); $clientMock = Mockery::mock(PaypalServerSdkClient::class); $clientMock->shouldReceive('getOrdersController')->andReturn($ordersController); $factory = Mockery::mock(PaypalClientFactory::class); $factory->shouldReceive('make')->andReturn($clientMock); $this->app->instance(PaypalClientFactory::class, $factory); $response = $this->postJson('/paypal/capture-order', [ 'order_id' => 'ORDER-456', ]); $response->assertOk() ->assertJson(['status' => 'captured']); $this->assertDatabaseHas('package_purchases', [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'provider_id' => 'ORDER-456', 'price' => 15, ]); $this->assertDatabaseHas('tenant_packages', [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'price' => 15, 'active' => true, ]); $this->assertEquals('active', $tenant->fresh()->subscription_status); } public function test_paypal_capture_failure_returns_error(): void { [$tenant, $package] = $this->seedTenantWithPackage(); Auth::login($tenant->user); $ordersController = Mockery::mock(OrdersController::class); $ordersController->shouldReceive('captureOrder') ->once() ->andThrow(new \RuntimeException('Capture failed')); $clientMock = Mockery::mock(PaypalServerSdkClient::class); $clientMock->shouldReceive('getOrdersController')->andReturn($ordersController); $factory = Mockery::mock(PaypalClientFactory::class); $factory->shouldReceive('make')->andReturn($clientMock); $this->app->instance(PaypalClientFactory::class, $factory); $response = $this->postJson('/paypal/capture-order', [ 'order_id' => 'ORDER-999', ]); $response->assertStatus(500) ->assertJson(['error' => 'Capture failed']); $this->assertDatabaseCount('package_purchases', 0); $this->assertDatabaseCount('tenant_packages', 0); } public function test_paypal_subscription_creation_creates_initial_records(): void { [$tenant, $package] = $this->seedTenantWithPackage(price: 99, type: 'reseller'); Auth::login($tenant->user); $apiResponse = $this->apiResponse((object) [ 'id' => 'ORDER-SUB-1', 'links' => [ (object) ['rel' => 'approve', 'href' => 'https://paypal.test/approve/ORDER-SUB-1'], ], ]); $ordersController = Mockery::mock(OrdersController::class); $ordersController->shouldReceive('createOrder') ->once() ->andReturn($apiResponse); $clientMock = Mockery::mock(PaypalServerSdkClient::class); $clientMock->shouldReceive('getOrdersController')->andReturn($ordersController); $factory = Mockery::mock(PaypalClientFactory::class); $factory->shouldReceive('make')->andReturn($clientMock); $this->app->instance(PaypalClientFactory::class, $factory); $response = $this->postJson('/paypal/create-subscription', [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'plan_id' => 'PLAN-123', ]); $response->assertOk() ->assertJson([ 'order_id' => 'ORDER-SUB-1', 'approve_url' => 'https://paypal.test/approve/ORDER-SUB-1', ]); $this->assertDatabaseHas('tenant_packages', [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'price' => 99, 'active' => true, ]); $this->assertDatabaseHas('package_purchases', [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'provider_id' => 'ORDER-SUB-1_sub_PLAN-123', ]); } public function test_paypal_webhook_capture_completes_purchase(): void { [$tenant, $package] = $this->seedTenantWithPackage(price: 20); $metadata = json_encode([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, ]); $apiResponse = $this->apiResponse((object) [ 'purchaseUnits' => [ (object) ['customId' => $metadata], ], ]); $ordersController = Mockery::mock(OrdersController::class); $ordersController->shouldReceive('showOrder') ->andReturn($apiResponse); $clientMock = Mockery::mock(PaypalServerSdkClient::class); $clientMock->shouldReceive('getOrdersController')->andReturn($ordersController); $factory = Mockery::mock(PaypalClientFactory::class); $factory->shouldReceive('make')->andReturn($clientMock); $this->app->instance(PaypalClientFactory::class, $factory); $event = [ 'event_type' => 'PAYMENT.CAPTURE.COMPLETED', 'resource' => [ 'id' => 'CAPTURE-1', 'order_id' => 'ORDER-WEBHOOK-1', ], ]; $response = $this->postJson('/paypal/webhook', [ 'webhook_id' => 'WH-1', 'webhook_event' => $event, ]); $response->assertOk() ->assertJson(['status' => 'SUCCESS']); $this->assertDatabaseHas('package_purchases', [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'provider_id' => 'ORDER-WEBHOOK-1', ]); $this->assertDatabaseHas('tenant_packages', [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'active' => true, ]); $this->assertEquals('active', $tenant->fresh()->subscription_status); } public function test_paypal_webhook_capture_is_idempotent(): void { [$tenant, $package] = $this->seedTenantWithPackage(price: 25); $metadata = json_encode([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, ]); $apiResponse = $this->apiResponse((object) [ 'purchaseUnits' => [ (object) ['customId' => $metadata], ], ]); $ordersController = Mockery::mock(OrdersController::class); $ordersController->shouldReceive('showOrder') ->andReturn($apiResponse); $clientMock = Mockery::mock(PaypalServerSdkClient::class); $clientMock->shouldReceive('getOrdersController')->andReturn($ordersController); $factory = Mockery::mock(PaypalClientFactory::class); $factory->shouldReceive('make')->andReturn($clientMock); $this->app->instance(PaypalClientFactory::class, $factory); $event = [ 'event_type' => 'PAYMENT.CAPTURE.COMPLETED', 'resource' => [ 'id' => 'CAPTURE-2', 'order_id' => 'ORDER-WEBHOOK-2', ], ]; $payload = [ 'webhook_id' => 'WH-3', 'webhook_event' => $event, ]; $this->postJson('/paypal/webhook', $payload)->assertOk(); $this->postJson('/paypal/webhook', $payload)->assertOk(); $this->assertDatabaseCount('package_purchases', 1); $this->assertDatabaseHas('package_purchases', [ 'provider_id' => 'ORDER-WEBHOOK-2', ]); } private function apiResponse(object $result, int $status = 201): ApiResponse { $response = Mockery::mock(ApiResponse::class); $response->shouldReceive('getStatusCode')->andReturn($status); $response->shouldReceive('getResult')->andReturn($result); return $response; } private function seedTenantWithPackage(int $price = 10, string $type = 'endcustomer'): array { $user = User::factory()->create(['email_verified_at' => now()]); $tenant = Tenant::factory()->create(['user_id' => $user->id]); $package = Package::factory()->create([ 'price' => $price, 'type' => $type, ]); return [$tenant->fresh(), $package]; } }