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.'); });