feat(cms): import legacy article documents into document library
This commit is contained in:
390
app/Console/Commands/ImportArticleDocuments.php
Normal file
390
app/Console/Commands/ImportArticleDocuments.php
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\ArticleAttachment;
|
||||||
|
use App\Models\AuditLog;
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Models\DocumentCategory;
|
||||||
|
use App\Models\DocumentVersion;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
|
||||||
|
class ImportArticleDocuments extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'articles:import-documents
|
||||||
|
{--category-slug= : Fallback document category slug}
|
||||||
|
{--fallback-user-id=1 : Fallback user ID when source article has no valid creator}
|
||||||
|
{--include-unpublished : Also import draft/archived article documents}
|
||||||
|
{--mark-archived : Mark source articles as archived after successful import}
|
||||||
|
{--limit=0 : Limit number of articles to import (0 means no limit)}
|
||||||
|
{--dry-run : Preview actions without writing data}';
|
||||||
|
|
||||||
|
protected $description = 'Import legacy article documents into document library for unified CRUD management';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$dryRun = (bool) $this->option('dry-run');
|
||||||
|
$includeUnpublished = (bool) $this->option('include-unpublished');
|
||||||
|
$markArchived = (bool) $this->option('mark-archived');
|
||||||
|
$limit = max((int) $this->option('limit'), 0);
|
||||||
|
|
||||||
|
$fallbackUser = User::find((int) $this->option('fallback-user-id'));
|
||||||
|
if (! $fallbackUser) {
|
||||||
|
$this->error('Fallback user not found.');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fallbackCategory = $this->resolveFallbackCategory($this->option('category-slug'));
|
||||||
|
if (! $fallbackCategory) {
|
||||||
|
$this->error('No document category found. Please create one or pass --category-slug.');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = Article::query()
|
||||||
|
->with(['attachments', 'categories', 'creator'])
|
||||||
|
->where('content_type', Article::CONTENT_TYPE_DOCUMENT);
|
||||||
|
|
||||||
|
if (! $includeUnpublished) {
|
||||||
|
$query->where('status', Article::STATUS_PUBLISHED);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($limit > 0) {
|
||||||
|
$query->limit($limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
$articles = $query->orderBy('id')->get();
|
||||||
|
|
||||||
|
if ($articles->isEmpty()) {
|
||||||
|
$this->info('No article documents matched the import criteria.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Article document import started');
|
||||||
|
$this->line('Matched articles: '.$articles->count());
|
||||||
|
$this->line('Fallback category: '.$fallbackCategory->slug);
|
||||||
|
$this->line('Dry run: '.($dryRun ? 'yes' : 'no'));
|
||||||
|
$this->line('Mark source archived: '.($markArchived ? 'yes' : 'no'));
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
$imported = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$failed = 0;
|
||||||
|
$archivedSources = 0;
|
||||||
|
|
||||||
|
foreach ($articles as $article) {
|
||||||
|
try {
|
||||||
|
$result = $this->importSingleArticle(
|
||||||
|
article: $article,
|
||||||
|
fallbackCategory: $fallbackCategory,
|
||||||
|
fallbackUser: $fallbackUser,
|
||||||
|
dryRun: $dryRun,
|
||||||
|
markArchived: $markArchived
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result === 'imported') {
|
||||||
|
$imported++;
|
||||||
|
} elseif ($result === 'archived_source') {
|
||||||
|
$imported++;
|
||||||
|
$archivedSources++;
|
||||||
|
} else {
|
||||||
|
$skipped++;
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$failed++;
|
||||||
|
$this->error("✗ {$article->slug}: {$e->getMessage()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info('Import finished');
|
||||||
|
$this->line("Imported: {$imported}");
|
||||||
|
$this->line("Skipped: {$skipped}");
|
||||||
|
$this->line("Failed: {$failed}");
|
||||||
|
if ($markArchived) {
|
||||||
|
$this->line("Source articles archived: {$archivedSources}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $failed > 0 ? self::FAILURE : self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function importSingleArticle(
|
||||||
|
Article $article,
|
||||||
|
DocumentCategory $fallbackCategory,
|
||||||
|
User $fallbackUser,
|
||||||
|
bool $dryRun,
|
||||||
|
bool $markArchived
|
||||||
|
): string {
|
||||||
|
$publicUuid = $this->deterministicPublicUuid($article);
|
||||||
|
$existing = Document::where('public_uuid', $publicUuid)->first();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$this->warn("↷ {$article->slug}: already imported as document #{$existing->id}, skipping.");
|
||||||
|
|
||||||
|
return 'skipped';
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = $this->resolveCategoryForArticle($article, $fallbackCategory);
|
||||||
|
$actor = $article->creator ?: $fallbackUser;
|
||||||
|
$accessLevel = $this->normalizeAccessLevel($article->access_level);
|
||||||
|
$documentStatus = $article->status === Article::STATUS_PUBLISHED ? 'active' : 'archived';
|
||||||
|
$description = $this->resolveDescription($article);
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line("• {$article->slug}: would import to category={$category->slug}, access={$accessLevel}, attachments={$article->attachments->count()}");
|
||||||
|
|
||||||
|
return 'skipped';
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () use (
|
||||||
|
$article,
|
||||||
|
$publicUuid,
|
||||||
|
$category,
|
||||||
|
$actor,
|
||||||
|
$accessLevel,
|
||||||
|
$documentStatus,
|
||||||
|
$description,
|
||||||
|
$markArchived
|
||||||
|
): void {
|
||||||
|
$document = Document::create([
|
||||||
|
'document_category_id' => $category->id,
|
||||||
|
'title' => $article->title,
|
||||||
|
'document_number' => null,
|
||||||
|
'description' => $description,
|
||||||
|
'public_uuid' => $publicUuid,
|
||||||
|
'access_level' => $accessLevel,
|
||||||
|
'status' => $documentStatus,
|
||||||
|
'created_by_user_id' => $actor->id,
|
||||||
|
'last_updated_by_user_id' => $actor->id,
|
||||||
|
'version_count' => 0,
|
||||||
|
'archived_at' => $documentStatus === 'archived' ? now() : null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$versionIds = [];
|
||||||
|
$orderedAttachments = $article->attachments->sortBy([
|
||||||
|
fn (ArticleAttachment $attachment) => $attachment->created_at?->timestamp ?? 0,
|
||||||
|
fn (ArticleAttachment $attachment) => $attachment->id,
|
||||||
|
])->values();
|
||||||
|
|
||||||
|
foreach ($orderedAttachments as $attachment) {
|
||||||
|
$copied = $this->copyAttachmentToPrivate($article, $attachment);
|
||||||
|
if (! $copied) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$version = DocumentVersion::create([
|
||||||
|
'document_id' => $document->id,
|
||||||
|
'version_number' => $this->versionNumberFromIndex(count($versionIds)),
|
||||||
|
'version_notes' => $attachment->description ?: 'Imported from legacy article attachment',
|
||||||
|
'is_current' => false,
|
||||||
|
'file_path' => $copied['file_path'],
|
||||||
|
'original_filename' => $copied['original_filename'],
|
||||||
|
'mime_type' => $copied['mime_type'],
|
||||||
|
'file_size' => $copied['file_size'],
|
||||||
|
'file_hash' => $copied['file_hash'],
|
||||||
|
'uploaded_by_user_id' => $actor->id,
|
||||||
|
'uploaded_at' => $attachment->created_at ?? $article->updated_at ?? now(),
|
||||||
|
'created_at' => $attachment->created_at ?? $article->updated_at ?? now(),
|
||||||
|
'updated_at' => $attachment->updated_at ?? $attachment->created_at ?? now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$versionIds[] = $version->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($versionIds === []) {
|
||||||
|
$markdownVersion = $this->createMarkdownVersion($article, $document, $actor);
|
||||||
|
$versionIds[] = $markdownVersion->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentVersionId = end($versionIds) ?: null;
|
||||||
|
if (! $currentVersionId) {
|
||||||
|
throw new \RuntimeException("Failed to create version for article {$article->slug}");
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentVersion::where('document_id', $document->id)->update(['is_current' => false]);
|
||||||
|
DocumentVersion::where('id', $currentVersionId)->update(['is_current' => true]);
|
||||||
|
|
||||||
|
$document->forceFill([
|
||||||
|
'current_version_id' => $currentVersionId,
|
||||||
|
'version_count' => count($versionIds),
|
||||||
|
'created_at' => $article->published_at ?? $article->created_at ?? now(),
|
||||||
|
'updated_at' => $article->updated_at ?? $article->published_at ?? now(),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
if ($markArchived && $article->status !== Article::STATUS_ARCHIVED) {
|
||||||
|
$article->update([
|
||||||
|
'status' => Article::STATUS_ARCHIVED,
|
||||||
|
'archived_at' => now(),
|
||||||
|
'last_updated_by_user_id' => $actor->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuditLog::create([
|
||||||
|
'user_id' => $actor->id,
|
||||||
|
'action' => 'article.document_imported',
|
||||||
|
'description' => "Imported article document {$article->slug} -> document #{$document->id}",
|
||||||
|
'auditable_type' => Document::class,
|
||||||
|
'auditable_id' => $document->id,
|
||||||
|
'metadata' => [
|
||||||
|
'source_article_id' => $article->id,
|
||||||
|
'source_article_slug' => $article->slug,
|
||||||
|
'imported_version_count' => count($versionIds),
|
||||||
|
],
|
||||||
|
'ip_address' => '127.0.0.1',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->info("✓ {$article->slug}: imported.");
|
||||||
|
|
||||||
|
return $markArchived ? 'archived_source' : 'imported';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveFallbackCategory(?string $categorySlug): ?DocumentCategory
|
||||||
|
{
|
||||||
|
if ($categorySlug) {
|
||||||
|
return DocumentCategory::where('slug', $categorySlug)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
return DocumentCategory::where('slug', 'organization-public-disclosure')->first()
|
||||||
|
?: DocumentCategory::orderBy('sort_order')->orderBy('id')->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveCategoryForArticle(Article $article, DocumentCategory $fallbackCategory): DocumentCategory
|
||||||
|
{
|
||||||
|
foreach ($article->categories as $category) {
|
||||||
|
$matched = DocumentCategory::where('slug', $category->slug)->first();
|
||||||
|
if ($matched) {
|
||||||
|
return $matched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $fallbackCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deterministicPublicUuid(Article $article): string
|
||||||
|
{
|
||||||
|
return Uuid::uuid5(Uuid::NAMESPACE_URL, "legacy-article-document:{$article->id}:{$article->slug}")->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveDescription(Article $article): ?string
|
||||||
|
{
|
||||||
|
$description = $article->summary ?: $article->meta_description;
|
||||||
|
if ($description) {
|
||||||
|
return $description;
|
||||||
|
}
|
||||||
|
|
||||||
|
$plain = trim(strip_tags($article->content));
|
||||||
|
if ($plain === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Str::limit($plain, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeAccessLevel(?string $accessLevel): string
|
||||||
|
{
|
||||||
|
return in_array($accessLevel, ['public', 'members', 'admin', 'board'], true)
|
||||||
|
? $accessLevel
|
||||||
|
: 'members';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function versionNumberFromIndex(int $index): string
|
||||||
|
{
|
||||||
|
if ($index === 0) {
|
||||||
|
return '1.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '1.'.$index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{file_path:string, original_filename:string, mime_type:string, file_size:int, file_hash:string}|null
|
||||||
|
*/
|
||||||
|
private function copyAttachmentToPrivate(Article $article, ArticleAttachment $attachment): ?array
|
||||||
|
{
|
||||||
|
if (! Storage::disk('public')->exists($attachment->file_path)) {
|
||||||
|
$this->warn(" - {$article->slug}: attachment #{$attachment->id} file missing ({$attachment->file_path}), skipped.");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytes = Storage::disk('public')->get($attachment->file_path);
|
||||||
|
$extension = pathinfo($attachment->original_filename, PATHINFO_EXTENSION);
|
||||||
|
$safeName = $this->sanitizeFilename(pathinfo($attachment->original_filename, PATHINFO_FILENAME) ?: 'attachment');
|
||||||
|
$targetPath = 'documents/imported/articles/'.$article->id.'/'.Str::uuid().'-'.$safeName.($extension ? '.'.$extension : '');
|
||||||
|
|
||||||
|
Storage::disk('private')->put($targetPath, $bytes);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'file_path' => $targetPath,
|
||||||
|
'original_filename' => $attachment->original_filename,
|
||||||
|
'mime_type' => $attachment->mime_type ?: 'application/octet-stream',
|
||||||
|
'file_size' => strlen($bytes),
|
||||||
|
'file_hash' => hash('sha256', $bytes),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createMarkdownVersion(Article $article, Document $document, User $actor): DocumentVersion
|
||||||
|
{
|
||||||
|
$content = $this->buildMarkdownFromArticle($article);
|
||||||
|
$targetPath = 'documents/imported/articles/'.$article->id.'/'.Str::uuid().'-'.$article->slug.'.md';
|
||||||
|
Storage::disk('private')->put($targetPath, $content);
|
||||||
|
|
||||||
|
return DocumentVersion::create([
|
||||||
|
'document_id' => $document->id,
|
||||||
|
'version_number' => '1.0',
|
||||||
|
'version_notes' => 'Imported from legacy article content (no file attachments)',
|
||||||
|
'is_current' => false,
|
||||||
|
'file_path' => $targetPath,
|
||||||
|
'original_filename' => $article->slug.'.md',
|
||||||
|
'mime_type' => 'text/markdown',
|
||||||
|
'file_size' => strlen($content),
|
||||||
|
'file_hash' => hash('sha256', $content),
|
||||||
|
'uploaded_by_user_id' => $actor->id,
|
||||||
|
'uploaded_at' => $article->updated_at ?? $article->published_at ?? now(),
|
||||||
|
'created_at' => $article->created_at ?? now(),
|
||||||
|
'updated_at' => $article->updated_at ?? $article->created_at ?? now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildMarkdownFromArticle(Article $article): string
|
||||||
|
{
|
||||||
|
$sections = [
|
||||||
|
'# '.$article->title,
|
||||||
|
'',
|
||||||
|
'- Source article slug: `'.$article->slug.'`',
|
||||||
|
'- Source article id: `'.$article->id.'`',
|
||||||
|
'- Imported at: `'.now()->toIso8601String().'`',
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($article->summary) {
|
||||||
|
$sections[] = '## Summary';
|
||||||
|
$sections[] = '';
|
||||||
|
$sections[] = trim($article->summary);
|
||||||
|
$sections[] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sections[] = '## Content';
|
||||||
|
$sections[] = '';
|
||||||
|
$sections[] = trim($article->content);
|
||||||
|
$sections[] = '';
|
||||||
|
|
||||||
|
return implode("\n", $sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeFilename(string $name): string
|
||||||
|
{
|
||||||
|
$sanitized = preg_replace('/[^A-Za-z0-9._-]+/', '-', $name) ?: 'file';
|
||||||
|
|
||||||
|
return trim($sanitized, '-');
|
||||||
|
}
|
||||||
|
}
|
||||||
130
tests/Feature/Cms/ImportArticleDocumentsCommandTest.php
Normal file
130
tests/Feature/Cms/ImportArticleDocumentsCommandTest.php
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Cms;
|
||||||
|
|
||||||
|
use App\Models\Article;
|
||||||
|
use App\Models\ArticleAttachment;
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Models\DocumentCategory;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ImportArticleDocumentsCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_it_imports_article_documents_and_is_idempotent(): void
|
||||||
|
{
|
||||||
|
Storage::fake('public');
|
||||||
|
Storage::fake('private');
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
DocumentCategory::factory()->create([
|
||||||
|
'slug' => 'organization-public-disclosure',
|
||||||
|
'name' => '組織公開資訊',
|
||||||
|
'default_access_level' => 'public',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$articleWithAttachment = Article::factory()->create([
|
||||||
|
'title' => '舊版章程 PDF',
|
||||||
|
'slug' => 'legacy-charter',
|
||||||
|
'content_type' => Article::CONTENT_TYPE_DOCUMENT,
|
||||||
|
'status' => Article::STATUS_PUBLISHED,
|
||||||
|
'access_level' => Article::ACCESS_LEVEL_PUBLIC,
|
||||||
|
'summary' => '舊版章程摘要',
|
||||||
|
'content' => '舊版章程內容',
|
||||||
|
'created_by_user_id' => $user->id,
|
||||||
|
'last_updated_by_user_id' => $user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Storage::disk('public')->put('articles/attachments/legacy-charter.pdf', '%PDF-1.4 test');
|
||||||
|
ArticleAttachment::create([
|
||||||
|
'article_id' => $articleWithAttachment->id,
|
||||||
|
'file_path' => 'articles/attachments/legacy-charter.pdf',
|
||||||
|
'original_filename' => 'legacy-charter.pdf',
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
'file_size' => 13,
|
||||||
|
'description' => '舊版附件',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$articleWithoutAttachment = Article::factory()->create([
|
||||||
|
'title' => '純文字福利資源',
|
||||||
|
'slug' => 'legacy-welfare-links',
|
||||||
|
'content_type' => Article::CONTENT_TYPE_DOCUMENT,
|
||||||
|
'status' => Article::STATUS_PUBLISHED,
|
||||||
|
'access_level' => Article::ACCESS_LEVEL_PUBLIC,
|
||||||
|
'summary' => '福利連結摘要',
|
||||||
|
'content' => "第一行\n第二行",
|
||||||
|
'created_by_user_id' => $user->id,
|
||||||
|
'last_updated_by_user_id' => $user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->artisan('articles:import-documents', [
|
||||||
|
'--fallback-user-id' => $user->id,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('documents', 2);
|
||||||
|
|
||||||
|
$importedFromAttachment = Document::where('title', $articleWithAttachment->title)->firstOrFail();
|
||||||
|
$this->assertSame('public', $importedFromAttachment->access_level);
|
||||||
|
$this->assertSame('active', $importedFromAttachment->status);
|
||||||
|
$this->assertSame(1, $importedFromAttachment->version_count);
|
||||||
|
$this->assertNotNull($importedFromAttachment->current_version_id);
|
||||||
|
$this->assertTrue($importedFromAttachment->currentVersion()->exists());
|
||||||
|
$this->assertSame('legacy-charter.pdf', $importedFromAttachment->currentVersion->original_filename);
|
||||||
|
$this->assertTrue(Storage::disk('private')->exists($importedFromAttachment->currentVersion->file_path));
|
||||||
|
|
||||||
|
$importedFromMarkdown = Document::where('title', $articleWithoutAttachment->title)->firstOrFail();
|
||||||
|
$this->assertSame(1, $importedFromMarkdown->version_count);
|
||||||
|
$this->assertNotNull($importedFromMarkdown->current_version_id);
|
||||||
|
$this->assertSame('text/markdown', $importedFromMarkdown->currentVersion->mime_type);
|
||||||
|
$this->assertSame('legacy-welfare-links.md', $importedFromMarkdown->currentVersion->original_filename);
|
||||||
|
$this->assertTrue(Storage::disk('private')->exists($importedFromMarkdown->currentVersion->file_path));
|
||||||
|
|
||||||
|
$markdown = Storage::disk('private')->get($importedFromMarkdown->currentVersion->file_path);
|
||||||
|
$this->assertStringContainsString('# 純文字福利資源', $markdown);
|
||||||
|
$this->assertStringContainsString('Source article slug: `legacy-welfare-links`', $markdown);
|
||||||
|
|
||||||
|
// Idempotency check
|
||||||
|
$this->artisan('articles:import-documents', [
|
||||||
|
'--fallback-user-id' => $user->id,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
|
||||||
|
$this->assertDatabaseCount('documents', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_can_archive_source_articles_after_import(): void
|
||||||
|
{
|
||||||
|
Storage::fake('public');
|
||||||
|
Storage::fake('private');
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
DocumentCategory::factory()->create([
|
||||||
|
'slug' => 'organization-public-disclosure',
|
||||||
|
'name' => '組織公開資訊',
|
||||||
|
'default_access_level' => 'public',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$article = Article::factory()->create([
|
||||||
|
'title' => '待封存來源文章',
|
||||||
|
'slug' => 'legacy-archive-me',
|
||||||
|
'content_type' => Article::CONTENT_TYPE_DOCUMENT,
|
||||||
|
'status' => Article::STATUS_PUBLISHED,
|
||||||
|
'access_level' => Article::ACCESS_LEVEL_PUBLIC,
|
||||||
|
'created_by_user_id' => $user->id,
|
||||||
|
'last_updated_by_user_id' => $user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->artisan('articles:import-documents', [
|
||||||
|
'--fallback-user-id' => $user->id,
|
||||||
|
'--mark-archived' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
|
||||||
|
$article->refresh();
|
||||||
|
$this->assertSame(Article::STATUS_ARCHIVED, $article->status);
|
||||||
|
$this->assertNotNull($article->archived_at);
|
||||||
|
$this->assertDatabaseCount('documents', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user