Ein Sitz fuehrt Absicht und Wirklichkeit getrennt
status = was der Inhaber will, nc_state = was in der Nextcloud ist. Ein einzelnes Feld muesste luegen — und "fehlgeschlagen" liesse sich gar nicht sagen. Rollen werden Nextcloud-Gruppen. Der Inhaber-Sitz wird mit dem Admin-Konto verknuepft, das die Bereitstellung laengst angelegt hat. SeatFactory setzt nc_state jetzt explizit (wie status/locale bei Customer und Instance): Eloquents create() liest DB-Spalten-Vorgaben nicht ins In-Memory-Modell zurueck, sonst haette Seat::factory()->create()->nc_state null statt 'none' geliefert.feat/versandtakt
parent
6513ccdb00
commit
94210a7176
|
|
@ -14,11 +14,70 @@ class Seat extends Model
|
|||
|
||||
public const ROLES = ['owner', 'admin', 'member', 'readonly'];
|
||||
|
||||
protected $fillable = ['customer_id', 'email', 'name', 'role', 'status', 'invited_at'];
|
||||
/**
|
||||
* Welche Nextcloud-Gruppe eine Rolle bedeutet.
|
||||
*
|
||||
* Rollen sind Gruppen im Gast, damit weitere spaeter eine Zeile sind und
|
||||
* kein Umbau. `owner` und `admin` teilen sich `admin` — der Unterschied
|
||||
* zwischen beiden ist eine CluPilot-Angelegenheit (der letzte owner darf
|
||||
* nicht entfernt werden), keine Nextcloud-Angelegenheit.
|
||||
*/
|
||||
public const GROUPS = [
|
||||
'owner' => 'admin',
|
||||
'admin' => 'admin',
|
||||
'member' => 'mitarbeiter',
|
||||
'readonly' => 'nur-lesen',
|
||||
];
|
||||
|
||||
public const STATE_NONE = 'none';
|
||||
|
||||
public const STATE_PENDING = 'pending';
|
||||
|
||||
public const STATE_SYNCED = 'synced';
|
||||
|
||||
public const STATE_FAILED = 'failed';
|
||||
|
||||
protected $fillable = [
|
||||
'customer_id', 'email', 'name', 'role', 'status', 'invited_at',
|
||||
'nc_username', 'nc_state', 'nc_error', 'nc_synced_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['invited_at' => 'datetime'];
|
||||
return ['invited_at' => 'datetime', 'nc_synced_at' => 'datetime'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Rolle, die keinen eigenen Speicherplatz bekommt — und die einzige,
|
||||
* bei der ein eigener Wert am Konto richtig ist. Siehe NextcloudUsers.
|
||||
*/
|
||||
public function isReadonly(): bool
|
||||
{
|
||||
return $this->role === 'readonly';
|
||||
}
|
||||
|
||||
/**
|
||||
* Verknuepft einen Inhaber-Sitz mit dem Admin-Konto seiner Instanz.
|
||||
*
|
||||
* Dieses Konto existiert in der Nextcloud tatsaechlich — CreateCustomerAdmin
|
||||
* hat es beim Aufbau angelegt. Ein zweites Admin-Konto fuer dieselbe Person
|
||||
* waere eines zu viel.
|
||||
*/
|
||||
public function linkToInstanceAdmin(): void
|
||||
{
|
||||
$ref = $this->customer?->instances()
|
||||
->whereIn('status', ['active', 'cancellation_scheduled'])
|
||||
->latest('id')->first()?->nc_admin_ref;
|
||||
|
||||
if (blank($ref)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->forceFill([
|
||||
'nc_username' => $ref,
|
||||
'nc_state' => self::STATE_SYNCED,
|
||||
'nc_synced_at' => now(),
|
||||
])->save();
|
||||
}
|
||||
|
||||
public function customer(): BelongsTo
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ class SeatFactory extends Factory
|
|||
'role' => 'member',
|
||||
'status' => 'active',
|
||||
'invited_at' => now(),
|
||||
// Eloquents create() liest die DB-Vorgabe nicht ins In-Memory-Modell
|
||||
// zurueck — ohne diese Zeile bliebe nc_state hier null statt 'none',
|
||||
// wie bei jedem anderen Feld mit Spalten-Vorgabe in diesem Bestand
|
||||
// (siehe CustomerFactory::status, InstanceFactory::status).
|
||||
'nc_state' => Seat::STATE_NONE,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Seat;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Ein Sitz fuehrt ab hier ZWEI Angaben nebeneinander.
|
||||
*
|
||||
* `status` ist, was der Inhaber WILL — er aendert sich sofort beim Klick.
|
||||
* `nc_state` ist, was in der Nextcloud WIRKLICH ist — er aendert sich, wenn
|
||||
* der Auftrag durch ist. Ein einzelnes Feld muesste luegen: entweder es
|
||||
* behauptet einen Erfolg, den nur die Warteschlange bringen kann, oder es
|
||||
* verschweigt dem Inhaber, dass sein Klick angekommen ist.
|
||||
*
|
||||
* Nur so laesst sich "fehlgeschlagen" ueberhaupt sagen.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('seats', function (Blueprint $table) {
|
||||
// Einmal gesetzt, nie wieder geaendert: Nextcloud kann Benutzer
|
||||
// nicht umbenennen.
|
||||
$table->string('nc_username')->nullable()->after('email');
|
||||
$table->string('nc_state', 16)->default('none')->after('status');
|
||||
$table->text('nc_error')->nullable()->after('nc_state');
|
||||
$table->timestamp('nc_synced_at')->nullable()->after('nc_error');
|
||||
});
|
||||
|
||||
// Bestandssitze sind ehrlich beschrieben: angelegt, nie eingeladen —
|
||||
// denn eingeladen hat sie nie jemand, resend() war eine Attrappe.
|
||||
// EINE Ausnahme: der Inhaber-Sitz IST das Admin-Konto der Instanz.
|
||||
//
|
||||
// with('customer') vor chunkById: linkToInstanceAdmin() liest
|
||||
// $this->customer, und ohne Eager Load waere das eine Anfrage pro
|
||||
// Sitz statt einer pro Bloecken von 100 — bei owner-Sitzen (einer
|
||||
// je Kunde) ist der Bestand hier so gross wie der Kundenstamm.
|
||||
Seat::query()->where('role', 'owner')->with('customer')->chunkById(100, function ($sitze) {
|
||||
foreach ($sitze as $sitz) {
|
||||
$sitz->linkToInstanceAdmin();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('seats', function (Blueprint $table) {
|
||||
$table->dropColumn(['nc_username', 'nc_state', 'nc_error', 'nc_synced_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<?php // tests/Feature/Seats/SeatModelTest.php
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\Instance;
|
||||
use App\Models\Seat;
|
||||
|
||||
it('startet jeden Sitz als noch nicht in der Nextcloud', function () {
|
||||
expect(Seat::factory()->create()->nc_state)->toBe(Seat::STATE_NONE);
|
||||
});
|
||||
|
||||
it('bildet jede Rolle auf genau eine Nextcloud-Gruppe ab', function () {
|
||||
// Keine Rolle ohne Gruppe: eine Rolle, die im Portal waehlbar ist und im
|
||||
// Gast nichts bewirkt, ist genau die Attrappe, die hier abgeschafft wird.
|
||||
foreach (Seat::ROLES as $rolle) {
|
||||
expect(Seat::GROUPS)->toHaveKey($rolle)
|
||||
->and(Seat::GROUPS[$rolle])->not->toBe('');
|
||||
}
|
||||
});
|
||||
|
||||
it('fuehrt owner und admin in die Admin-Gruppe', function () {
|
||||
expect(Seat::GROUPS['owner'])->toBe('admin')
|
||||
->and(Seat::GROUPS['admin'])->toBe('admin');
|
||||
});
|
||||
|
||||
it('erkennt die Rolle, die keinen Speicherplatz bekommt', function () {
|
||||
expect(Seat::factory()->create(['role' => 'readonly'])->isReadonly())->toBeTrue()
|
||||
->and(Seat::factory()->create(['role' => 'member'])->isReadonly())->toBeFalse();
|
||||
});
|
||||
|
||||
it('verknuepft den Inhaber-Sitz mit dem bestehenden Admin-Konto', function () {
|
||||
// Die Wanderung. Der owner-Sitz eines Kunden, dessen Instanz ein
|
||||
// nc_admin_ref traegt, IST dieses Konto — die Bereitstellung hat es
|
||||
// angelegt. Stuende er auf 'none', boete das Panel dem Inhaber an, sich
|
||||
// selbst einzuladen, und der Auftrag traefe auf einen Benutzer, den es
|
||||
// laengst gibt.
|
||||
$customer = Customer::factory()->create();
|
||||
Instance::factory()->for($customer)->create(['nc_admin_ref' => 'admin', 'status' => 'active']);
|
||||
$sitz = Seat::factory()->for($customer)->create(['role' => 'owner']);
|
||||
|
||||
// Die Wanderung lief beim Anlegen der Testdatenbank; hier wird die
|
||||
// Nachziehmethode geprueft, die sie benutzt.
|
||||
$sitz->linkToInstanceAdmin();
|
||||
|
||||
expect($sitz->fresh()->nc_username)->toBe('admin')
|
||||
->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_SYNCED);
|
||||
});
|
||||
|
||||
it('laesst einen Inhaber-Sitz ohne Instanz unangetastet', function () {
|
||||
// Kein Auftrag hat je ein Admin-Konto angelegt — es gibt nichts zu
|
||||
// verknuepfen, und es darf nichts krachen, weil customer->instances()
|
||||
// leer ist.
|
||||
$customer = Customer::factory()->create();
|
||||
$sitz = Seat::factory()->for($customer)->create(['role' => 'owner']);
|
||||
|
||||
$sitz->linkToInstanceAdmin();
|
||||
|
||||
expect($sitz->fresh()->nc_username)->toBeNull()
|
||||
->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_NONE);
|
||||
});
|
||||
|
||||
it('laesst einen Inhaber-Sitz ohne nc_admin_ref unangetastet', function () {
|
||||
// Eine Instanz ohne Admin-Konto — etwa eine, deren Bereitstellung noch
|
||||
// nicht bis dahin gekommen ist. blank() statt einer reinen Null-Pruefung,
|
||||
// weil ein leerer String hier dieselbe Aussage ist wie kein Wert.
|
||||
$customer = Customer::factory()->create();
|
||||
Instance::factory()->for($customer)->create(['nc_admin_ref' => null, 'status' => 'active']);
|
||||
$sitz = Seat::factory()->for($customer)->create(['role' => 'owner']);
|
||||
|
||||
$sitz->linkToInstanceAdmin();
|
||||
|
||||
expect($sitz->fresh()->nc_username)->toBeNull()
|
||||
->and($sitz->fresh()->nc_state)->toBe(Seat::STATE_NONE);
|
||||
});
|
||||
Loading…
Reference in New Issue