Harden credit flows and add RevenueCat webhook
This commit is contained in:
@@ -11,9 +11,9 @@ class EmotionResourceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_import_emotions_csv()
|
||||
public function test_import_emotions_csv(): void
|
||||
{
|
||||
$csvData = "name_de,name_en,icon,color,description_de,description_en,sort_order,is_active,event_types\nGlück,Joy,😊,#FFD700,Gefühl des Glücks,Feeling of joy,1,1,wedding|birthday";
|
||||
$csvData = "name_de,name_en,icon,color,description_de,description_en,sort_order,is_active,event_types\nGlueck,Joy,smile,#FFD700,Gefuehl des Gluecks,Feeling of joy,1,1,wedding|birthday";
|
||||
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'emotions_');
|
||||
file_put_contents($tempFile, $csvData);
|
||||
@@ -24,15 +24,15 @@ class EmotionResourceTest extends TestCase
|
||||
$this->assertSame(0, $failed);
|
||||
|
||||
$this->assertDatabaseHas('emotions', [
|
||||
'name' => json_encode(['de' => 'Glück', 'en' => 'Joy']),
|
||||
'icon' => '😊',
|
||||
'name' => json_encode(['de' => 'Glueck', 'en' => 'Joy']),
|
||||
'icon' => 'smile',
|
||||
'color' => '#FFD700',
|
||||
'description' => json_encode(['de' => 'Gefühl des Glücks', 'en' => 'Feeling of joy']),
|
||||
'description' => json_encode(['de' => 'Gefuehl des Gluecks', 'en' => 'Feeling of joy']),
|
||||
'sort_order' => 1,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
|
||||
$emotion = Emotion::where('name->de', 'Glück')->first();
|
||||
$emotion = Emotion::where('name->de', 'Glueck')->first();
|
||||
$this->assertNotNull($emotion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,5 +139,16 @@ KEY;
|
||||
$this->assertArrayHasKey('access_token', $refreshData);
|
||||
$this->assertArrayHasKey('refresh_token', $refreshData);
|
||||
$this->assertNotEquals($refreshData['access_token'], $tokenData['access_token']);
|
||||
$this->withServerVariables(['REMOTE_ADDR' => '198.51.100.10'])
|
||||
->post('/api/v1/oauth/token', [
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshData['refresh_token'],
|
||||
'client_id' => 'tenant-admin-app',
|
||||
])
|
||||
->assertStatus(403)
|
||||
->assertJson([
|
||||
'error' => 'Refresh token cannot be used from this IP address',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
57
tests/Feature/RevenueCatWebhookTest.php
Normal file
57
tests/Feature/RevenueCatWebhookTest.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Jobs\ProcessRevenueCatWebhook;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RevenueCatWebhookTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_webhook_dispatches_job_with_valid_signature(): void
|
||||
{
|
||||
config()->set('services.revenuecat.webhook', 'shared-secret');
|
||||
|
||||
$payload = [
|
||||
'event' => [
|
||||
'app_user_id' => 'tenant-123',
|
||||
'product_id' => 'pro_month',
|
||||
],
|
||||
];
|
||||
|
||||
$json = json_encode($payload);
|
||||
$signature = base64_encode(hash_hmac('sha1', $json, 'shared-secret', true));
|
||||
|
||||
Bus::fake();
|
||||
|
||||
$response = $this->postJson('/api/v1/webhooks/revenuecat', $payload, [
|
||||
'X-Signature' => $signature,
|
||||
]);
|
||||
|
||||
$response->assertStatus(202)
|
||||
->assertJson(['status' => 'accepted']);
|
||||
|
||||
Bus::assertDispatched(ProcessRevenueCatWebhook::class);
|
||||
}
|
||||
|
||||
public function test_webhook_rejects_invalid_signature(): void
|
||||
{
|
||||
config()->set('services.revenuecat.webhook', 'shared-secret');
|
||||
|
||||
Bus::fake();
|
||||
|
||||
$response = $this->postJson('/api/v1/webhooks/revenuecat', [
|
||||
'event' => ['app_user_id' => 'tenant-123'],
|
||||
], [
|
||||
'X-Signature' => 'invalid-signature',
|
||||
]);
|
||||
|
||||
$response->assertStatus(400)
|
||||
->assertJson(['error' => 'Invalid signature']);
|
||||
|
||||
Bus::assertNotDispatched(ProcessRevenueCatWebhook::class);
|
||||
}
|
||||
}
|
||||
46
tests/Feature/Tenant/EventCreditsTest.php
Normal file
46
tests/Feature/Tenant/EventCreditsTest.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Tenant;
|
||||
|
||||
use App\Models\EventType;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class EventCreditsTest extends TenantTestCase
|
||||
{
|
||||
public function test_event_creation_requires_credits(): void
|
||||
{
|
||||
$this->tenant->update(['event_credits_balance' => 0]);
|
||||
$eventType = EventType::factory()->create();
|
||||
|
||||
$payload = [
|
||||
'name' => 'Sample Event',
|
||||
'description' => 'Test description',
|
||||
'event_date' => Carbon::now()->addDays(3)->toDateString(),
|
||||
'event_type_id' => $eventType->id,
|
||||
];
|
||||
|
||||
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/events', $payload);
|
||||
|
||||
$response->assertStatus(402)
|
||||
->assertJson([
|
||||
'error' => 'Insufficient event credits. Please purchase more credits.',
|
||||
]);
|
||||
|
||||
$this->tenant->update(['event_credits_balance' => 2]);
|
||||
|
||||
$createResponse = $this->authenticatedRequest('POST', '/api/v1/tenant/events', $payload);
|
||||
|
||||
$createResponse->assertStatus(201)
|
||||
->assertJsonPath('message', 'Event created successfully')
|
||||
->assertJsonPath('balance', 1);
|
||||
|
||||
$this->tenant->refresh();
|
||||
$this->assertSame(1, $this->tenant->event_credits_balance);
|
||||
|
||||
$this->assertDatabaseHas('event_credits_ledger', [
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'delta' => -1,
|
||||
'reason' => 'event_create',
|
||||
]);
|
||||
}
|
||||
}
|
||||
62
tests/Unit/ProcessRevenueCatWebhookTest.php
Normal file
62
tests/Unit/ProcessRevenueCatWebhookTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Jobs\ProcessRevenueCatWebhook;
|
||||
use App\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProcessRevenueCatWebhookTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_job_creates_purchase_and_updates_balance(): void
|
||||
{
|
||||
config()->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);
|
||||
$this->assertSame($expiresAt->timestamp, $tenant->subscription_expires_at->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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user