Implement academy analytics, billing, and web stories updates

This commit is contained in:
2026-05-26 07:27:29 +02:00
parent 456c3d6bb0
commit 0b33a1b074
177 changed files with 27360 additions and 2685 deletions

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('world_web_stories', function (Blueprint $table): void {
$table->id();
$table->foreignId('world_id')->nullable()->constrained('worlds')->nullOnDelete();
$table->string('slug')->unique();
$table->string('title');
$table->string('subtitle')->nullable();
$table->text('excerpt')->nullable();
$table->text('description')->nullable();
$table->string('seo_title')->nullable();
$table->text('seo_description')->nullable();
$table->string('poster_portrait_path')->nullable();
$table->string('poster_square_path')->nullable();
$table->string('publisher_logo_path')->nullable();
$table->enum('status', ['draft', 'published', 'archived'])->default('draft');
$table->boolean('featured')->default(false);
$table->boolean('active')->default(true);
$table->boolean('noindex')->default(false);
$table->timestamp('published_at')->nullable();
$table->timestamp('starts_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->softDeletes();
$table->index('world_id');
$table->index('slug');
$table->index(['status', 'active', 'published_at']);
$table->index(['featured', 'status', 'active']);
});
}
public function down(): void
{
Schema::dropIfExists('world_web_stories');
}
};

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('world_web_story_pages', function (Blueprint $table): void {
$table->id();
$table->foreignId('story_id')->constrained('world_web_stories')->cascadeOnDelete();
$table->foreignId('artwork_id')->nullable()->constrained('artworks')->nullOnDelete();
$table->unsignedInteger('position');
$table->enum('layout', ['cover', 'artwork', 'creator', 'mood', 'collection', 'cta']);
$table->enum('background_type', ['image', 'video', 'gradient']);
$table->string('background_path')->nullable();
$table->string('background_mobile_path')->nullable();
$table->string('headline')->nullable();
$table->text('body')->nullable();
$table->string('cta_label')->nullable();
$table->string('cta_url')->nullable();
$table->text('alt_text')->nullable();
$table->string('caption')->nullable();
$table->string('credit_text')->nullable();
$table->enum('text_position', ['top', 'center', 'bottom'])->default('bottom');
$table->unsignedTinyInteger('overlay_strength')->default(35);
$table->enum('animation', ['fade-in', 'fly-in-bottom', 'pulse', 'pan-left', 'pan-right'])->nullable();
$table->boolean('active')->default(true);
$table->timestamps();
$table->softDeletes();
$table->index(['story_id', 'position']);
$table->index(['story_id', 'active']);
$table->index('artwork_id');
});
}
public function down(): void
{
Schema::dropIfExists('world_web_story_pages');
}
};

View File

@@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('academy_events', function (Blueprint $table): void {
$table->id();
$table->string('event_type')->index();
$table->string('content_type')->nullable()->index();
$table->unsignedBigInteger('content_id')->nullable()->index();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('visitor_id', 120)->nullable()->index();
$table->string('session_id', 120)->nullable()->index();
$table->text('url')->nullable();
$table->string('route_name')->nullable()->index();
$table->text('referrer')->nullable();
$table->string('utm_source')->nullable()->index();
$table->string('utm_medium')->nullable()->index();
$table->string('utm_campaign')->nullable()->index();
$table->string('device_type')->nullable()->index();
$table->string('browser')->nullable();
$table->string('platform')->nullable();
$table->string('country_code', 8)->nullable()->index();
$table->boolean('is_logged_in')->default(false)->index();
$table->boolean('is_subscriber')->default(false)->index();
$table->boolean('is_admin')->default(false)->index();
$table->boolean('is_bot')->default(false)->index();
$table->boolean('is_crawler')->default(false)->index();
$table->boolean('is_suspicious')->default(false)->index();
$table->json('metadata')->nullable();
$table->timestamp('occurred_at')->index();
$table->timestamps();
$table->index(['event_type', 'occurred_at']);
$table->index(['content_type', 'content_id', 'occurred_at'], 'academy_events_content_occurred_idx');
$table->index(['user_id', 'occurred_at']);
$table->index(['visitor_id', 'occurred_at']);
$table->index(['is_bot', 'is_admin', 'occurred_at'], 'academy_events_bot_admin_occurred_idx');
});
}
public function down(): void
{
Schema::dropIfExists('academy_events');
}
};

View File

@@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('academy_content_metrics_daily', function (Blueprint $table): void {
$table->id();
$table->date('date')->index();
$table->string('content_type')->index();
$table->unsignedBigInteger('content_id')->nullable()->index();
$table->unsignedInteger('views')->default(0);
$table->unsignedInteger('unique_visitors')->default(0);
$table->unsignedInteger('guest_views')->default(0);
$table->unsignedInteger('user_views')->default(0);
$table->unsignedInteger('subscriber_views')->default(0);
$table->unsignedInteger('engaged_views')->default(0);
$table->unsignedInteger('scroll_50')->default(0);
$table->unsignedInteger('scroll_75')->default(0);
$table->unsignedInteger('scroll_100')->default(0);
$table->unsignedInteger('likes')->default(0);
$table->unsignedInteger('saves')->default(0);
$table->unsignedInteger('prompt_copies')->default(0);
$table->unsignedInteger('negative_prompt_copies')->default(0);
$table->unsignedInteger('starts')->default(0);
$table->unsignedInteger('completions')->default(0);
$table->unsignedInteger('upgrade_clicks')->default(0);
$table->unsignedInteger('premium_preview_views')->default(0);
$table->unsignedInteger('search_impressions')->default(0);
$table->unsignedInteger('search_clicks')->default(0);
$table->unsignedInteger('bounce_count')->default(0);
$table->unsignedInteger('avg_engaged_seconds')->nullable();
$table->decimal('popularity_score', 12, 2)->default(0);
$table->decimal('conversion_score', 12, 2)->default(0);
$table->timestamps();
$table->unique(['date', 'content_type', 'content_id'], 'academy_metrics_daily_unique');
});
}
public function down(): void
{
Schema::dropIfExists('academy_content_metrics_daily');
}
};

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('academy_likes', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('content_type')->index();
$table->unsignedBigInteger('content_id')->index();
$table->timestamps();
$table->unique(['user_id', 'content_type', 'content_id'], 'academy_likes_unique');
});
}
public function down(): void
{
Schema::dropIfExists('academy_likes');
}
};

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('academy_saves', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('content_type')->index();
$table->unsignedBigInteger('content_id')->index();
$table->timestamps();
$table->unique(['user_id', 'content_type', 'content_id'], 'academy_saves_unique');
});
}
public function down(): void
{
Schema::dropIfExists('academy_saves');
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('academy_user_progress', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('course_id')->nullable()->constrained('academy_courses')->nullOnDelete();
$table->foreignId('lesson_id')->nullable()->constrained('academy_lessons')->nullOnDelete();
$table->string('status')->index();
$table->unsignedTinyInteger('progress_percent')->default(0);
$table->timestamp('started_at')->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamp('last_seen_at')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->unique(['user_id', 'course_id', 'lesson_id'], 'academy_user_progress_unique');
});
}
public function down(): void
{
Schema::dropIfExists('academy_user_progress');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('academy_search_logs', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('visitor_id', 120)->nullable()->index();
$table->string('query')->index();
$table->string('normalized_query')->index();
$table->unsignedInteger('results_count')->default(0)->index();
$table->string('clicked_content_type')->nullable()->index();
$table->unsignedBigInteger('clicked_content_id')->nullable()->index();
$table->json('filters')->nullable();
$table->boolean('is_logged_in')->default(false)->index();
$table->boolean('is_subscriber')->default(false)->index();
$table->boolean('is_bot')->default(false)->index();
$table->timestamps();
$table->index(['normalized_query', 'created_at']);
$table->index(['results_count', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('academy_search_logs');
}
};

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('academy_prompt_templates', function (Blueprint $table): void {
if (! Schema::hasColumn('academy_prompt_templates', 'documentation')) {
$table->json('documentation')->nullable()->after('workflow_notes');
}
if (! Schema::hasColumn('academy_prompt_templates', 'placeholders')) {
$table->json('placeholders')->nullable()->after('documentation');
}
if (! Schema::hasColumn('academy_prompt_templates', 'helper_prompts')) {
$table->json('helper_prompts')->nullable()->after('placeholders');
}
if (! Schema::hasColumn('academy_prompt_templates', 'prompt_variants')) {
$table->json('prompt_variants')->nullable()->after('helper_prompts');
}
});
}
public function down(): void
{
Schema::table('academy_prompt_templates', function (Blueprint $table): void {
$columns = array_values(array_filter([
Schema::hasColumn('academy_prompt_templates', 'documentation') ? 'documentation' : null,
Schema::hasColumn('academy_prompt_templates', 'placeholders') ? 'placeholders' : null,
Schema::hasColumn('academy_prompt_templates', 'helper_prompts') ? 'helper_prompts' : null,
Schema::hasColumn('academy_prompt_templates', 'prompt_variants') ? 'prompt_variants' : null,
]));
if ($columns !== []) {
$table->dropColumn($columns);
}
});
}
};

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('comment_reactions', function (Blueprint $table): void {
$table->index(['created_at', 'id'], 'idx_comment_reactions_created_at');
});
Schema::table('user_mentions', function (Blueprint $table): void {
$table->index(['created_at', 'id'], 'idx_user_mentions_created_at');
});
}
public function down(): void
{
Schema::table('user_mentions', function (Blueprint $table): void {
$table->dropIndex('idx_user_mentions_created_at');
});
Schema::table('comment_reactions', function (Blueprint $table): void {
$table->dropIndex('idx_comment_reactions_created_at');
});
}
};

View File

@@ -0,0 +1,40 @@
<?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) {
$table->string('stripe_id')->nullable()->index();
$table->string('pm_type')->nullable();
$table->string('pm_last_four', 4)->nullable();
$table->timestamp('trial_ends_at')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropIndex([
'stripe_id',
]);
$table->dropColumn([
'stripe_id',
'pm_type',
'pm_last_four',
'trial_ends_at',
]);
});
}
};

View File

@@ -0,0 +1,37 @@
<?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::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->foreignId('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']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('subscriptions');
}
};

View File

@@ -0,0 +1,34 @@
<?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::create('subscription_items', function (Blueprint $table) {
$table->id();
$table->foreignId('subscription_id');
$table->string('stripe_id')->unique();
$table->string('stripe_product');
$table->string('stripe_price');
$table->integer('quantity')->nullable();
$table->timestamps();
$table->index(['subscription_id', 'stripe_price']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('subscription_items');
}
};

View File

@@ -0,0 +1,28 @@
<?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('subscription_items', function (Blueprint $table) {
$table->string('meter_id')->nullable()->after('stripe_price');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('subscription_items', function (Blueprint $table) {
$table->dropColumn('meter_id');
});
}
};

View File

@@ -0,0 +1,28 @@
<?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('subscription_items', function (Blueprint $table) {
$table->string('meter_event_name')->nullable()->after('quantity');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('subscription_items', function (Blueprint $table) {
$table->dropColumn('meter_event_name');
});
}
};

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('academy_billing_events', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('stripe_event_id')->nullable()->unique();
$table->string('stripe_customer_id')->nullable()->index();
$table->string('stripe_subscription_id')->nullable()->index();
$table->string('event_type');
$table->string('academy_tier')->nullable();
$table->string('academy_plan')->nullable();
$table->json('payload_summary')->nullable();
$table->timestamp('processed_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('academy_billing_events');
}
};