create([ 'slug' => 'credits-tenant', 'event_credits_balance' => 0, ]); $client = OAuthClient::create([ 'id' => (string) Str::uuid(), 'client_id' => 'tenant-admin-app', 'tenant_id' => $tenant->id, 'redirect_uris' => ['http://localhost/callback'], 'scopes' => ['tenant:read', 'tenant:write'], 'is_active' => true, ]); [$accessToken] = $this->obtainTokens($client); $headers = [ 'Authorization' => 'Bearer '.$accessToken, ]; $balanceResponse = $this->withHeaders($headers) ->getJson('/api/v1/tenant/credits/balance'); $balanceResponse->assertOk() ->assertJsonStructure(['balance', 'free_event_granted_at']); $purchaseResponse = $this->withHeaders($headers) ->postJson('/api/v1/tenant/credits/purchase', [ 'package_id' => 'event_starter', 'credits_added' => 5, 'platform' => 'capacitor', 'transaction_id' => 'txn_test_123', 'subscription_active' => false, ]); $purchaseResponse->assertCreated() ->assertJsonStructure(['message', 'balance', 'subscription_active']); $tenant->refresh(); $this->assertSame(5, $tenant->event_credits_balance); $this->assertDatabaseHas('event_purchases', [ 'tenant_id' => $tenant->id, 'events_purchased' => 5, 'external_receipt_id' => 'txn_test_123', ]); $this->assertDatabaseHas('event_credits_ledger', [ 'tenant_id' => $tenant->id, 'delta' => 5, 'reason' => 'purchase', ]); $syncResponse = $this->withHeaders($headers) ->postJson('/api/v1/tenant/credits/sync', [ 'balance' => $tenant->event_credits_balance, 'subscription_active' => false, 'last_sync' => now()->toIso8601String(), ]); $syncResponse->assertOk() ->assertJsonStructure(['balance', 'subscription_active', 'server_time']); } private function obtainTokens(OAuthClient $client): array { $codeVerifier = 'tenant-credits-code-verifier-1234567890'; $codeChallenge = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '='); $state = Str::random(10); $response = $this->get('/api/v1/oauth/authorize?' . http_build_query([ 'client_id' => $client->client_id, 'redirect_uri' => 'http://localhost/callback', 'response_type' => 'code', 'scope' => 'tenant:read tenant:write', 'state' => $state, 'code_challenge' => $codeChallenge, 'code_challenge_method' => 'S256', ])); $response->assertRedirect(); $location = $response->headers->get('Location'); $this->assertNotNull($location); $query = []; parse_str(parse_url($location, PHP_URL_QUERY) ?? '', $query); $authorizationCode = $query['code'] ?? null; $this->assertNotNull($authorizationCode, 'Authorization code should be present'); $tokenResponse = $this->post('/api/v1/oauth/token', [ 'grant_type' => 'authorization_code', 'code' => $authorizationCode, 'client_id' => $client->client_id, 'redirect_uri' => 'http://localhost/callback', 'code_verifier' => $codeVerifier, ]); $tokenResponse->assertOk(); return [ $tokenResponse->json('access_token'), $tokenResponse->json('refresh_token'), ]; } }