'templates.token.customer', 'contact' => 'templates.token.contact', 'email' => 'templates.token.email', 'plan' => 'templates.token.plan', 'amount' => 'templates.token.amount', 'instance' => 'templates.token.instance', 'operator' => 'templates.token.operator', 'company' => 'templates.token.company', ]; /** * Subject and body, filled in. * * @return array{subject: string, body: string} */ public function render(MailTemplate $template, Customer $customer, ?Operator $operator = null): array { $values = $this->values($customer, $operator); return [ 'subject' => $this->fill($template->subject, $values), 'body' => $this->fill($template->body, $values), ]; } /** * What each placeholder stands for, for this customer. * * @return array */ public function values(Customer $customer, ?Operator $operator = null): array { $subscription = Subscription::query() ->where('customer_id', $customer->id) ->whereIn('status', ['active', 'past_due']) ->latest('id') ->first(); $instance = Instance::query() ->where('customer_id', $customer->id) ->whereNotIn('status', ['ended', 'failed']) ->latest('id') ->first(); return [ 'customer' => (string) $customer->name, // The person, where one is on record; the company otherwise. A // salutation is written to somebody, and "Guten Tag Beispiel GmbH" // is how a mail announces that it came out of a database. 'contact' => (string) ($customer->contact_name ?: $customer->name), 'email' => (string) $customer->email, 'plan' => $subscription === null ? '' : __('billing.plan.'.$subscription->plan), // What was actually charged, because that is the figure the customer // knows: it is what their bank statement says. 'amount' => $subscription === null ? '' : Number::currency( $this->chargedCents($subscription, $customer) / 100, in: (string) ($subscription->currency ?: 'EUR'), locale: app()->getLocale(), ), 'instance' => (string) ($instance?->subdomain ?? ''), 'operator' => (string) ($operator?->name ?? ''), 'company' => (string) CompanyProfile::get('name', ''), ]; } /** @param array $values */ private function fill(string $text, array $values): string { return preg_replace_callback( // Whitespace inside the braces is tolerated: an operator typing // "{{ customer }}" means the same thing as "{{customer}}", and // failing on a space would be a rule nobody can see. '/\{\{\s*([a-z_]+)\s*\}\}/i', // Unknown tokens are left exactly as they were — see the class // docblock. The operator reads this before anything is sent. fn (array $m) => $values[strtolower($m[1])] ?? $m[0], $text, ) ?? $text; } /** * The contract's monthly figure as this customer is charged it. * * Through TaxTreatment, which is the one place the amount taken is formed, and * therefore the same figure the Stripe Price for this contract carries. It used * to be a multiplication at the domestic rate written out here, with the * reasoning that reverse charge was a matter for documents and not for * sentences — and that stopped being true the moment a reverse-charge business * began to be charged the bare net. A mail naming a fifth more than the bank * statement is a mail the customer writes back about. */ private function chargedCents(Subscription $subscription, Customer $customer): int { return TaxTreatment::for($customer)->chargeCents((int) $subscription->price_cents); } }