feat: stripe

This commit is contained in:
2026-05-27 10:15:21 +02:00
parent 79e020ffe3
commit a99d027771
29 changed files with 1160 additions and 4 deletions
+51
View File
@@ -0,0 +1,51 @@
<?php
use App\Enums\CreditProductType;
use App\Models\CreditProduct;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
uses(RefreshDatabase::class);
it('lists active credit products ordered for the mobile app', function () {
Sanctum::actingAs(User::factory()->create());
CreditProduct::create([
'name' => 'Pack 10',
'type' => CreditProductType::ONE_TIME,
'credits' => 10,
'amount' => 499,
'currency' => 'eur',
'stripe_price_id' => 'price_pack_10',
'sort_order' => 20,
]);
CreditProduct::create([
'name' => 'Mensuel 30',
'type' => CreditProductType::MONTHLY,
'credits' => 30,
'amount' => 999,
'currency' => 'eur',
'stripe_price_id' => 'price_monthly_30',
'sort_order' => 10,
]);
CreditProduct::create([
'name' => 'Inactive',
'type' => CreditProductType::ONE_TIME,
'credits' => 5,
'amount' => 299,
'currency' => 'eur',
'stripe_price_id' => 'price_inactive',
'is_active' => false,
]);
$this
->getJson('/api/billing/products')
->assertOk()
->assertJsonCount(2, 'data')
->assertJsonPath('data.0.name', 'Mensuel 30')
->assertJsonPath('data.0.type', CreditProductType::MONTHLY->value)
->assertJsonPath('data.0.credits', 30)
->assertJsonPath('data.0.formattedAmount', '9,99 EUR')
->assertJsonPath('data.1.name', 'Pack 10');
});
+40
View File
@@ -3,9 +3,11 @@
use App\Ai\Agents\MealImageAnalyzer;
use App\Enums\AiUsageStatus;
use App\Enums\AiUsageType;
use App\Enums\CreditLedgerEntryType;
use App\Enums\IngredientUnit;
use App\Enums\MealPostType;
use App\Models\AiUsage;
use App\Models\CreditLedgerEntry;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
@@ -64,6 +66,7 @@ it('analyzes a meal image into a draft', function () {
$response
->assertOk()
->assertJsonPath('credits.balance', 2)
->assertJsonPath('data.title', 'Bowl saumon avocat')
->assertJsonPath('data.type', MealPostType::LUNCH->value)
->assertJsonPath('data.calories', 612)
@@ -106,6 +109,15 @@ it('analyzes a meal image into a draft', function () {
],
],
]);
$user->refresh();
$ledgerEntry = CreditLedgerEntry::query()->sole();
expect($user->ai_credits_balance)->toBe(2)
->and($ledgerEntry->user_id)->toBe($user->id)
->and($ledgerEntry->type)->toBe(CreditLedgerEntryType::USAGE)
->and($ledgerEntry->credits)->toBe(-1)
->and($ledgerEntry->balance_after)->toBe(2);
});
it('records a failed meal image analysis attempt', function () {
@@ -139,6 +151,34 @@ it('records a failed meal image analysis attempt', function () {
])
->and($usage->output)->toBeNull()
->and($usage->error_message)->toContain('Provider unavailable');
expect($user->fresh()->ai_credits_balance)->toBe(3);
});
it('requires available credits to analyze a meal image', function () {
$user = User::factory()->create([
'ai_credits_balance' => 0,
]);
Sanctum::actingAs($user);
MealImageAnalyzer::fake([
[
'title' => 'Bowl',
],
]);
$this
->withHeader('Accept', 'application/json')
->post('/api/meal-posts/analyze-image', [
'image' => fakeMealAnalysisImage(),
])
->assertStatus(402)
->assertJsonPath('message', __('api.meal_image_analysis.insufficient_credits'))
->assertJsonPath('credits.balance', 0);
expect(AiUsage::query()->count())->toBe(0)
->and(CreditLedgerEntry::query()->count())->toBe(0);
});
it('requires authentication to analyze a meal image', function () {