feat(cms): expose public document api and trigger site revalidation
This commit is contained in:
89
app/Http/Controllers/Api/PublicDocumentController.php
Normal file
89
app/Http/Controllers/Api/PublicDocumentController.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Resources\PublicDocumentCollectionResource;
|
||||||
|
use App\Http\Resources\PublicDocumentResource;
|
||||||
|
use App\Models\Document;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class PublicDocumentController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* List public documents for external site consumption.
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = Document::query()
|
||||||
|
->with(['category', 'currentVersion.uploadedBy'])
|
||||||
|
->where('status', 'active')
|
||||||
|
->where('access_level', 'public')
|
||||||
|
->whereNotNull('current_version_id')
|
||||||
|
->orderByDesc('updated_at');
|
||||||
|
|
||||||
|
if ($request->filled('search')) {
|
||||||
|
$search = trim((string) $request->input('search'));
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('title', 'like', "%{$search}%")
|
||||||
|
->orWhere('description', 'like', "%{$search}%")
|
||||||
|
->orWhere('document_number', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('category')) {
|
||||||
|
$category = (string) $request->input('category');
|
||||||
|
|
||||||
|
if (ctype_digit($category)) {
|
||||||
|
$query->where('document_category_id', (int) $category);
|
||||||
|
} else {
|
||||||
|
$query->whereHas('category', function ($q) use ($category) {
|
||||||
|
$q->where('slug', $category);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$perPage = min(max($request->integer('per_page', 100), 1), 500);
|
||||||
|
|
||||||
|
return PublicDocumentCollectionResource::collection(
|
||||||
|
$query->paginate($perPage)->withQueryString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a single public document with version history.
|
||||||
|
*/
|
||||||
|
public function show(string $uuid)
|
||||||
|
{
|
||||||
|
$document = Document::query()
|
||||||
|
->with([
|
||||||
|
'category',
|
||||||
|
'currentVersion.uploadedBy',
|
||||||
|
'versions.uploadedBy',
|
||||||
|
'versions.document',
|
||||||
|
'createdBy',
|
||||||
|
'lastUpdatedBy',
|
||||||
|
])
|
||||||
|
->where('public_uuid', $uuid)
|
||||||
|
->where('status', 'active')
|
||||||
|
->where('access_level', 'public')
|
||||||
|
->whereNotNull('current_version_id')
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$related = Document::query()
|
||||||
|
->with(['category', 'currentVersion.uploadedBy'])
|
||||||
|
->where('status', 'active')
|
||||||
|
->where('access_level', 'public')
|
||||||
|
->whereNotNull('current_version_id')
|
||||||
|
->where('id', '!=', $document->id)
|
||||||
|
->where('document_category_id', $document->document_category_id)
|
||||||
|
->orderByDesc('updated_at')
|
||||||
|
->limit(4)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => new PublicDocumentResource($document),
|
||||||
|
'related' => PublicDocumentCollectionResource::collection($related),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
app/Http/Resources/PublicDocumentCollectionResource.php
Normal file
71
app/Http/Resources/PublicDocumentCollectionResource.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class PublicDocumentCollectionResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'slug' => $this->public_uuid,
|
||||||
|
'public_uuid' => $this->public_uuid,
|
||||||
|
'title' => $this->title,
|
||||||
|
'document_number' => $this->document_number,
|
||||||
|
'summary' => $this->description,
|
||||||
|
'description' => $this->description,
|
||||||
|
'status' => $this->status,
|
||||||
|
'status_label' => $this->getStatusLabel(),
|
||||||
|
'access_level' => $this->access_level,
|
||||||
|
'access_level_label' => $this->getAccessLevelLabel(),
|
||||||
|
'published_at' => $this->created_at?->toIso8601String(),
|
||||||
|
'updated_at' => $this->updated_at?->toIso8601String(),
|
||||||
|
'expires_at' => $this->expires_at?->toDateString(),
|
||||||
|
'version_count' => $this->version_count,
|
||||||
|
'category' => $this->whenLoaded('category', function () {
|
||||||
|
return [
|
||||||
|
'id' => $this->category->id,
|
||||||
|
'name' => $this->category->name,
|
||||||
|
'slug' => $this->category->slug,
|
||||||
|
'icon' => $this->category->icon,
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
'current_version' => $this->whenLoaded('currentVersion', function () {
|
||||||
|
if (! $this->currentVersion) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $this->currentVersion->id,
|
||||||
|
'version_number' => $this->currentVersion->version_number,
|
||||||
|
'version_notes' => $this->currentVersion->version_notes,
|
||||||
|
'is_current' => (bool) $this->currentVersion->is_current,
|
||||||
|
'original_filename' => $this->currentVersion->original_filename,
|
||||||
|
'mime_type' => $this->currentVersion->mime_type,
|
||||||
|
'file_extension' => $this->currentVersion->getFileExtension(),
|
||||||
|
'file_size' => $this->currentVersion->file_size,
|
||||||
|
'file_size_human' => $this->currentVersion->getFileSizeHuman(),
|
||||||
|
'file_hash' => $this->currentVersion->file_hash,
|
||||||
|
'uploaded_by' => $this->currentVersion->uploadedBy?->name,
|
||||||
|
'uploaded_at' => $this->currentVersion->uploaded_at?->toIso8601String(),
|
||||||
|
'download_url' => route('documents.public.download', $this->public_uuid),
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
'links' => [
|
||||||
|
'api_url' => url('/api/v1/public-documents/'.$this->public_uuid),
|
||||||
|
'detail_url' => route('documents.public.show', $this->public_uuid),
|
||||||
|
'web_url' => route('documents.public.show', $this->public_uuid),
|
||||||
|
'download_url' => route('documents.public.download', $this->public_uuid),
|
||||||
|
],
|
||||||
|
'metadata' => [
|
||||||
|
'document_type' => $this->category?->name,
|
||||||
|
'expiration_status' => $this->getExpirationStatusLabel(),
|
||||||
|
'auto_archive_on_expiry' => (bool) $this->auto_archive_on_expiry,
|
||||||
|
'expiry_notice' => $this->expiry_notice,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Http/Resources/PublicDocumentResource.php
Normal file
25
app/Http/Resources/PublicDocumentResource.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class PublicDocumentResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return array_merge(
|
||||||
|
(new PublicDocumentCollectionResource($this))->toArray($request),
|
||||||
|
[
|
||||||
|
'versions' => PublicDocumentVersionResource::collection($this->whenLoaded('versions')),
|
||||||
|
'audit' => [
|
||||||
|
'view_count' => $this->view_count,
|
||||||
|
'download_count' => $this->download_count,
|
||||||
|
'last_updated_by' => $this->lastUpdatedBy?->name,
|
||||||
|
'created_by' => $this->createdBy?->name,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Http/Resources/PublicDocumentVersionResource.php
Normal file
31
app/Http/Resources/PublicDocumentVersionResource.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class PublicDocumentVersionResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'version_number' => $this->version_number,
|
||||||
|
'version_notes' => $this->version_notes,
|
||||||
|
'is_current' => (bool) $this->is_current,
|
||||||
|
'original_filename' => $this->original_filename,
|
||||||
|
'mime_type' => $this->mime_type,
|
||||||
|
'file_extension' => $this->getFileExtension(),
|
||||||
|
'file_size' => $this->file_size,
|
||||||
|
'file_size_human' => $this->getFileSizeHuman(),
|
||||||
|
'file_hash' => $this->file_hash,
|
||||||
|
'uploaded_by' => $this->uploadedBy?->name,
|
||||||
|
'uploaded_at' => $this->uploaded_at?->toIso8601String(),
|
||||||
|
'download_url' => route('documents.public.download-version', [
|
||||||
|
'uuid' => $this->document->public_uuid,
|
||||||
|
'version' => $this->id,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/Observers/DocumentObserver.php
Normal file
54
app/Observers/DocumentObserver.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Services\SiteRevalidationService;
|
||||||
|
|
||||||
|
class DocumentObserver
|
||||||
|
{
|
||||||
|
private const RELEVANT_FIELDS = [
|
||||||
|
'title',
|
||||||
|
'document_number',
|
||||||
|
'description',
|
||||||
|
'document_category_id',
|
||||||
|
'access_level',
|
||||||
|
'status',
|
||||||
|
'archived_at',
|
||||||
|
'current_version_id',
|
||||||
|
'version_count',
|
||||||
|
'expires_at',
|
||||||
|
'auto_archive_on_expiry',
|
||||||
|
'expiry_notice',
|
||||||
|
'deleted_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function created(Document $document): void
|
||||||
|
{
|
||||||
|
$this->revalidate($document);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updated(Document $document): void
|
||||||
|
{
|
||||||
|
if (! $document->wasChanged(self::RELEVANT_FIELDS)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->revalidate($document);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleted(Document $document): void
|
||||||
|
{
|
||||||
|
$this->revalidate($document);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function restored(Document $document): void
|
||||||
|
{
|
||||||
|
$this->revalidate($document);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function revalidate(Document $document): void
|
||||||
|
{
|
||||||
|
SiteRevalidationService::revalidateDocument($document->public_uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Observers\DocumentObserver;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
@@ -19,6 +21,6 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
//
|
Document::observe(DocumentObserver::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,20 @@ class SiteRevalidationService
|
|||||||
static::revalidate('page', $slug);
|
static::revalidate('page', $slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revalidate document cache on the Next.js frontend.
|
||||||
|
*/
|
||||||
|
public static function revalidateDocument(?string $slug = null): void
|
||||||
|
{
|
||||||
|
static::revalidate('document', $slug);
|
||||||
|
}
|
||||||
|
|
||||||
private static function revalidate(string $type, ?string $slug = null): void
|
private static function revalidate(string $type, ?string $slug = null): void
|
||||||
{
|
{
|
||||||
|
if (app()->runningUnitTests()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$url = config('services.nextjs.revalidate_url');
|
$url = config('services.nextjs.revalidate_url');
|
||||||
$token = config('services.nextjs.revalidate_token');
|
$token = config('services.nextjs.revalidate_token');
|
||||||
|
|
||||||
|
|||||||
@@ -127,38 +127,46 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings Dropdown -->
|
<!-- Settings Dropdown -->
|
||||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
@auth
|
||||||
<x-dropdown align="right" width="48">
|
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||||
<x-slot name="trigger">
|
<x-dropdown align="right" width="48">
|
||||||
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150 dark:bg-slate-800 dark:text-slate-100 dark:hover:text-white">
|
<x-slot name="trigger">
|
||||||
<div>{{ Auth::user()->name }}</div>
|
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150 dark:bg-slate-800 dark:text-slate-100 dark:hover:text-white">
|
||||||
|
<div>{{ Auth::user()->name }}</div>
|
||||||
|
|
||||||
<div class="ms-1">
|
<div class="ms-1">
|
||||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
|
|
||||||
<x-slot name="content">
|
<x-slot name="content">
|
||||||
<x-dropdown-link :href="route('profile.edit')">
|
<x-dropdown-link :href="route('profile.edit')">
|
||||||
個人檔案
|
個人檔案
|
||||||
</x-dropdown-link>
|
|
||||||
|
|
||||||
<!-- Authentication -->
|
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
|
||||||
@csrf
|
|
||||||
|
|
||||||
<x-dropdown-link :href="route('logout')"
|
|
||||||
onclick="event.preventDefault();
|
|
||||||
this.closest('form').submit();">
|
|
||||||
登出
|
|
||||||
</x-dropdown-link>
|
</x-dropdown-link>
|
||||||
</form>
|
|
||||||
</x-slot>
|
<!-- Authentication -->
|
||||||
</x-dropdown>
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
</div>
|
@csrf
|
||||||
|
|
||||||
|
<x-dropdown-link :href="route('logout')"
|
||||||
|
onclick="event.preventDefault();
|
||||||
|
this.closest('form').submit();">
|
||||||
|
登出
|
||||||
|
</x-dropdown-link>
|
||||||
|
</form>
|
||||||
|
</x-slot>
|
||||||
|
</x-dropdown>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||||
|
<x-nav-link :href="route('login')" :active="request()->routeIs('login')">
|
||||||
|
登入
|
||||||
|
</x-nav-link>
|
||||||
|
</div>
|
||||||
|
@endauth
|
||||||
|
|
||||||
<!-- Hamburger -->
|
<!-- Hamburger -->
|
||||||
<div class="-me-2 flex items-center sm:hidden">
|
<div class="-me-2 flex items-center sm:hidden">
|
||||||
@@ -279,28 +287,38 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Responsive Settings Options -->
|
<!-- Responsive Settings Options -->
|
||||||
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-700">
|
@auth
|
||||||
<div class="px-4">
|
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-700">
|
||||||
<div class="font-medium text-base text-gray-800 dark:text-slate-200">{{ Auth::user()->name }}</div>
|
<div class="px-4">
|
||||||
<div class="font-medium text-sm text-gray-500 dark:text-slate-400">{{ Auth::user()->email }}</div>
|
<div class="font-medium text-base text-gray-800 dark:text-slate-200">{{ Auth::user()->name }}</div>
|
||||||
</div>
|
<div class="font-medium text-sm text-gray-500 dark:text-slate-400">{{ Auth::user()->email }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 space-y-1">
|
<div class="mt-3 space-y-1">
|
||||||
<x-responsive-nav-link :href="route('profile.edit')">
|
<x-responsive-nav-link :href="route('profile.edit')">
|
||||||
個人檔案
|
個人檔案
|
||||||
</x-responsive-nav-link>
|
|
||||||
|
|
||||||
<!-- Authentication -->
|
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
|
||||||
@csrf
|
|
||||||
|
|
||||||
<x-responsive-nav-link :href="route('logout')"
|
|
||||||
onclick="event.preventDefault();
|
|
||||||
this.closest('form').submit();">
|
|
||||||
登出
|
|
||||||
</x-responsive-nav-link>
|
</x-responsive-nav-link>
|
||||||
</form>
|
|
||||||
|
<!-- Authentication -->
|
||||||
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<x-responsive-nav-link :href="route('logout')"
|
||||||
|
onclick="event.preventDefault();
|
||||||
|
this.closest('form').submit();">
|
||||||
|
登出
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@else
|
||||||
|
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="mt-3 space-y-1">
|
||||||
|
<x-responsive-nav-link :href="route('login')" :active="request()->routeIs('login')">
|
||||||
|
登入
|
||||||
|
</x-responsive-nav-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use App\Http\Controllers\Api\ArticleController;
|
use App\Http\Controllers\Api\ArticleController;
|
||||||
use App\Http\Controllers\Api\HomepageController;
|
use App\Http\Controllers\Api\HomepageController;
|
||||||
use App\Http\Controllers\Api\PageController;
|
use App\Http\Controllers\Api\PageController;
|
||||||
|
use App\Http\Controllers\Api\PublicDocumentController;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@@ -44,4 +45,9 @@ Route::prefix('v1')->group(function () {
|
|||||||
|
|
||||||
// Homepage
|
// Homepage
|
||||||
Route::get('/homepage', [HomepageController::class, 'index']);
|
Route::get('/homepage', [HomepageController::class, 'index']);
|
||||||
|
|
||||||
|
// Public documents (from member document library)
|
||||||
|
Route::get('/public-documents', [PublicDocumentController::class, 'index']);
|
||||||
|
Route::get('/public-documents/{uuid}', [PublicDocumentController::class, 'show'])
|
||||||
|
->whereUuid('uuid');
|
||||||
});
|
});
|
||||||
|
|||||||
131
tests/Feature/Cms/PublicDocumentApiTest.php
Normal file
131
tests/Feature/Cms/PublicDocumentApiTest.php
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Cms;
|
||||||
|
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Models\DocumentCategory;
|
||||||
|
use App\Models\DocumentVersion;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class PublicDocumentApiTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_public_documents_index_only_returns_active_public_documents(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$category = DocumentCategory::factory()->create(['slug' => 'legal-docs', 'name' => '法規文件']);
|
||||||
|
|
||||||
|
$visible = $this->createDocumentWithVersion($user, $category, [
|
||||||
|
'title' => '可見文件',
|
||||||
|
'access_level' => 'public',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->createDocumentWithVersion($user, $category, [
|
||||||
|
'title' => '非公開文件',
|
||||||
|
'access_level' => 'members',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->createDocumentWithVersion($user, $category, [
|
||||||
|
'title' => '封存文件',
|
||||||
|
'access_level' => 'public',
|
||||||
|
'status' => 'archived',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->getJson('/api/v1/public-documents?per_page=50')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonCount(1, 'data')
|
||||||
|
->assertJsonPath('data.0.slug', $visible->public_uuid)
|
||||||
|
->assertJsonPath('data.0.current_version.version_number', '1.0')
|
||||||
|
->assertJsonPath('data.0.category.slug', 'legal-docs')
|
||||||
|
->assertJsonMissing(['title' => '非公開文件'])
|
||||||
|
->assertJsonMissing(['title' => '封存文件']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_public_documents_show_returns_versions_and_related_documents(): void
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$category = DocumentCategory::factory()->create(['slug' => 'governance', 'name' => '治理文件']);
|
||||||
|
|
||||||
|
$document = $this->createDocumentWithVersion($user, $category, [
|
||||||
|
'title' => '主文件',
|
||||||
|
'description' => '主文件描述',
|
||||||
|
'access_level' => 'public',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$secondVersion = DocumentVersion::create([
|
||||||
|
'document_id' => $document->id,
|
||||||
|
'version_number' => '1.1',
|
||||||
|
'version_notes' => '更新內容',
|
||||||
|
'is_current' => true,
|
||||||
|
'file_path' => 'documents/test-main-v1_1.pdf',
|
||||||
|
'original_filename' => 'test-main-v1_1.pdf',
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
'file_size' => 4096,
|
||||||
|
'file_hash' => Str::random(64),
|
||||||
|
'uploaded_by_user_id' => $user->id,
|
||||||
|
'uploaded_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$document->versions()->where('id', '!=', $secondVersion->id)->update(['is_current' => false]);
|
||||||
|
$document->update([
|
||||||
|
'current_version_id' => $secondVersion->id,
|
||||||
|
'version_count' => 2,
|
||||||
|
'last_updated_by_user_id' => $user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$related = $this->createDocumentWithVersion($user, $category, [
|
||||||
|
'title' => '相關文件',
|
||||||
|
'access_level' => 'public',
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->getJson('/api/v1/public-documents/'.$document->public_uuid)
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.slug', $document->public_uuid)
|
||||||
|
->assertJsonPath('data.title', '主文件')
|
||||||
|
->assertJsonPath('data.version_count', 2)
|
||||||
|
->assertJsonPath('data.current_version.version_number', '1.1')
|
||||||
|
->assertJsonPath('data.versions.0.version_number', '1.1')
|
||||||
|
->assertJsonPath('related.0.slug', $related->public_uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createDocumentWithVersion(User $user, DocumentCategory $category, array $overrides = []): Document
|
||||||
|
{
|
||||||
|
$document = Document::factory()->create(array_merge([
|
||||||
|
'document_category_id' => $category->id,
|
||||||
|
'created_by_user_id' => $user->id,
|
||||||
|
'last_updated_by_user_id' => $user->id,
|
||||||
|
'access_level' => 'public',
|
||||||
|
'status' => 'active',
|
||||||
|
'version_count' => 0,
|
||||||
|
], $overrides));
|
||||||
|
|
||||||
|
$version = DocumentVersion::create([
|
||||||
|
'document_id' => $document->id,
|
||||||
|
'version_number' => '1.0',
|
||||||
|
'version_notes' => '初始版本',
|
||||||
|
'is_current' => true,
|
||||||
|
'file_path' => 'documents/test-'.$document->id.'.pdf',
|
||||||
|
'original_filename' => 'test-'.$document->id.'.pdf',
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
'file_size' => 2048,
|
||||||
|
'file_hash' => Str::random(64),
|
||||||
|
'uploaded_by_user_id' => $user->id,
|
||||||
|
'uploaded_at' => now()->subHour(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$document->update([
|
||||||
|
'current_version_id' => $version->id,
|
||||||
|
'version_count' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $document->fresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user