## Summary - move the Laravel application into `apps/platform` and keep the repository root for orchestration, docs, and tooling - update the local command model, Sail/Docker wiring, runtime paths, and ignore rules around the new platform location - add relocation quickstart/contracts plus focused smoke coverage for bootstrap, command model, routes, and runtime behavior ## Validation - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PlatformRelocation` - integrated browser smoke validated `/up`, `/`, `/admin`, `/admin/choose-workspace`, and tenant route semantics for `200`, `403`, and `404` ## Remaining Rollout Checks - validate Dokploy build context and working-directory assumptions against the new `apps/platform` layout - confirm web, queue, and scheduler processes all start from the expected working directory in staging/production - verify no legacy volume mounts or asset-publish paths still point at the old root-level `public/` or `storage/` locations Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #213
138 lines
4.4 KiB
PHP
138 lines
4.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\TenantReviews;
|
|
|
|
use App\Models\TenantReview;
|
|
use App\Support\TenantReviewCompletenessState;
|
|
use App\Support\TenantReviewStatus;
|
|
use Illuminate\Support\Collection;
|
|
|
|
final class TenantReviewReadinessGate
|
|
{
|
|
/**
|
|
* @param iterable<array<string, mixed>> $sections
|
|
* @return list<string>
|
|
*/
|
|
public function blockersForSections(iterable $sections): array
|
|
{
|
|
$blockers = [];
|
|
|
|
foreach ($sections as $section) {
|
|
$required = (bool) ($section['required'] ?? false);
|
|
$state = (string) ($section['completeness_state'] ?? TenantReviewCompletenessState::Missing->value);
|
|
$title = (string) ($section['title'] ?? 'Review section');
|
|
|
|
if (! $required) {
|
|
continue;
|
|
}
|
|
|
|
if ($state === TenantReviewCompletenessState::Missing->value) {
|
|
$blockers[] = sprintf('%s is missing.', $title);
|
|
}
|
|
|
|
if ($state === TenantReviewCompletenessState::Stale->value) {
|
|
$blockers[] = sprintf('%s is stale and must be refreshed before publication.', $title);
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique($blockers));
|
|
}
|
|
|
|
/**
|
|
* @param iterable<array<string, mixed>> $sections
|
|
*/
|
|
public function completenessForSections(iterable $sections): TenantReviewCompletenessState
|
|
{
|
|
$states = collect($sections)
|
|
->map(static fn (array $section): string => (string) ($section['completeness_state'] ?? TenantReviewCompletenessState::Missing->value))
|
|
->values();
|
|
|
|
if ($states->isEmpty()) {
|
|
return TenantReviewCompletenessState::Missing;
|
|
}
|
|
|
|
if ($states->contains(TenantReviewCompletenessState::Missing->value)) {
|
|
return TenantReviewCompletenessState::Missing;
|
|
}
|
|
|
|
if ($states->contains(TenantReviewCompletenessState::Stale->value)) {
|
|
return TenantReviewCompletenessState::Stale;
|
|
}
|
|
|
|
if ($states->contains(TenantReviewCompletenessState::Partial->value)) {
|
|
return TenantReviewCompletenessState::Partial;
|
|
}
|
|
|
|
return TenantReviewCompletenessState::Complete;
|
|
}
|
|
|
|
/**
|
|
* @param iterable<array<string, mixed>> $sections
|
|
*/
|
|
public function statusForSections(iterable $sections): TenantReviewStatus
|
|
{
|
|
return $this->blockersForSections($sections) === []
|
|
? TenantReviewStatus::Ready
|
|
: TenantReviewStatus::Draft;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public function blockersForReview(TenantReview $review): array
|
|
{
|
|
$sections = $review->relationLoaded('sections')
|
|
? $review->sections
|
|
: $review->sections()->get();
|
|
|
|
return $this->blockersForSections($sections->map(static function ($section): array {
|
|
return [
|
|
'title' => (string) $section->title,
|
|
'required' => (bool) $section->required,
|
|
'completeness_state' => (string) $section->completeness_state,
|
|
];
|
|
})->all());
|
|
}
|
|
|
|
public function canPublish(TenantReview $review): bool
|
|
{
|
|
if (! $review->isMutable()) {
|
|
return false;
|
|
}
|
|
|
|
return $this->blockersForReview($review) === [];
|
|
}
|
|
|
|
public function canExport(TenantReview $review): bool
|
|
{
|
|
if (! in_array($review->statusEnum(), [
|
|
TenantReviewStatus::Ready,
|
|
TenantReviewStatus::Published,
|
|
], true)) {
|
|
return false;
|
|
}
|
|
|
|
return $this->blockersForReview($review) === [];
|
|
}
|
|
|
|
/**
|
|
* @param iterable<array<string, mixed>> $sections
|
|
* @return array<string, int>
|
|
*/
|
|
public function sectionStateCounts(iterable $sections): array
|
|
{
|
|
$counts = collect($sections)
|
|
->groupBy(static fn (array $section): string => (string) ($section['completeness_state'] ?? TenantReviewCompletenessState::Missing->value))
|
|
->map(static fn (Collection $group): int => $group->count());
|
|
|
|
return [
|
|
'complete' => (int) ($counts[TenantReviewCompletenessState::Complete->value] ?? 0),
|
|
'partial' => (int) ($counts[TenantReviewCompletenessState::Partial->value] ?? 0),
|
|
'missing' => (int) ($counts[TenantReviewCompletenessState::Missing->value] ?? 0),
|
|
'stale' => (int) ($counts[TenantReviewCompletenessState::Stale->value] ?? 0),
|
|
];
|
|
}
|
|
}
|