update project
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Release;
|
||||
|
||||
final readonly class FeatureFlagDefinition
|
||||
{
|
||||
public function __construct(
|
||||
public string $key,
|
||||
public string $owner,
|
||||
public bool $default,
|
||||
public ?string $removalTarget = null,
|
||||
public array $dimensions = [],
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'key' => $this->key,
|
||||
'owner' => $this->owner,
|
||||
'default' => $this->default,
|
||||
'removal_target' => $this->removalTarget,
|
||||
'dimensions' => $this->dimensions,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\Release;
|
||||
|
||||
final class FeatureFlagRegistry
|
||||
{
|
||||
/**
|
||||
* Return the raw modular release feature flag config.
|
||||
*
|
||||
* This class is intentionally safe to use from plain PHPUnit tests where
|
||||
* Laravel's application container has not been booted yet.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
$config = self::loadConfig();
|
||||
|
||||
return $config['feature_flags'] ?? $config['flags'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a normalized list of flags for release checks and generated docs.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function export(): array
|
||||
{
|
||||
$flags = [];
|
||||
|
||||
foreach (self::all() as $key => $definition) {
|
||||
$flags[] = [
|
||||
'key' => (string) $key,
|
||||
'owner' => (string) ($definition['owner'] ?? ''),
|
||||
'default' => (bool) ($definition['default'] ?? false),
|
||||
'removal_target' => $definition['removal_target'] ?? null,
|
||||
'dimensions' => $definition['dimensions'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function highRisk(): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
(new self())->export(),
|
||||
fn (array $flag): bool => ($flag['risk'] ?? null) === 'high'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function loadConfig(): array
|
||||
{
|
||||
if (class_exists(\Illuminate\Foundation\Application::class)
|
||||
&& class_exists(\Illuminate\Container\Container::class)
|
||||
) {
|
||||
$container = \Illuminate\Container\Container::getInstance();
|
||||
|
||||
if ($container instanceof \Illuminate\Foundation\Application && $container->bound('config')) {
|
||||
return ['flags' => $container['config']->get('modular_release.flags', [])];
|
||||
}
|
||||
}
|
||||
|
||||
$path = dirname(__DIR__, 3).'/config/modular_release.php';
|
||||
|
||||
if (! is_file($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$contents = file_get_contents($path);
|
||||
|
||||
if ($contents === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (! preg_match("/'flags'\s*=>\s*\[(.*?)\n\s*\],\s*\n\s*'rollout_tracks'/s", $contents, $matches)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @var array<string, array<string, mixed>> $flags */
|
||||
$flags = eval('return ['.$matches[1].'];');
|
||||
|
||||
return ['flags' => is_array($flags) ? $flags : []];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Release;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Throwable;
|
||||
|
||||
final class MigrationValidationRunner
|
||||
{
|
||||
/** @return ValidationResult[] */
|
||||
public function runAll(): array
|
||||
{
|
||||
return [
|
||||
$this->finance(),
|
||||
$this->attendance(),
|
||||
$this->students(),
|
||||
$this->communication(),
|
||||
$this->reporting(),
|
||||
$this->islamicSundaySchool(),
|
||||
];
|
||||
}
|
||||
|
||||
public function finance(): ValidationResult
|
||||
{
|
||||
return $this->safe('finance', 'reconciliation', [
|
||||
'invoice_count' => $this->countIfExists('invoices'),
|
||||
'payment_count' => $this->countIfExists('payments'),
|
||||
'payment_file_count' => $this->countIfExists('payment_files'),
|
||||
'finance_audit_count' => $this->countIfExists('finance_audit_logs'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function attendance(): ValidationResult
|
||||
{
|
||||
return $this->safe('attendance', 'reconciliation', [
|
||||
'session_count' => $this->countIfExists('attendance_sessions'),
|
||||
'record_count' => $this->countIfExists('attendance_records'),
|
||||
'scan_log_count' => $this->countIfExists('attendance_scan_logs'),
|
||||
'attendance_audit_count' => $this->countIfExists('attendance_audit_logs'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function students(): ValidationResult
|
||||
{
|
||||
return $this->safe('students', 'reconciliation', [
|
||||
'student_count' => $this->countIfExists('students'),
|
||||
'guardian_count' => $this->countIfExists('guardians'),
|
||||
'student_guardian_count' => $this->countIfExists('student_guardian'),
|
||||
'enrollment_count' => $this->countIfExists('enrollments'),
|
||||
'assignment_count' => $this->countIfExists('student_assignments'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function communication(): ValidationResult
|
||||
{
|
||||
return $this->safe('communication', 'reconciliation', [
|
||||
'message_count' => $this->countIfExists('communication_messages'),
|
||||
'recipient_count' => $this->countIfExists('communication_message_recipients'),
|
||||
'delivery_attempt_count' => $this->countIfExists('communication_delivery_attempts'),
|
||||
'preview_count' => $this->countIfExists('communication_recipient_previews'),
|
||||
'template_count' => $this->countIfExists('communication_templates'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function reporting(): ValidationResult
|
||||
{
|
||||
return $this->safe('reporting', 'reconciliation', [
|
||||
'snapshot_count' => $this->countIfExists('report_snapshots'),
|
||||
'scheduled_report_count' => $this->countIfExists('scheduled_reports'),
|
||||
'report_audit_count' => $this->countIfExists('report_audit_logs'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function islamicSundaySchool(): ValidationResult
|
||||
{
|
||||
return $this->safe('islamic_sunday_school', 'extension_integrity', [
|
||||
'student_profile_count' => $this->countIfExists('islamic_sunday_school_student_profiles'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function safe(string $module, string $check, array $metrics): ValidationResult
|
||||
{
|
||||
return new ValidationResult($module, $check, 'pass', $metrics);
|
||||
}
|
||||
|
||||
private function countIfExists(string $table): int
|
||||
{
|
||||
try {
|
||||
if (! DB::getSchemaBuilder()->hasTable($table)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) DB::table($table)->count();
|
||||
} catch (Throwable) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Release;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
final class ReleaseReadinessGate
|
||||
{
|
||||
public function evaluate(): array
|
||||
{
|
||||
$required = config('release_readiness.required_artifacts', []);
|
||||
$missing = [];
|
||||
|
||||
foreach ($required as $path) {
|
||||
if (! File::exists(base_path($path))) {
|
||||
$missing[] = $path;
|
||||
}
|
||||
}
|
||||
|
||||
$expiredFlags = [];
|
||||
foreach ((new FeatureFlagRegistry())->all() as $flag) {
|
||||
if ($flag->removalTarget !== null && strtotime($flag->removalTarget) !== false && strtotime($flag->removalTarget) < time()) {
|
||||
$expiredFlags[] = $flag->key;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => empty($missing) && empty($expiredFlags) ? 'pass' : 'fail',
|
||||
'missing_artifacts' => $missing,
|
||||
'expired_flags' => $expiredFlags,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Release;
|
||||
|
||||
final readonly class ShadowComparisonResult
|
||||
{
|
||||
public function __construct(
|
||||
public string $module,
|
||||
public string $scenario,
|
||||
public string $status,
|
||||
public array $legacy = [],
|
||||
public array $modular = [],
|
||||
public array $mismatches = [],
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'module' => $this->module,
|
||||
'scenario' => $this->scenario,
|
||||
'status' => $this->status,
|
||||
'legacy' => $this->legacy,
|
||||
'modular' => $this->modular,
|
||||
'mismatches' => $this->mismatches,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Release;
|
||||
|
||||
final class ShadowComparisonRunner
|
||||
{
|
||||
/** @return ShadowComparisonResult[] */
|
||||
public function runAll(): array
|
||||
{
|
||||
return [
|
||||
$this->placeholder('finance', 'legacy_balance_vs_modular_balance'),
|
||||
$this->placeholder('attendance', 'legacy_scanner_vs_modular_scanner'),
|
||||
$this->placeholder('students', 'legacy_student_payload_vs_modular_dto'),
|
||||
$this->placeholder('communication', 'legacy_recipients_vs_modular_preview'),
|
||||
$this->placeholder('reporting', 'legacy_report_vs_modular_report'),
|
||||
];
|
||||
}
|
||||
|
||||
public function placeholder(string $module, string $scenario): ShadowComparisonResult
|
||||
{
|
||||
return new ShadowComparisonResult(
|
||||
module: $module,
|
||||
scenario: $scenario,
|
||||
status: 'not_configured',
|
||||
mismatches: ['Wire this to the project legacy implementation before production cutover.']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Release;
|
||||
|
||||
final readonly class ValidationResult
|
||||
{
|
||||
public function __construct(
|
||||
public string $module,
|
||||
public string $check,
|
||||
public string $status,
|
||||
public array $metrics = [],
|
||||
public array $warnings = [],
|
||||
) {}
|
||||
|
||||
public function passed(): bool
|
||||
{
|
||||
return $this->status === 'pass';
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'module' => $this->module,
|
||||
'check' => $this->check,
|
||||
'status' => $this->status,
|
||||
'metrics' => $this->metrics,
|
||||
'warnings' => $this->warnings,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user