121 lines
9.2 KiB
PHP
121 lines
9.2 KiB
PHP
<?php
|
|
|
|
use App\Http\Controllers\InvestmentWorkspaceController;
|
|
use App\Models\User;
|
|
use App\Services\InvestmentWorkspace;
|
|
use App\Services\OrganizationCountry;
|
|
use App\Services\OrganizationCountryInstallation;
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Str;
|
|
use PragmaRX\Google2FA\Google2FA;
|
|
|
|
$root = dirname(__DIR__);
|
|
require $root.'/vendor/autoload.php';
|
|
$link = json_decode(file_get_contents($root.'/storage/app/private/investmoz-v2/portal-audit/country-candidate-backup-latest.json'), true, 512, JSON_THROW_ON_ERROR);
|
|
$proof = json_decode(file_get_contents($link['path']), true, 512, JSON_THROW_ON_ERROR);
|
|
throw_unless($proof['restore_verified'] && $proof['files_verified'], RuntimeException::class, 'Complete restored backup required.');
|
|
require __DIR__.'/portal-qa-environment.php';
|
|
$target = portalQaEnvironment();
|
|
$target['database'] = $proof['restored_target']['database'];
|
|
throw_unless(preg_match('/^country_candidate_\d{8}_\d{6}_test$/', $target['database']), RuntimeException::class, 'Candidate clone required.');
|
|
$environment = json_decode(file_get_contents($link['directory'].'/files-restored/runtime/effective-runtime.json'), true, 512, JSON_THROW_ON_ERROR);
|
|
foreach (['DB_DATABASE' => $target['database'], 'DB_USERNAME' => 'postgres', 'DB_PASSWORD' => '', 'APP_KEY' => $environment['app_key']] as $key => $value) {
|
|
putenv($key.'='.$value);
|
|
$_ENV[$key] = $value;
|
|
$_SERVER[$key] = $value;
|
|
}
|
|
$app = require $root.'/bootstrap/app.php';
|
|
$app->make(Kernel::class)->bootstrap();
|
|
ini_set('zend.exception_ignore_args', '1');
|
|
set_exception_handler(static function (Throwable $error): void {
|
|
echo json_encode(['result' => 'failed', 'class' => get_class($error), 'reason' => get_class($error) === RuntimeException::class ? $error->getMessage() : null, 'file' => basename($error->getFile()), 'line' => $error->getLine(), 'status' => method_exists($error, 'getStatusCode') ? $error->getStatusCode() : null, 'active_writes' => 0]).PHP_EOL;
|
|
exit(1);
|
|
});
|
|
portalQaAssertTarget($target);
|
|
config(['portal.profile_access_enabled' => false, 'portal.opportunity_dashboard_access_enabled' => false, 'portal.opportunity_specialized_drafts_enabled' => false, 'portal.opportunity_modular_profiles_qa_enabled' => false, 'investor-path.enabled' => false, 'portal.organization_country_enabled' => false]);
|
|
config(['app.cipher' => $environment['cipher']]);
|
|
$mfaRecovered = 0;
|
|
foreach (DB::table('users')->whereIn('id', $proof['original_mfa_user_ids'] ?? [])->cursor() as $originalUser) {
|
|
Crypt::decryptString($originalUser->two_factor_secret);
|
|
if ($originalUser->two_factor_recovery_codes !== null) {
|
|
Crypt::decryptString($originalUser->two_factor_recovery_codes);
|
|
}
|
|
$mfaRecovered++;
|
|
}
|
|
throw_unless($mfaRecovered === count($proof['original_mfa_user_ids'] ?? []), RuntimeException::class, 'Original MFA inventory differs.');
|
|
$country = app(OrganizationCountry::class);
|
|
throw_unless(! $country->available(), RuntimeException::class, 'Missing extension must degrade safely.');
|
|
$before = ['users' => DB::table('users')->count(), 'organizations' => DB::table('organizations')->count(), 'audits' => DB::table('audit_events')->count()];
|
|
$missing = ['2026_10_09_030000_extend_opportunity_resources', '2026_10_10_040000_create_portal_profile_authorization', '2026_10_09_190000_prepare_investor_path'];
|
|
foreach ($missing as $m) {
|
|
throw_unless(! DB::table('migrations')->where('migration', $m)->exists(), RuntimeException::class, 'Independence must be proved without other installations.');
|
|
}
|
|
$actor = User::firstOrCreate(['email' => 'country-candidate-installer@example.invalid'], ['name' => 'Synthetic COUNTRY02 clone installer', 'password' => bcrypt(Str::random(40))]);
|
|
$actor->forceFill(['email_verified_at' => now(), 'two_factor_secret' => Crypt::encryptString((new Google2FA)->generateSecretKey()), 'two_factor_confirmed_at' => now()])->save();
|
|
config(['portal.managers' => [$actor->email]]);
|
|
config(['session.driver' => 'array']);
|
|
$session = app('session')->driver('array');
|
|
$session->start();
|
|
$session->put(['portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time()]);
|
|
$request = Request::create('/management/opportunity-resources-install/organization-country', 'POST');
|
|
$request->setLaravelSession($session);
|
|
$request->setUserResolver(fn () => $actor);
|
|
$app->instance('request', $request);
|
|
app('auth')->guard()->setUser($actor);
|
|
// Explicit policy fixture on the clone; never published as backup/readiness evidence.
|
|
$installer = new class extends OrganizationCountryInstallation
|
|
{
|
|
public function evidence(object $identity): array
|
|
{
|
|
return ['ready' => true, 'backupDate' => null];
|
|
}
|
|
};
|
|
throw_unless($installer->fingerprint() === $proof['code_hash'], RuntimeException::class, 'Candidate changed since backup.');
|
|
$input = ['hash' => $installer->preview($actor)['hash'], 'idempotency_key' => (string) Str::uuid(), 'confirm' => true, 'reason' => 'Synthetic clone compatibility fixture; no active installation or holder designation'];
|
|
$initial = $installer->preview($actor);
|
|
$result = $installer->apply($actor, $input);
|
|
throw_unless($result['changed'] === ! $initial['installed'] && $result['new_columns'] === ($initial['installed'] ? 0 : 2), RuntimeException::class, 'Installation receipt differs from initial schema.');
|
|
throw_unless($installer->apply($actor, $input) == $result, RuntimeException::class, 'Idempotent receipt differs.');
|
|
$again = $installer->apply($actor, [...$input, 'hash' => $installer->preview($actor)['hash'], 'idempotency_key' => (string) Str::uuid()]);
|
|
throw_unless(! $again['changed'], RuntimeException::class, 'Repeated installation duplicated DDL.');
|
|
config(['portal.organization_country_enabled' => true]);
|
|
$org = (string) Str::uuid();
|
|
DB::table('organizations')->insert(['id' => $org, 'name' => 'Synthetic COUNTRY02 clone organization', 'source_system' => 'candidate-qa-only', 'external_id' => 'country02-compatibility', 'organization_type' => 'private']);
|
|
DB::table('memberships')->insert(['organization_id' => $org, 'user_id' => $actor->id, 'role' => 'officer']);
|
|
$saved = $country->save($actor, $org, ['version' => 1, 'country' => 'PT', 'source_reference' => 'Synthetic QA declaration only; never imported into active master data']);
|
|
$profileRequest = Request::create('/api/v1/investment/profile', 'GET', [], [], [], ['HTTP_ACCEPT' => 'application/json']);
|
|
$profileRequest->setUserResolver(fn () => $actor);
|
|
$profile = app(InvestmentWorkspaceController::class)->profile($profileRequest, app(InvestmentWorkspace::class));
|
|
throw_unless($profile instanceof JsonResponse && $profile->getData(true)['organizationCountryAvailable'] === true
|
|
&& $profile->getData(true)['organizations'][0]['country'] === 'PT', RuntimeException::class, 'Legacy profile consumer requires another module.');
|
|
$snapshot = DB::table('organizations')->where('id', $org)->first();
|
|
throw_unless($saved['country'] === 'PT' && $country->represented($actor)[0]['country'] === 'PT', RuntimeException::class, 'Country recovery failed.');
|
|
config(['portal.organization_country_enabled' => false]);
|
|
throw_unless(! $country->available(), RuntimeException::class, 'Disable reversal failed.');
|
|
$after = DB::table('organizations')->where('id', $org)->first();
|
|
throw_unless((array) $snapshot === (array) $after && Schema::hasColumn('organizations', 'incorporation_country_code'), RuntimeException::class, 'Reversal lost country history.');
|
|
foreach ($missing as $m) {
|
|
throw_unless(! DB::table('migrations')->where('migration', $m)->exists(), RuntimeException::class, 'Independent country installation installed another module.');
|
|
}
|
|
$documentsChecked = 0;
|
|
if (Schema::hasColumn('documents', 'storage_encryption')) {
|
|
foreach (DB::table('documents')->where('storage_encryption', 'laravel')->whereNotNull('storage_key')->cursor() as $doc) {
|
|
$file = $link['directory'].'/files-restored/private/'.$doc->storage_key;
|
|
$bytes = Crypt::decryptString(file_get_contents($file));
|
|
throw_unless(hash('sha256', $bytes) === trim($doc->sha256), RuntimeException::class, 'Stable-key original recovery failed.');
|
|
$documentsChecked++;
|
|
}
|
|
}
|
|
$proof['compatibility_verified'] = true;
|
|
$proof['reversal_verified'] = true;
|
|
$proof['original_mfa_envelopes_recovered'] = $mfaRecovered;
|
|
$proof['legacy_profile_controller_verified'] = true;
|
|
$proof['clone_test'] = ['isolated' => $target, 'initial_country_installed' => $initial['installed'], 'missing_other_installations' => $missing, 'policy_fixture_mfa' => true, 'real_browser_mfa_scope' => 'Existing QA Chromium receipts; this clone fixture is not a real MFA/browser test', 'new_columns' => $result['new_columns'], 'duplicate_ddl' => false, 'disable_preserves_data' => true, 'synthetic_users_created' => 1, 'synthetic_organizations_created' => 1, 'active_fixtures_imported' => false, 'encrypted_originals_recovered' => $documentsChecked, 'source_counts' => $before];
|
|
file_put_contents($link['path'], json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
|
echo json_encode(['result' => 'passed', 'clone' => $target, 'other_installations' => false, 'disable_preserves_data' => true, 'originals_decrypted' => $documentsChecked, 'active_writes' => 0], JSON_THROW_ON_ERROR).PHP_EOL;
|