Build world campaigns rewards and recaps

This commit is contained in:
2026-05-01 11:44:41 +02:00
parent 28e7e46e13
commit 257b0dbef6
100 changed files with 11300 additions and 367 deletions

View File

@@ -0,0 +1,47 @@
<?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('worlds', function (Blueprint $table): void {
$table->boolean('is_active_campaign')->default(false)->after('is_featured');
$table->boolean('is_homepage_featured')->default(false)->after('is_active_campaign');
$table->integer('campaign_priority')->nullable()->after('is_homepage_featured');
$table->string('campaign_label', 120)->nullable()->after('badge_label');
$table->string('teaser_title', 180)->nullable()->after('summary');
$table->string('teaser_summary', 320)->nullable()->after('teaser_title');
$table->string('teaser_image_path', 2048)->nullable()->after('cover_path');
$table->timestamp('promotion_starts_at')->nullable()->after('ends_at');
$table->timestamp('promotion_ends_at')->nullable()->after('promotion_starts_at');
$table->index(['status', 'is_active_campaign', 'campaign_priority'], 'worlds_activation_idx');
$table->index(['is_homepage_featured', 'campaign_priority'], 'worlds_homepage_featured_idx');
$table->index(['promotion_starts_at', 'promotion_ends_at'], 'worlds_promotion_window_idx');
});
}
public function down(): void
{
Schema::table('worlds', function (Blueprint $table): void {
$table->dropIndex('worlds_activation_idx');
$table->dropIndex('worlds_homepage_featured_idx');
$table->dropIndex('worlds_promotion_window_idx');
$table->dropColumn([
'is_active_campaign',
'is_homepage_featured',
'campaign_priority',
'campaign_label',
'teaser_title',
'teaser_summary',
'teaser_image_path',
'promotion_starts_at',
'promotion_ends_at',
]);
});
}
};

View File

@@ -0,0 +1,37 @@
<?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_reward_grants', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('world_id')->constrained('worlds')->cascadeOnDelete();
$table->foreignId('artwork_id')->nullable()->constrained('artworks')->nullOnDelete();
$table->foreignId('world_submission_id')->nullable()->constrained('world_submissions')->nullOnDelete();
$table->foreignId('granted_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('reward_type', 32);
$table->string('grant_source', 16)->default('manual');
$table->text('note')->nullable();
$table->timestamp('granted_at')->nullable();
$table->timestamps();
$table->unique(['user_id', 'world_id', 'reward_type'], 'world_reward_grants_unique_reward');
$table->index(['world_id', 'reward_type']);
$table->index(['artwork_id']);
$table->index(['world_submission_id']);
});
}
public function down(): void
{
Schema::dropIfExists('world_reward_grants');
}
};

View File

@@ -0,0 +1,26 @@
<?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('worlds', function (Blueprint $table): void {
$table->dropIndex('worlds_recurrence_idx');
$table->unique(['recurrence_key', 'edition_year'], 'worlds_recurrence_year_unique');
});
}
public function down(): void
{
Schema::table('worlds', function (Blueprint $table): void {
$table->dropUnique('worlds_recurrence_year_unique');
$table->index(['recurrence_key', 'edition_year'], 'worlds_recurrence_idx');
});
}
};

View File

@@ -0,0 +1,25 @@
<?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('worlds', function (Blueprint $table): void {
$table->json('hidden_linked_challenge_artwork_ids_json')
->nullable();
});
}
public function down(): void
{
Schema::table('worlds', function (Blueprint $table): void {
$table->dropColumn('hidden_linked_challenge_artwork_ids_json');
});
}
};

View File

@@ -0,0 +1,53 @@
<?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
{
$afterColumn = Schema::hasColumn('worlds', 'recap_intro')
? 'recap_intro'
: (Schema::hasColumn('worlds', 'published_at') ? 'published_at' : null);
$hasRecapEditorNote = Schema::hasColumn('worlds', 'recap_editor_note');
$hasRecapCoverPath = Schema::hasColumn('worlds', 'recap_cover_path');
Schema::table('worlds', function (Blueprint $table) use ($afterColumn, $hasRecapEditorNote, $hasRecapCoverPath): void {
if (! $hasRecapEditorNote) {
$column = $table->text('recap_editor_note')->nullable();
if ($afterColumn !== null) {
$column->after($afterColumn);
}
}
if (! $hasRecapCoverPath) {
$column = $table->string('recap_cover_path', 2048)->nullable();
if (! $hasRecapEditorNote) {
$column->after('recap_editor_note');
} elseif ($afterColumn !== null) {
$column->after($afterColumn);
}
}
});
}
public function down(): void
{
Schema::table('worlds', function (Blueprint $table): void {
$columns = array_values(array_filter([
Schema::hasColumn('worlds', 'recap_editor_note') ? 'recap_editor_note' : null,
Schema::hasColumn('worlds', 'recap_cover_path') ? 'recap_cover_path' : null,
]));
if ($columns !== []) {
$table->dropColumn($columns);
}
});
}
};

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('world_editorial_suggestion_states', function (Blueprint $table): void {
$table->id();
$table->foreignId('world_id')->constrained()->cascadeOnDelete();
$table->string('related_type', 40);
$table->unsignedBigInteger('related_id');
$table->string('status', 40);
$table->string('section_key', 80)->nullable();
$table->foreignId('acted_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->unique(['world_id', 'related_type', 'related_id'], 'world_editorial_suggestion_states_unique');
$table->index(['world_id', 'status'], 'world_editorial_suggestion_states_status_idx');
});
}
public function down(): void
{
Schema::dropIfExists('world_editorial_suggestion_states');
}
};

View File

@@ -0,0 +1,38 @@
<?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('worlds', function (Blueprint $table): void {
$table->foreignId('linked_challenge_id')->nullable()->after('parent_world_id')->constrained('group_challenges')->nullOnDelete();
$table->boolean('show_linked_challenge_section')->default(true)->after('linked_challenge_id');
$table->boolean('show_linked_challenge_entries')->default(true)->after('show_linked_challenge_section');
$table->boolean('show_linked_challenge_winners')->default(true)->after('show_linked_challenge_entries');
$table->boolean('show_linked_challenge_finalists')->default(true)->after('show_linked_challenge_winners');
$table->boolean('auto_grant_challenge_world_rewards')->default(true)->after('show_linked_challenge_finalists');
$table->text('challenge_teaser_override')->nullable()->after('auto_grant_challenge_world_rewards');
});
}
public function down(): void
{
Schema::table('worlds', function (Blueprint $table): void {
$table->dropConstrainedForeignId('linked_challenge_id');
$table->dropColumn([
'show_linked_challenge_section',
'show_linked_challenge_entries',
'show_linked_challenge_winners',
'show_linked_challenge_finalists',
'auto_grant_challenge_world_rewards',
'challenge_teaser_override',
]);
});
}
};

View File

@@ -0,0 +1,36 @@
<?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('group_challenge_outcomes', function (Blueprint $table): void {
$table->id();
$table->foreignId('group_challenge_id')->constrained('group_challenges')->cascadeOnDelete();
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('outcome_type', 32);
$table->unsignedInteger('position')->nullable();
$table->unsignedInteger('sort_order')->default(0);
$table->string('title_override', 120)->nullable();
$table->text('note')->nullable();
$table->foreignId('awarded_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('awarded_at')->nullable();
$table->timestamps();
$table->unique(['group_challenge_id', 'artwork_id', 'outcome_type'], 'group_challenge_outcomes_unique');
$table->index(['group_challenge_id', 'outcome_type', 'sort_order'], 'group_challenge_outcomes_type_sort_idx');
});
}
public function down(): void
{
Schema::dropIfExists('group_challenge_outcomes');
}
};

View File

@@ -0,0 +1,46 @@
<?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('world_analytics_events', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('world_id')->index();
$table->string('event_type', 40)->index();
$table->string('world_slug', 160)->index();
$table->string('world_type', 40)->nullable()->index();
$table->string('recurrence_key', 120)->nullable()->index();
$table->unsignedSmallInteger('edition_year')->nullable()->index();
$table->string('section_key', 80)->nullable()->index();
$table->string('cta_key', 80)->nullable()->index();
$table->string('entity_type', 40)->nullable()->index();
$table->unsignedBigInteger('entity_id')->nullable()->index();
$table->string('entity_title', 180)->nullable();
$table->unsignedBigInteger('challenge_id')->nullable()->index();
$table->string('source_surface', 80)->nullable()->index();
$table->string('source_detail', 80)->nullable()->index();
$table->string('viewer_type', 16)->index();
$table->unsignedBigInteger('user_id')->nullable()->index();
$table->string('visitor_key', 64)->index();
$table->json('meta')->nullable();
$table->timestamp('occurred_at')->useCurrent()->index();
$table->index(['world_id', 'event_type', 'occurred_at'], 'world_analytics_world_event_occurred_idx');
$table->index(['world_id', 'source_surface', 'occurred_at'], 'world_analytics_world_source_occurred_idx');
$table->index(['world_id', 'section_key', 'occurred_at'], 'world_analytics_world_section_occurred_idx');
$table->foreign('world_id')->references('id')->on('worlds')->cascadeOnDelete();
$table->foreign('user_id')->references('id')->on('users')->nullOnDelete();
});
}
public function down(): void
{
Schema::dropIfExists('world_analytics_events');
}
};

View File

@@ -0,0 +1,41 @@
<?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('worlds', function (Blueprint $table): void {
$table->string('recap_status', 24)->default('draft')->after('published_at');
$table->string('recap_title', 180)->nullable()->after('recap_status');
$table->string('recap_summary', 320)->nullable()->after('recap_title');
$table->text('recap_intro')->nullable()->after('recap_summary');
$table->foreignId('recap_article_id')->nullable()->after('recap_intro')->constrained('news_articles')->nullOnDelete();
$table->json('recap_stats_snapshot_json')->nullable()->after('recap_article_id');
$table->timestamp('recap_published_at')->nullable()->after('recap_stats_snapshot_json');
$table->index(['recap_status', 'recap_published_at'], 'worlds_recap_status_idx');
});
}
public function down(): void
{
Schema::table('worlds', function (Blueprint $table): void {
$table->dropIndex('worlds_recap_status_idx');
$table->dropConstrainedForeignId('recap_article_id');
$table->dropColumn([
'recap_status',
'recap_title',
'recap_summary',
'recap_intro',
'recap_stats_snapshot_json',
'recap_published_at',
]);
});
}
};