210 lines
8.3 KiB
PHP
210 lines
8.3 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\CustomerDetail;
|
|
use App\Livewire\Admin\EditCustomer;
|
|
use App\Models\Customer;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\File;
|
|
use Livewire\Livewire;
|
|
|
|
/**
|
|
* Correcting a customer's own details from the console.
|
|
*
|
|
* It could show them and not touch them, so a moved office meant impersonating
|
|
* the customer or editing the database by hand. Most of what is tested here is
|
|
* not the saving — it is what saving must NOT quietly do.
|
|
*/
|
|
beforeEach(function () {
|
|
$this->customer = Customer::factory()->create([
|
|
'name' => 'Beispiel GmbH',
|
|
'contact_name' => 'Bea Berger',
|
|
'email' => 'bea@beispiel.test',
|
|
'phone' => '+43 1 111',
|
|
'customer_type' => Customer::TYPE_BUSINESS,
|
|
'vat_id' => 'ATU11111111',
|
|
'vat_id_verified_at' => now(),
|
|
'vat_id_verified_value' => 'ATU11111111',
|
|
'billing_address' => "Alte Gasse 1\n1010 Wien",
|
|
]);
|
|
});
|
|
|
|
/** The modal, mounted on this customer. */
|
|
function editing(Customer $customer, string $role = 'Owner')
|
|
{
|
|
return Livewire::actingAs(operator($role), 'operator')
|
|
->test(EditCustomer::class, ['uuid' => $customer->uuid]);
|
|
}
|
|
|
|
it('saves the details an operator corrected', function () {
|
|
editing($this->customer)
|
|
->set('name', 'Beispiel GmbH & Co KG')
|
|
->set('contactName', 'Bernd Berger')
|
|
->set('phone', '+43 1 999')
|
|
->set('billingStreet', 'Neue Gasse 9')
|
|
->set('billingPostcode', '1020')
|
|
->set('billingCity', 'Wien')
|
|
->set('locale', 'en')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
|
|
$fresh = $this->customer->fresh();
|
|
|
|
expect($fresh->name)->toBe('Beispiel GmbH & Co KG')
|
|
->and($fresh->contact_name)->toBe('Bernd Berger')
|
|
->and($fresh->phone)->toBe('+43 1 999')
|
|
->and($fresh->billing_address)->toContain('1020 Wien')
|
|
->and($fresh->locale)->toBe('en');
|
|
});
|
|
|
|
it('never resets a recorded customer type back to nobody asked', function () {
|
|
// That answer decides whether a withdrawal right exists. A form saved with
|
|
// the field untouched must not quietly undo it — and the field opens on
|
|
// "leave unchanged", so saving anything else would do exactly that.
|
|
editing($this->customer)->set('customerType', '')->call('save');
|
|
|
|
expect($this->customer->fresh()->customer_type)->toBe(Customer::TYPE_BUSINESS);
|
|
});
|
|
|
|
it('records the other answer when that is what was chosen', function () {
|
|
editing($this->customer)->set('customerType', Customer::TYPE_CONSUMER)->call('save');
|
|
|
|
expect($this->customer->fresh()->isConsumer())->toBeTrue();
|
|
});
|
|
|
|
it('treats a changed VAT number as unverified, and keeps what was checked', function () {
|
|
// The verification is bound to the VALUE, so a new number is unverified the
|
|
// moment it is stored. The record of what WAS checked stays: clearing it
|
|
// would destroy the evidence rather than the claim.
|
|
editing($this->customer)->set('vatId', 'ATU22222222')->call('save');
|
|
|
|
$fresh = $this->customer->fresh();
|
|
|
|
expect($fresh->vat_id)->toBe('ATU22222222')
|
|
->and($fresh->hasVerifiedVatId())->toBeFalse()
|
|
->and($fresh->vat_id_verified_value)->toBe('ATU11111111');
|
|
});
|
|
|
|
it('leaves a verification alone when the number was not touched', function () {
|
|
editing($this->customer)->set('phone', '+43 1 222')->call('save');
|
|
|
|
expect($this->customer->fresh()->hasVerifiedVatId())->toBeTrue();
|
|
});
|
|
|
|
// ---- The address, which is also the login ----
|
|
|
|
it('moves the portal login with the address and asks for it to be confirmed', function () {
|
|
// Otherwise the customer is locked out of an account that no longer matches
|
|
// their record. And the new address is unconfirmed by definition — nobody
|
|
// has shown they can read it.
|
|
$user = User::factory()->create(['email' => 'bea@beispiel.test', 'email_verified_at' => now()]);
|
|
$this->customer->update(['user_id' => $user->id]);
|
|
|
|
editing($this->customer->fresh())->set('email', 'neu@beispiel.test')->call('save')->assertHasNoErrors();
|
|
|
|
expect($this->customer->fresh()->email)->toBe('neu@beispiel.test')
|
|
->and($user->fresh()->email)->toBe('neu@beispiel.test')
|
|
->and($user->fresh()->email_verified_at)->toBeNull();
|
|
});
|
|
|
|
it('refuses an address that belongs to an operator', function () {
|
|
// R21: a customer and an operator must never share an address. No index
|
|
// spans the two tables, so this is checked rather than left to the database.
|
|
$operator = operator('Support');
|
|
|
|
editing($this->customer)
|
|
->set('email', $operator->email)
|
|
->call('save')
|
|
->assertHasErrors('email');
|
|
|
|
expect($this->customer->fresh()->email)->toBe('bea@beispiel.test');
|
|
});
|
|
|
|
it('refuses an address that belongs to another customer', function () {
|
|
// Two customers on one address makes every lookup that matches by address —
|
|
// inbound mail, the portal user, the register — ambiguous.
|
|
Customer::factory()->create(['email' => 'schon@da.test']);
|
|
|
|
editing($this->customer)
|
|
->set('email', 'schon@da.test')
|
|
->call('save')
|
|
->assertHasErrors('email');
|
|
|
|
expect($this->customer->fresh()->email)->toBe('bea@beispiel.test');
|
|
});
|
|
|
|
it('refuses an address that belongs to somebody else user account', function () {
|
|
User::factory()->create(['email' => 'fremd@example.test']);
|
|
|
|
editing($this->customer)
|
|
->set('email', 'fremd@example.test')
|
|
->call('save')
|
|
->assertHasErrors('email');
|
|
});
|
|
|
|
it('lets a customer keep their own address while something else is corrected', function () {
|
|
// The unique rule has to ignore this record, or nothing else on the form can
|
|
// ever be saved.
|
|
editing($this->customer)->set('name', 'Anderer Name')->call('save')->assertHasNoErrors();
|
|
|
|
expect($this->customer->fresh()->name)->toBe('Anderer Name');
|
|
});
|
|
|
|
it('lowercases the address, because that is how every lookup asks for it', function () {
|
|
editing($this->customer)->set('email', 'GROSS@Beispiel.test')->call('save');
|
|
|
|
expect($this->customer->fresh()->email)->toBe('gross@beispiel.test');
|
|
});
|
|
|
|
// ---- The rules around it ----
|
|
|
|
it('refuses an empty company name rather than storing one', function () {
|
|
editing($this->customer)->set('name', '')->call('save')->assertHasErrors('name');
|
|
|
|
expect($this->customer->fresh()->name)->toBe('Beispiel GmbH');
|
|
});
|
|
|
|
it('takes no language this application does not have', function () {
|
|
editing($this->customer)->set('locale', 'fr')->call('save')->assertHasErrors('locale');
|
|
});
|
|
|
|
it('keeps the form to operators who may manage customers', function () {
|
|
Livewire::actingAs(operator('Read-only'), 'operator')
|
|
->test(EditCustomer::class, ['uuid' => $this->customer->uuid])
|
|
->assertForbidden();
|
|
});
|
|
|
|
it('is reached from the customer page through openModal, not a page method', function () {
|
|
// R20: an edit button that calls a method on the page component is the inline
|
|
// editor coming back under a new name.
|
|
$page = File::get(resource_path('views/livewire/admin/customer-detail.blade.php'));
|
|
|
|
expect($page)->toContain("component: 'admin.edit-customer'");
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(CustomerDetail::class, ['uuid' => $this->customer->uuid])
|
|
->assertSee(__('customer_detail.edit'));
|
|
});
|
|
|
|
it('does not offer to change what an operator must not change here', function () {
|
|
// status has its own action with its own consequences for access;
|
|
// stripe_customer_id points at somebody else's payments if it is wrong; the
|
|
// brand fields are the customer's own.
|
|
// Comments stripped first. The class documents at length WHY each of these
|
|
// is absent, and reading that documentation as the offence is a trap this
|
|
// repository has fallen into three times — see the @vite, prefers-color-scheme
|
|
// and clupilot.cloud scans.
|
|
$code = implode('', array_map(
|
|
fn ($token) => is_array($token)
|
|
? (in_array($token[0], [T_COMMENT, T_DOC_COMMENT], true) ? '' : $token[1])
|
|
: $token,
|
|
token_get_all(File::get(app_path('Livewire/Admin/EditCustomer.php'))),
|
|
));
|
|
|
|
expect($code)->not->toContain("'status'")
|
|
->not->toContain('stripe_customer_id')
|
|
->not->toContain('brand_logo_path')
|
|
// And the fields it DOES write are the ones it is meant to.
|
|
->toContain("'billing_address'")
|
|
->toContain("'customer_type'");
|
|
});
|