234 lines
9.2 KiB
PHP
234 lines
9.2 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\CustomerDetail;
|
|
use App\Livewire\Admin\EditMailTemplate;
|
|
use App\Livewire\Admin\MailTemplates;
|
|
use App\Models\Customer;
|
|
use App\Models\Instance;
|
|
use App\Models\MailTemplate;
|
|
use App\Models\SentMail;
|
|
use App\Models\Subscription;
|
|
use App\Services\Mail\MailTemplateRenderer;
|
|
use App\Support\CompanyProfile;
|
|
use Livewire\Livewire;
|
|
|
|
/**
|
|
* The answers an operator gives over and over, written once.
|
|
*
|
|
* A template is INSERTED and then edited, never sent as it stands. So the thing
|
|
* worth testing is not "did a mail go out" but "did the right details end up in
|
|
* the field the operator is about to correct".
|
|
*/
|
|
beforeEach(fn () => CompanyProfile::put(['name' => 'CluPilot GmbH']));
|
|
|
|
it('fills a template with the customer own details', function () {
|
|
$customer = Customer::factory()->create([
|
|
'name' => 'Beispiel GmbH',
|
|
'contact_name' => 'Bea Berger',
|
|
'email' => 'bea@beispiel.test',
|
|
]);
|
|
Subscription::factory()->plan('team')->create([
|
|
'customer_id' => $customer->id,
|
|
'status' => 'active',
|
|
]);
|
|
|
|
$template = MailTemplate::create([
|
|
'name' => 'Datenübernahme',
|
|
'subject' => 'Ihre Anfrage — {{customer}}',
|
|
'body' => "Guten Tag {{contact}},\n\nzu Ihrem Paket {{plan}} für {{amount}}:\n\n{{operator}}\n{{company}}",
|
|
]);
|
|
|
|
$filled = app(MailTemplateRenderer::class)->render($template, $customer, operator('Owner'));
|
|
|
|
expect($filled['subject'])->toBe('Ihre Anfrage — Beispiel GmbH')
|
|
->and($filled['body'])->toContain('Guten Tag Bea Berger,')
|
|
->and($filled['body'])->toContain(__('billing.plan.team'))
|
|
->and($filled['body'])->toContain('CluPilot GmbH');
|
|
});
|
|
|
|
it('writes to the person where one is on record, and to the company otherwise', function () {
|
|
// "Guten Tag Beispiel GmbH" is how a mail announces that it came out of a
|
|
// database.
|
|
$withContact = Customer::factory()->create(['name' => 'Beispiel GmbH', 'contact_name' => 'Bea Berger']);
|
|
$without = Customer::factory()->create(['name' => 'Zweite GmbH', 'contact_name' => null]);
|
|
|
|
$renderer = app(MailTemplateRenderer::class);
|
|
|
|
expect($renderer->values($withContact)['contact'])->toBe('Bea Berger')
|
|
->and($renderer->values($without)['contact'])->toBe('Zweite GmbH');
|
|
});
|
|
|
|
it('quotes the amount the customer knows, which is the gross one', function () {
|
|
// What the price sheet quoted them and what their bank statement says.
|
|
$customer = Customer::factory()->create();
|
|
Subscription::factory()->plan('start')->create([
|
|
'customer_id' => $customer->id,
|
|
'status' => 'active',
|
|
]);
|
|
|
|
$amount = app(MailTemplateRenderer::class)->values($customer)['amount'];
|
|
|
|
// 49 € net at 20 % → 58,80 €.
|
|
expect($amount)->toContain('58,80');
|
|
});
|
|
|
|
it('leaves a placeholder empty rather than inventing a package nobody bought', function () {
|
|
// A guess in a mail to a paying customer is worse than a gap the operator
|
|
// can see and fill.
|
|
$customer = Customer::factory()->create();
|
|
|
|
$values = app(MailTemplateRenderer::class)->values($customer);
|
|
|
|
expect($values['plan'])->toBe('')
|
|
->and($values['amount'])->toBe('')
|
|
->and($values['instance'])->toBe('');
|
|
});
|
|
|
|
it('leaves a typo standing so it is seen before anything is sent', function () {
|
|
// Replacing it with nothing produces "Guten Tag ," — a mail that looks sent
|
|
// by a machine, which is what templates exist to avoid.
|
|
$customer = Customer::factory()->create(['name' => 'Beispiel GmbH']);
|
|
$template = MailTemplate::create([
|
|
'name' => 'Mit Tippfehler',
|
|
'subject' => 'Test',
|
|
'body' => 'Guten Tag {{kunde}}, für {{customer}}.',
|
|
]);
|
|
|
|
$body = app(MailTemplateRenderer::class)->render($template, $customer)['body'];
|
|
|
|
expect($body)->toContain('{{kunde}}')
|
|
->and($body)->toContain('Beispiel GmbH');
|
|
});
|
|
|
|
it('tolerates a space inside the braces, because somebody will type one', function () {
|
|
$customer = Customer::factory()->create(['name' => 'Beispiel GmbH']);
|
|
$template = MailTemplate::create(['name' => 'X', 'subject' => '{{ customer }}', 'body' => '{{ CUSTOMER }}']);
|
|
|
|
$filled = app(MailTemplateRenderer::class)->render($template, $customer);
|
|
|
|
expect($filled['subject'])->toBe('Beispiel GmbH')
|
|
->and($filled['body'])->toBe('Beispiel GmbH');
|
|
});
|
|
|
|
it('names the address of the cloud, where there is one', function () {
|
|
$customer = Customer::factory()->create();
|
|
Instance::factory()->create([
|
|
'customer_id' => $customer->id,
|
|
'subdomain' => 'kanzlei-berger',
|
|
'status' => 'active',
|
|
]);
|
|
|
|
expect(app(MailTemplateRenderer::class)->values($customer)['instance'])->toBe('kanzlei-berger');
|
|
});
|
|
|
|
// ---- Managing them ----
|
|
|
|
it('creates a template and offers it when writing', function () {
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(MailTemplates::class)
|
|
->set('name', 'Datenübernahme')
|
|
->set('subject', 'Ihre Anfrage')
|
|
->set('body', 'Guten Tag {{contact}},')
|
|
->call('create')
|
|
->assertHasNoErrors();
|
|
|
|
expect(MailTemplate::query()->count())->toBe(1)
|
|
->and(MailTemplate::query()->usable()->count())->toBe(1);
|
|
});
|
|
|
|
it('retires a template instead of deleting it', function () {
|
|
// An answer that is no longer given is still the answer somebody was given
|
|
// last year, and the mail register refers to it.
|
|
$template = MailTemplate::create(['name' => 'Alt', 'subject' => 'S', 'body' => 'B']);
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(MailTemplates::class)
|
|
->call('toggleActive', $template->uuid);
|
|
|
|
expect($template->fresh()->active)->toBeFalse()
|
|
->and(MailTemplate::query()->count())->toBe(1)
|
|
->and(MailTemplate::query()->usable()->count())->toBe(0);
|
|
});
|
|
|
|
it('edits a template in a modal, never in the row', function () {
|
|
// R20: a five-line textarea growing inside a list row pushes every column
|
|
// beside it and reads as a rendering fault.
|
|
$template = MailTemplate::create(['name' => 'Alt', 'subject' => 'S', 'body' => 'B']);
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(EditMailTemplate::class, ['uuid' => $template->uuid])
|
|
->set('name', 'Neu')
|
|
->set('body', 'Guten Tag {{contact}},')
|
|
->call('save');
|
|
|
|
expect($template->fresh()->name)->toBe('Neu')
|
|
->and($template->fresh()->body)->toContain('{{contact}}');
|
|
});
|
|
|
|
it('keeps the templates to operators who may write to customers', function () {
|
|
Livewire::actingAs(operator('Read-only'), 'operator')
|
|
->test(MailTemplates::class)
|
|
->assertForbidden();
|
|
});
|
|
|
|
// ---- Using one on the customer page ----
|
|
|
|
it('puts a template into the compose fields and opens that tab', function () {
|
|
$customer = Customer::factory()->create(['name' => 'Beispiel GmbH', 'contact_name' => 'Bea Berger']);
|
|
$template = MailTemplate::create([
|
|
'name' => 'Datenübernahme',
|
|
'subject' => 'Ihre Anfrage — {{customer}}',
|
|
'body' => 'Guten Tag {{contact}},',
|
|
]);
|
|
|
|
$page = Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(CustomerDetail::class, ['uuid' => $customer->uuid])
|
|
->call('useTemplate', $template->uuid);
|
|
|
|
expect($page->get('subject'))->toBe('Ihre Anfrage — Beispiel GmbH')
|
|
->and($page->get('body'))->toBe('Guten Tag Bea Berger,')
|
|
// Where the operator now has to be to finish it.
|
|
->and($page->get('tab'))->toBe('compose');
|
|
});
|
|
|
|
it('does not overwrite a subject the operator already typed', function () {
|
|
// A template that eats work is a template nobody uses twice.
|
|
$customer = Customer::factory()->create();
|
|
$template = MailTemplate::create(['name' => 'X', 'subject' => 'Aus der Vorlage', 'body' => 'Text']);
|
|
|
|
$page = Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(CustomerDetail::class, ['uuid' => $customer->uuid])
|
|
->set('subject', 'Selbst geschrieben')
|
|
->call('useTemplate', $template->uuid);
|
|
|
|
expect($page->get('subject'))->toBe('Selbst geschrieben')
|
|
->and($page->get('body'))->toBe('Text');
|
|
});
|
|
|
|
it('will not insert a retired template', function () {
|
|
$customer = Customer::factory()->create();
|
|
$template = MailTemplate::create(['name' => 'X', 'subject' => 'S', 'body' => 'B', 'active' => false]);
|
|
|
|
$page = Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(CustomerDetail::class, ['uuid' => $customer->uuid])
|
|
->call('useTemplate', $template->uuid);
|
|
|
|
expect($page->get('body'))->toBe('');
|
|
});
|
|
|
|
it('sends what the operator saw, not what the template said', function () {
|
|
// The whole point of inserting rather than sending: the last two sentences
|
|
// are always specific to the person asking.
|
|
$customer = Customer::factory()->create(['name' => 'Beispiel GmbH']);
|
|
$template = MailTemplate::create(['name' => 'X', 'subject' => 'S', 'body' => 'Aus der Vorlage.']);
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(CustomerDetail::class, ['uuid' => $customer->uuid])
|
|
->call('useTemplate', $template->uuid)
|
|
->set('body', 'Aus der Vorlage. Und mein eigener Satz.')
|
|
->call('send');
|
|
|
|
expect(SentMail::query()->where('customer_id', $customer->id)->latest('id')->first()->body)
|
|
->toBe('Aus der Vorlage. Und mein eigener Satz.');
|
|
});
|