fix: Abgleich der Firmenrechte entzieht Bestandskonten ohne E-Mail-Bestätigung nicht mehr pauschal alle Rechte — memberships:resync-auto mit Probelauf und Notbremse
Auf Live meldete memberships:resync-auto nach v1.4.0 „vorher 19607, nachher 9": syncMembershipsForEmail() entzog Konten ohne email_verified_at alle automatischen Rechte, und 19.597 davon gehören Bestandskonten aus dem Altsystem. Jetzt wird nur entzogen, was keine E-Mail-Grundlage mehr hat — unabhängig vom Kontostatus (S-03 bleibt wirksam); neue Rechte weiterhin nur für aktive, verifizierte Konten (F01). memberships:resync-auto läuft in einer Transaktion, kennt --dry-run und rollt zurück, wenn mehr als 5 % (mindestens 50) der Rechte wegfallen würden (--force zum Erzwingen). Probelauf gegen die Live-Kopie: 19.607 → 19.316. Wiederherstellung: storage/app/private/restore-company-user-auto-2026-10-07.sql (nicht versioniert, INSERT IGNORE, idempotent). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5dc3bf52d2
commit
c76e420f1d
4 changed files with 136 additions and 24 deletions
|
|
@ -140,3 +140,64 @@ test('the resync command removes stale automatic grants left by bulk updates', f
|
|||
expect(membershipOf($user, $company))->toBeNull()
|
||||
->and(membershipOf($keeper, $keptCompany)?->granted_via)->toBe('auto');
|
||||
});
|
||||
|
||||
/*
|
||||
* Regression v1.4.1: Bestandskonten aus dem Altsystem haben kein
|
||||
* `email_verified_at`. Der Abgleich am 07.10.2026 entzog ihnen pauschal alle
|
||||
* automatischen Rechte (19.598 von 19.607), obwohl die Grundlage bestand.
|
||||
*/
|
||||
test('legacy accounts without verified email keep automatic access whose basis still exists', function () {
|
||||
/** @var TestCase $this */
|
||||
$legacy = User::factory()->unverified()->create(['email' => 'altkunde@firma.test', 'is_active' => true]);
|
||||
$company = Company::factory()->presseecho()->create();
|
||||
$contact = Contact::factory()->for($company)->create(['email' => 'altkunde@firma.test', 'portal' => $company->portal->value]);
|
||||
$legacy->companies()->attach($company->id, ['role' => 'responsible', 'granted_via' => 'auto']);
|
||||
|
||||
$this->artisan('memberships:resync-auto')->assertSuccessful();
|
||||
app(ContactAccessService::class)->syncMembershipsForEmail($legacy);
|
||||
|
||||
expect(membershipOf($legacy, $company)?->granted_via)->toBe('auto');
|
||||
|
||||
// Fällt die Grundlage weg, endet das Recht auch für sie (S-03).
|
||||
$contact->delete();
|
||||
|
||||
expect(membershipOf($legacy, $company))->toBeNull();
|
||||
});
|
||||
|
||||
test('inactive or unverified accounts do not receive new automatic access', function (array $attributes) {
|
||||
$user = User::factory()->create(['email' => 'neu-ohne-nachweis@firma.test', ...$attributes]);
|
||||
$company = Company::factory()->presseecho()->create();
|
||||
Contact::factory()->for($company)->create(['email' => 'neu-ohne-nachweis@firma.test', 'portal' => $company->portal->value]);
|
||||
|
||||
app(ContactAccessService::class)->syncMembershipsForEmail($user);
|
||||
|
||||
expect(membershipOf($user, $company))->toBeNull();
|
||||
})->with([
|
||||
'unverified' => [['email_verified_at' => null]],
|
||||
'inactive' => [['is_active' => false]],
|
||||
]);
|
||||
|
||||
test('the resync command rolls back a suspicious mass revocation unless forced', function () {
|
||||
/** @var TestCase $this */
|
||||
$company = Company::factory()->presseecho()->create();
|
||||
$users = User::factory()->count(60)->create();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$user->companies()->attach($company->id, ['role' => 'responsible', 'granted_via' => 'auto']);
|
||||
}
|
||||
|
||||
$count = fn (): int => DB::table('company_user')->where('granted_via', 'auto')->count();
|
||||
|
||||
$this->artisan('memberships:resync-auto', ['--dry-run' => true])
|
||||
->expectsOutputToContain('vorher 60, nachher 0. Probelauf')
|
||||
->assertSuccessful();
|
||||
expect($count())->toBe(60);
|
||||
|
||||
$this->artisan('memberships:resync-auto')
|
||||
->expectsOutputToContain('zurückgerollt')
|
||||
->assertFailed();
|
||||
expect($count())->toBe(60);
|
||||
|
||||
$this->artisan('memberships:resync-auto', ['--force' => true])->assertSuccessful();
|
||||
expect($count())->toBe(0);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue