Use the organisation identity across every client-facing email (PMS-761) #513
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/PMS-761-org-identity-across-email"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes PMS-761.
Most of that story had already shipped: the organisation record holds contact name, phone, email and logo (MAPPS-429, PMS-755), onboarding requires them (PMS-752), settings edits them (PMS-758), and the request-form email renders the lot (PMS-747, PMS-748). What was missing is the other half of its title: use it across the system.
Every other client-facing email was anonymous
send_quote_ready(PMS-673)send_invoice_pay_now(PMS-711)send_note_email(PMS-15)None named the MSP or offered a way to reach them, and
Fromis the deployment-wideSMTP_FROM, so it did not identify the sender either. All three now open by naming the organisation and close with the contact line. The quote and invoice name it in the subject too, since those arrive cold and the subject is what a recipient decides to open on; the ticket-note subject is left alone because it is the thread key for a conversation the client is already in.The composition helpers lived privately inside
modules::forms::request_links, which is exactly why no other email could use them. They are nowmodules::tenants::identity::OrgIdentity, and the request-form path reads through it rather than keeping a copy.And the ticket-note email was not being sent at all
Found while wiring the identity into it.
ticket.note_addedwas seeded by migration 021 for the default tenant only, and unlike appointment/SLA (030), auth (097) and the request link (101) it never got a backfill nor a place inTenantService::copy_default_config.dispatchresolves rules by (tenant_id, event_type) and skips silently when there are none, so for every real tenant it fanned out to zero recipients while the note row was still stampedis_email_sent = TRUE. The UI has been reporting delivery of messages that were never sent.The same shape, one step less severe, for
forms.request_link: PMS-730 added its template to the copy list but not its rule, so any tenant created since has had the template sitting unreachable. Migration 104 backfills both, guarded the same way as 030 and 097, and the copy list gains both events.Deliberately not done
send_new_login_locationandsend_login_approval_codeare mokosh speaking to its own user about their account; a tenant's name and logo on "approve your sign-in" is what phishing looks like. A unit test asserts they carry no organisation identity.Fromheader. It is the right end state and it is not a code change: it needs a sending domain per tenant with SPF, DKIM and a bounce path, or the mail lands in spam.Verification
just checkclean (including the five script gates), 370 unit tests, and the full Postgres integration suite green. New coverage: the mailer bodies and subjects for both money emails, the security emails staying unbranded,OrgIdentity::loadreading the caller's own tenant, a new tenant actually having a usable rule for both client-facing events, and the seeded ticket-note body referencing the keys the service supplies.One duplication is left in place and now commented: the no-dispatcher fallback in
send_note_emailcomposes the same copy as migration 104's template. PMS-700 removed exactly this for the auth emails, but that fallback has no template to read. Change one and change the other.🤖 Generated with Claude Code
https://claude.ai/code/session_01X29MwLvt6mgSKrpqzh5zqB
The organisation's name, contact person, phone, email and logo were assembled by private helpers inside `modules::forms::request_links`. That is why the request-form email is the only client-facing message mokosh sends that identifies the MSP: no other caller could reach the wording, so the quote, invoice and ticket-note emails said nothing about who was writing. `OrgIdentity` moves the loader and the sentence composition into `modules::tenants::identity`, where any caller can use it. It normalises on the way in (trim, whitespace-only reads as unset) so a caller never has to decide whether `Some("")` means set, and keeps its fields private to hold that invariant. `contact_line` now takes the caller's opening question, because only the caller knows whether the message is about a request form, a quote or an invoice. It is deliberately outside this module's `multi-tenant` gate: its callers are unconditional, and a single-tenant build still has an organisation with a name. No behaviour change for the request-form email. Its wording, its per-form contact override and its logo block are the same code, reached through the shared type; the unit tests that covered those helpers move with them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X29MwLvt6mgSKrpqzh5zqB