set('services.revenuecat.product_mappings', 'pro_month:5'); $tenant = Tenant::factory()->create([ 'event_credits_balance' => 1, 'subscription_tier' => 'free', ]); $expiresAt = Carbon::now()->addDays(30)->setTimezone('UTC')->floorSecond(); $payload = [ 'event' => [ 'app_user_id' => 'tenant:' . $tenant->id, 'product_id' => 'pro_month', 'transaction_id' => 'txn-test-1', 'price' => 19.99, 'currency' => 'eur', 'expiration_at_ms' => (int) ($expiresAt->valueOf()), ], ]; $job = new ProcessRevenueCatWebhook($payload, 'evt-test-1'); $job->handle(); $tenant->refresh(); $this->assertSame(6, $tenant->event_credits_balance); $this->assertSame('pro', $tenant->subscription_tier); $this->assertNotNull($tenant->subscription_expires_at); $expected = $expiresAt->clone()->setTimezone(config('app.timezone')); $this->assertLessThanOrEqual(3600, abs($tenant->subscription_expires_at->timestamp - $expected->timestamp)); $this->assertDatabaseHas('event_purchases', [ 'tenant_id' => $tenant->id, 'provider' => 'revenuecat', 'external_receipt_id' => 'txn-test-1', 'events_purchased' => 5, ]); $this->assertDatabaseHas('event_credits_ledger', [ 'tenant_id' => $tenant->id, 'delta' => 5, 'reason' => 'purchase', ]); $duplicateJob = new ProcessRevenueCatWebhook($payload, 'evt-test-1'); $duplicateJob->handle(); $this->assertSame(6, $tenant->fresh()->event_credits_balance); } }