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,131 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\World;
use App\Services\WebStories\WorldWebStoryGenerator;
use Illuminate\Console\Command;
use Illuminate\Validation\ValidationException;
final class GenerateWorldWebStoriesCommand extends Command
{
protected $signature = 'skinbase:webstories:generate
{world? : World ID or slug}
{--all : Generate stories in batch mode}
{--pages=7 : Number of pages to generate (5-10)}
{--limit=25 : Maximum worlds to process in batch mode}
{--force : Rebuild an existing story for the target world}
{--publish : Publish immediately after generation if validation passes}
{--dry-run : Preview generation without saving anything}';
protected $description = 'Generate standalone AMP Web Stories from Skinbase Worlds';
public function handle(WorldWebStoryGenerator $generator): int
{
$worldKey = $this->argument('world');
$force = (bool) $this->option('force');
$publish = (bool) $this->option('publish');
$dryRun = (bool) $this->option('dry-run');
$pages = max(5, min(10, (int) $this->option('pages')));
if ($worldKey !== null && trim((string) $worldKey) !== '') {
$world = $this->resolveWorld((string) $worldKey);
if (! $world instanceof World) {
$this->error(sprintf('World [%s] was not found.', (string) $worldKey));
return self::FAILURE;
}
return $this->generateOne($generator, $world, $pages, $force, $publish, $dryRun);
}
if (! (bool) $this->option('all')) {
$this->error('Provide a world ID/slug or pass --all for batch generation.');
return self::INVALID;
}
return $this->generateBatch($generator, $pages, $force, $publish, $dryRun, max(1, (int) $this->option('limit')));
}
private function generateOne(WorldWebStoryGenerator $generator, World $world, int $pages, bool $force, bool $publish, bool $dryRun): int
{
try {
$result = $generator->generateFromWorld($world, null, $pages, $force, $publish, $dryRun);
} catch (ValidationException $exception) {
foreach ($exception->errors() as $messages) {
foreach ($messages as $message) {
$this->error((string) $message);
}
}
return self::FAILURE;
}
$story = $result['story'];
$validation = $result['validation'];
$this->info(sprintf(
'%s story for world [%s] -> /web-stories/%s (%d pages)',
$result['created'] ? 'Created' : 'Updated',
(string) $world->slug,
(string) $story->slug,
(int) $validation['page_count'],
));
foreach ((array) $validation['warnings'] as $warning) {
$this->warn(' - ' . $warning);
}
foreach ((array) $validation['errors'] as $error) {
$this->error(' - ' . $error);
}
return $validation['valid'] || ! $publish ? self::SUCCESS : self::FAILURE;
}
private function generateBatch(WorldWebStoryGenerator $generator, int $pages, bool $force, bool $publish, bool $dryRun, int $limit): int
{
$processed = 0;
$created = 0;
$updated = 0;
$failed = 0;
$query = World::query()
->published()
->orderByDesc('published_at')
->orderByDesc('id');
if (! $force) {
$query->whereDoesntHave('webStories');
}
$query->limit($limit)->get()->each(function (World $world) use ($generator, $pages, $force, $publish, $dryRun, &$processed, &$created, &$updated, &$failed): void {
$processed++;
try {
$result = $generator->generateFromWorld($world, null, $pages, $force, $publish, $dryRun);
$result['created'] ? $created++ : $updated++;
$this->line(sprintf('[%d] %s -> %s', (int) $world->id, (string) $world->slug, (string) $result['story']->slug));
} catch (ValidationException $exception) {
$failed++;
$first = collect($exception->errors())->flatten()->first();
$this->error(sprintf('[%d] %s failed: %s', (int) $world->id, (string) $world->slug, (string) $first));
}
});
$this->info(sprintf('Done. processed=%d created=%d updated=%d failed=%d', $processed, $created, $updated, $failed));
return $failed === 0 ? self::SUCCESS : self::FAILURE;
}
private function resolveWorld(string $value): ?World
{
return World::query()
->when(is_numeric($value), fn ($query) => $query->where('id', (int) $value), fn ($query) => $query->where('slug', $value))
->first();
}
}