feat: revenue cat
CI / 🧪 Tests Laravel (push) Successful in 2m24s
CI / 🐳 Build & Push Image (push) Successful in 1m13s

This commit is contained in:
2026-08-12 16:00:11 +02:00
parent 31633cfd2c
commit 29e85589b0
55 changed files with 703 additions and 2363 deletions
@@ -0,0 +1,141 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->string('subscription_product_id')->nullable()->index();
$table->string('subscription_store')->nullable();
$table->timestampTz('subscription_expires_at')->nullable()->index();
$table->boolean('subscription_is_trial')->default(false);
});
Schema::create('revenue_cat_webhook_events', function (Blueprint $table): void {
$table->ulid('id')->primary();
$table->string('event_id')->unique();
$table->string('type')->index();
$table->string('app_user_id')->nullable()->index();
$table->string('product_id')->nullable();
$table->json('entitlement_ids')->nullable();
$table->string('environment')->nullable();
$table->json('payload');
$table->timestampTz('processed_at')->nullable();
$table->timestamps();
});
Schema::dropIfExists('subscription_items');
Schema::dropIfExists('subscriptions');
Schema::dropIfExists('payment_transactions');
Schema::dropIfExists('billing_products');
Schema::table('users', function (Blueprint $table): void {
$table->dropIndex(['stripe_id']);
$table->dropColumn([
'stripe_id',
'pm_type',
'pm_last_four',
'trial_ends_at',
]);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('revenue_cat_webhook_events');
Schema::table('users', function (Blueprint $table): void {
$table->dropIndex(['subscription_product_id']);
$table->dropIndex(['subscription_expires_at']);
$table->dropColumn([
'subscription_product_id',
'subscription_store',
'subscription_expires_at',
'subscription_is_trial',
]);
$table->string('stripe_id')->nullable()->index();
$table->string('pm_type')->nullable();
$table->string('pm_last_four', 4)->nullable();
$table->timestamp('trial_ends_at')->nullable();
});
Schema::create('billing_products', function (Blueprint $table): void {
$table->ulid('id')->primary();
$table->string('name');
$table->text('description')->nullable();
$table->string('purpose', 32)->unique();
$table->unsignedInteger('amount');
$table->string('currency', 3)->default('eur');
$table->string('stripe_product_id')->nullable()->index();
$table->string('stripe_price_id')->nullable()->unique();
$table->boolean('is_active')->default(true)->index();
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
$table->index(['purpose', 'is_active', 'sort_order']);
});
Schema::create('payment_transactions', function (Blueprint $table): void {
$table->ulid('id')->primary();
$table->foreignUlid('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->foreignUlid('billing_product_id')->nullable()->constrained('billing_products')->nullOnDelete();
$table->string('type', 64);
$table->string('status', 64);
$table->string('stripe_event_id')->nullable()->index();
$table->string('stripe_event_type')->nullable()->index();
$table->string('stripe_customer_id')->nullable()->index();
$table->string('stripe_checkout_session_id')->nullable()->index();
$table->string('stripe_invoice_id')->nullable()->index();
$table->string('stripe_payment_intent_id')->nullable()->index();
$table->string('stripe_subscription_id')->nullable()->index();
$table->string('stripe_price_id')->nullable()->index();
$table->unsignedInteger('amount')->nullable();
$table->string('currency', 3)->nullable();
$table->string('invoice_url', 2048)->nullable();
$table->string('invoice_pdf_url', 2048)->nullable();
$table->timestamp('invoice_email_sent_at')->nullable();
$table->text('error_message')->nullable();
$table->json('payload')->nullable();
$table->timestamp('processed_at')->nullable();
$table->timestamps();
$table->index(['status', 'created_at']);
$table->index(['type', 'status', 'created_at']);
});
Schema::create('subscriptions', function (Blueprint $table): void {
$table->id();
$table->foreignUlid('user_id');
$table->string('type');
$table->string('stripe_id')->unique();
$table->string('stripe_status');
$table->string('stripe_price')->nullable();
$table->integer('quantity')->nullable();
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'stripe_status']);
});
Schema::create('subscription_items', function (Blueprint $table): void {
$table->id();
$table->foreignId('subscription_id');
$table->string('stripe_id')->unique();
$table->string('stripe_product');
$table->string('stripe_price');
$table->string('meter_id')->nullable();
$table->integer('quantity')->nullable();
$table->string('meter_event_name')->nullable();
$table->timestamps();
$table->index(['subscription_id', 'stripe_price']);
});
}
};
+3 -3
View File
@@ -64,7 +64,7 @@ class LegalDocumentSeeder extends Seeder
[
'title' => 'Paiements et abonnements',
'body' => [
'Stripe traite les paiements, les abonnements, les factures et les informations de moyen de paiement. Bowli conserve uniquement les identifiants et statuts nécessaires au suivi de la transaction et de tes droits daccès.',
'Apple ou Google traite le paiement selon ton appareil. RevenueCat synchronise les achats, abonnements et droits daccès sans recevoir les informations complètes de ton moyen de paiement.',
],
],
[
@@ -118,8 +118,8 @@ class LegalDocumentSeeder extends Seeder
[
'title' => 'Essai, abonnement et certification',
'body' => [
'La publication de plats reste accessible sans abonnement. Lanalyse automatique des photos est accessible pendant la période dessai indiquée dans lapplication, puis avec lunique formule dabonnement active.',
'La certification est un achat unique distinct de labonnement. Le paiement donne le droit de soumettre une demande, mais le badge nest accordé quaprès vérification dune pièce didentité valide. En cas de refus, une nouvelle pièce peut être envoyée sans nouvel achat.',
'La publication de plats reste accessible sans abonnement. Lanalyse automatique des photos est accessible avec un abonnement mensuel ou annuel. Une éventuelle période dessai concerne uniquement ces abonnements et sa durée est affichée avant validation par le store.',
'La certification ne comporte aucune période dessai. Cest un achat unique distinct de labonnement : le paiement donne le droit de soumettre une demande, mais le badge nest accordé quaprès vérification dune pièce didentité valide. En cas de refus, une nouvelle pièce peut être envoyée sans nouvel achat.',
],
],
[