set('billing.revenuecat.secret_key', 'rc-secret'); config()->set('billing.revenuecat.subscription_entitlement', 'bowli Pro'); config()->set('billing.revenuecat.certification_entitlement', 'certification'); }); it('synchronizes a monthly subscription trial from RevenueCat', function () { $user = User::factory()->create(); Sanctum::actingAs($user); Http::fake([ "api.revenuecat.com/v1/subscribers/{$user->id}" => Http::response([ 'subscriber' => [ 'entitlements' => [ 'bowli Pro' => [ 'expires_date' => now()->addDays(7)->toISOString(), 'grace_period_expires_date' => null, 'product_identifier' => 'monthly', 'purchase_date' => now()->toISOString(), ], ], 'subscriptions' => [ 'monthly' => [ 'period_type' => 'trial', 'store' => 'app_store', ], ], ], ]), ]); $this->postJson('/api/billing/sync') ->assertOk() ->assertJsonPath('data.hasActiveSubscription', true) ->assertJsonPath('data.analysisAccessLevel', 'trial') ->assertJsonPath('data.subscriptionProductId', 'monthly') ->assertJsonPath('data.certificationPurchased', false); expect($user->fresh()->subscription_is_trial)->toBeTrue() ->and($user->fresh()->subscription_store)->toBe('app_store'); Http::assertSent(fn ($request): bool => $request->hasHeader('Authorization', 'Bearer rc-secret')); }); it('synchronizes annual access and the lifetime certification independently', function () { $user = User::factory()->create(); Sanctum::actingAs($user); $certificationPurchasedAt = now()->subDay()->startOfSecond(); Http::fake([ "api.revenuecat.com/v1/subscribers/{$user->id}" => Http::response([ 'subscriber' => [ 'entitlements' => [ 'bowli Pro' => [ 'expires_date' => now()->addYear()->toISOString(), 'grace_period_expires_date' => null, 'product_identifier' => 'yearly', 'purchase_date' => now()->toISOString(), ], 'certification' => [ 'expires_date' => null, 'grace_period_expires_date' => null, 'product_identifier' => 'lifetime', 'purchase_date' => $certificationPurchasedAt->toISOString(), ], ], 'subscriptions' => [ 'yearly' => [ 'period_type' => 'normal', 'store' => 'play_store', ], ], ], ]), ]); $this->postJson('/api/billing/sync') ->assertOk() ->assertJsonPath('data.analysisAccessLevel', 'subscribed') ->assertJsonPath('data.certificationPurchased', true); $user->refresh(); expect($user->subscription_product_id)->toBe('yearly') ->and($user->subscription_is_trial)->toBeFalse() ->and($user->certification_purchased_at?->equalTo($certificationPurchasedAt))->toBeTrue(); }); it('removes access when RevenueCat returns expired entitlements', function () { $user = User::factory()->create([ 'certification_purchased_at' => now()->subMonth(), 'subscription_expires_at' => now()->addMonth(), 'subscription_is_trial' => true, 'subscription_product_id' => 'monthly', 'subscription_store' => 'app_store', ]); Sanctum::actingAs($user); Http::fake([ "api.revenuecat.com/v1/subscribers/{$user->id}" => Http::response([ 'subscriber' => [ 'entitlements' => [ 'bowli Pro' => [ 'expires_date' => now()->subDay()->toISOString(), 'grace_period_expires_date' => null, 'product_identifier' => 'monthly', ], ], 'subscriptions' => [], ], ]), ]); $this->postJson('/api/billing/sync') ->assertOk() ->assertJsonPath('data.analysisAccessLevel', 'free') ->assertJsonPath('data.hasActiveSubscription', false) ->assertJsonPath('data.certificationPurchased', false); expect($user->fresh()->subscription_product_id)->toBeNull() ->and($user->fresh()->subscription_is_trial)->toBeFalse() ->and($user->fresh()->certification_purchased_at)->toBeNull(); });