legacy:migrate-agency-names: Agentur-Namen → users.agency_name
Übernimmt je Owner den Namen seiner Legacy-Agentur-Firma(en) ins neue Profilfeld (A2 des Umbaus Agentur-Attribution → User-Profil). Regeln: zuletzt aktualisierter Datensatz gewinnt bei Mehrfach-Namen (alle Varianten im Report), bereits gesetzte Werte werden nie überschrieben, Owner-lose Agentur-Firmen werden nur reportet. --dry-run + JSON-Report nach storage/app/private/migration/; im Deploy-Runbook (Erst-Einrichtung) nach dem Firmen-/User-Import eingereiht. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e457e7a81c
commit
402fa4a38b
3 changed files with 258 additions and 3 deletions
131
app/Console/Commands/MigrateLegacyAgencyNames.php
Normal file
131
app/Console/Commands/MigrateLegacyAgencyNames.php
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\CompanyType;
|
||||
use App\Models\Company;
|
||||
use App\Models\User;
|
||||
use App\Scopes\PortalScope;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Übernimmt die Namen der Legacy-Agentur-Firmen (companies.type = agency)
|
||||
* in das Profilfeld users.agency_name des jeweiligen Owners — Teil des
|
||||
* Umbaus Agentur-Attribution → User-Profil (dev/agentur-attribution/
|
||||
* entwicklungsplan.md, A2). Läuft im Go-Live NACH dem Firmen-/User-Import
|
||||
* und VOR companies:retire-agency-type. Wiederholbar: bereits gesetzte
|
||||
* agency_name-Werte werden nie überschrieben.
|
||||
*
|
||||
* Verwendung:
|
||||
* php artisan legacy:migrate-agency-names --dry-run
|
||||
* php artisan legacy:migrate-agency-names
|
||||
*/
|
||||
class MigrateLegacyAgencyNames extends Command
|
||||
{
|
||||
protected $signature = 'legacy:migrate-agency-names
|
||||
{--dry-run : Nur anzeigen und Report schreiben, keine User ändern}';
|
||||
|
||||
protected $description = 'Übernimmt Namen der Legacy-Agentur-Firmen in das Profilfeld users.agency_name.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('[DRY-RUN] Kein tatsächlicher Schreibvorgang.');
|
||||
}
|
||||
|
||||
// Bewusst ohne is_active-Filter: die Attributions-Zeile hat auch
|
||||
// inaktive Agentur-Firmen angezeigt; dieses Verhalten bleibt erhalten.
|
||||
$agencies = Company::withoutGlobalScope(PortalScope::class)
|
||||
->where('type', CompanyType::Agency)
|
||||
->get(['id', 'portal', 'owner_user_id', 'name', 'is_active', 'updated_at']);
|
||||
|
||||
$report = [
|
||||
'generated_at' => now()->toIso8601String(),
|
||||
'dry_run' => $dryRun,
|
||||
'summary' => [],
|
||||
'multiple_names' => [],
|
||||
'skipped_existing' => [],
|
||||
'ownerless' => [],
|
||||
'missing_owner' => [],
|
||||
];
|
||||
|
||||
foreach ($agencies->whereNull('owner_user_id') as $orphan) {
|
||||
$report['ownerless'][] = [
|
||||
'company_id' => $orphan->id,
|
||||
'portal' => $orphan->portal?->value,
|
||||
'name' => $orphan->name,
|
||||
];
|
||||
}
|
||||
|
||||
$byOwner = $agencies->whereNotNull('owner_user_id')->groupBy('owner_user_id');
|
||||
$users = User::withTrashed()->whereIn('id', $byOwner->keys())->get()->keyBy('id');
|
||||
|
||||
$migrated = 0;
|
||||
|
||||
foreach ($byOwner as $userId => $companies) {
|
||||
$user = $users->get($userId);
|
||||
|
||||
if ($user === null) {
|
||||
$report['missing_owner'][] = [
|
||||
'user_id' => $userId,
|
||||
'company_ids' => $companies->pluck('id')->all(),
|
||||
];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var Company $winner Zuletzt aktualisierter Datensatz gewinnt (Regel Mehrfach-Fälle). */
|
||||
$winner = $companies->sortByDesc('updated_at')->first();
|
||||
$variants = $companies->pluck('name')->unique()->values();
|
||||
|
||||
if ($variants->count() > 1) {
|
||||
$report['multiple_names'][] = [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'chosen' => $winner->name,
|
||||
'variants' => $variants->all(),
|
||||
];
|
||||
}
|
||||
|
||||
if (filled($user->agency_name)) {
|
||||
$report['skipped_existing'][] = [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'existing' => $user->agency_name,
|
||||
'candidate' => $winner->name,
|
||||
];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $dryRun) {
|
||||
$user->update(['agency_name' => $winner->name]);
|
||||
}
|
||||
|
||||
$migrated++;
|
||||
}
|
||||
|
||||
$report['summary'] = [
|
||||
'agency_companies' => $agencies->count(),
|
||||
'users_migrated' => $migrated,
|
||||
'users_skipped_existing' => count($report['skipped_existing']),
|
||||
'users_multiple_names' => count($report['multiple_names']),
|
||||
'companies_ownerless' => count($report['ownerless']),
|
||||
'owners_missing' => count($report['missing_owner']),
|
||||
];
|
||||
|
||||
$this->table(
|
||||
['Agentur-Firmen', 'User migriert', 'Bereits gesetzt', 'Mehrfach-Namen', 'Ohne Owner', 'Owner fehlt'],
|
||||
[array_values($report['summary'])],
|
||||
);
|
||||
|
||||
$path = 'migration/agency-names-'.now()->format('Ymd-His').'.json';
|
||||
Storage::disk('local')->put($path, json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
$this->line("Report geschrieben: storage/app/private/{$path}");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -130,10 +130,14 @@ Events: die Cashier-Defaults (Subscriptions, Invoices, Payment-Intents).
|
|||
`legacy:import` → `legacy:fix-timestamps` → `legacy:migrate-media`
|
||||
(bzw. `legacy:sync-company-logos` / `legacy:sync-press-release-images`)
|
||||
→ `legacy:archive-invoices` → `legacy:grandfather-subscriptions`.
|
||||
2. Ersten Admin-User anlegen (Prod hat keine Demo-User):
|
||||
2. Agentur-Attribution (läuft NACH dem Firmen-/User-Import aus Schritt 1):
|
||||
`legacy:migrate-agency-names` — erst mit `--dry-run` (Report unter
|
||||
`storage/app/private/migration/` prüfen, insbes. Mehrfach-Namen und
|
||||
Owner-lose Agentur-Firmen), dann scharf. Nie überschreibend.
|
||||
3. Ersten Admin-User anlegen (Prod hat keine Demo-User):
|
||||
Registrierung + Rolle via Tinker/DB oder dedizierter Befehl.
|
||||
3. Favicons je Domain deployen (Platzhalter-Generator reicht nicht) [EXTERN R5.4].
|
||||
4. Smoke-Test: siehe Abschnitt 8.
|
||||
4. Favicons je Domain deployen (Platzhalter-Generator reicht nicht) [EXTERN R5.4].
|
||||
5. Smoke-Test: siehe Abschnitt 8.
|
||||
|
||||
## 8. Smoke-Test nach jedem Deploy
|
||||
|
||||
|
|
|
|||
120
tests/Feature/MigrateLegacyAgencyNamesTest.php
Normal file
120
tests/Feature/MigrateLegacyAgencyNamesTest.php
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\CompanyType;
|
||||
use App\Enums\Portal;
|
||||
use App\Models\Company;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
beforeEach(function (): void {
|
||||
/** @var TestCase $this */
|
||||
Storage::fake('local');
|
||||
});
|
||||
|
||||
test('copies the agency name of a single agency company to the owner profile', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = User::factory()->create();
|
||||
Company::factory()->create([
|
||||
'portal' => Portal::Presseecho,
|
||||
'type' => CompanyType::Agency,
|
||||
'name' => 'connektar.de Presseverteiler',
|
||||
'owner_user_id' => $user->id,
|
||||
]);
|
||||
|
||||
$this->artisan('legacy:migrate-agency-names')->assertSuccessful();
|
||||
|
||||
expect($user->fresh()->agency_name)->toBe('connektar.de Presseverteiler');
|
||||
});
|
||||
|
||||
test('with multiple differently named agency companies the most recently updated wins', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = User::factory()->create();
|
||||
Company::factory()->create([
|
||||
'portal' => Portal::Presseecho,
|
||||
'type' => CompanyType::Agency,
|
||||
'name' => 'Alter Verteilername',
|
||||
'owner_user_id' => $user->id,
|
||||
'updated_at' => now()->subYears(2),
|
||||
]);
|
||||
Company::factory()->create([
|
||||
'portal' => Portal::Businessportal24,
|
||||
'type' => CompanyType::Agency,
|
||||
'name' => 'Neuer Verteilername',
|
||||
'owner_user_id' => $user->id,
|
||||
'updated_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
$this->artisan('legacy:migrate-agency-names')->assertSuccessful();
|
||||
|
||||
expect($user->fresh()->agency_name)->toBe('Neuer Verteilername');
|
||||
|
||||
$reportFile = collect(Storage::disk('local')->allFiles('migration'))->first();
|
||||
$report = json_decode(Storage::disk('local')->get($reportFile), true);
|
||||
expect($report['multiple_names'])->toHaveCount(1)
|
||||
->and($report['multiple_names'][0]['variants'])
|
||||
->toContain('Alter Verteilername', 'Neuer Verteilername');
|
||||
});
|
||||
|
||||
test('never overwrites an already set agency name', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = User::factory()->withAgency('Manuell gepflegter Name')->create();
|
||||
Company::factory()->create([
|
||||
'portal' => Portal::Presseecho,
|
||||
'type' => CompanyType::Agency,
|
||||
'name' => 'Legacy-Name',
|
||||
'owner_user_id' => $user->id,
|
||||
]);
|
||||
|
||||
$this->artisan('legacy:migrate-agency-names')->assertSuccessful();
|
||||
|
||||
expect($user->fresh()->agency_name)->toBe('Manuell gepflegter Name');
|
||||
});
|
||||
|
||||
test('only reports agency companies without an owner', function () {
|
||||
/** @var TestCase $this */
|
||||
$orphan = Company::factory()->create([
|
||||
'portal' => Portal::Presseecho,
|
||||
'type' => CompanyType::Agency,
|
||||
'name' => 'Verwaiste Agentur',
|
||||
'owner_user_id' => null,
|
||||
]);
|
||||
|
||||
$this->artisan('legacy:migrate-agency-names')->assertSuccessful();
|
||||
|
||||
$reportFile = collect(Storage::disk('local')->allFiles('migration'))->first();
|
||||
$report = json_decode(Storage::disk('local')->get($reportFile), true);
|
||||
expect($report['ownerless'])->toHaveCount(1)
|
||||
->and($report['ownerless'][0]['company_id'])->toBe($orphan->id);
|
||||
});
|
||||
|
||||
test('dry-run writes nothing but still produces a report', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = User::factory()->create();
|
||||
Company::factory()->create([
|
||||
'portal' => Portal::Presseecho,
|
||||
'type' => CompanyType::Agency,
|
||||
'name' => 'connektar.de Presseverteiler',
|
||||
'owner_user_id' => $user->id,
|
||||
]);
|
||||
|
||||
$this->artisan('legacy:migrate-agency-names', ['--dry-run' => true])->assertSuccessful();
|
||||
|
||||
expect($user->fresh()->agency_name)->toBeNull()
|
||||
->and(Storage::disk('local')->allFiles('migration'))->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('ignores companies that are not agencies', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = User::factory()->create();
|
||||
Company::factory()->create([
|
||||
'portal' => Portal::Presseecho,
|
||||
'type' => CompanyType::Company,
|
||||
'name' => 'Echte Firma GmbH',
|
||||
'owner_user_id' => $user->id,
|
||||
]);
|
||||
|
||||
$this->artisan('legacy:migrate-agency-names')->assertSuccessful();
|
||||
|
||||
expect($user->fresh()->agency_name)->toBeNull();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue