feat(invitations): email the invitee on invite create (PMS-246) #181

Merged
longjacksonle merged 2 commits from feat/mapps-246-invite-email into main 2026-06-11 19:14:12 +02:00

What

Closes the delivery gap from PMS-244: invites existed in the API but nobody was notified. Creating an invite now emails the invitee.

How

  • The email is enqueued as a notifications row (channel=email, explicit recipient = the invitee, pre-rendered subject/body) in the same transaction as the invite. The existing dispatcher worker drains it via the SMTP mailer with its retry/backoff - the same path the codebase uses for password-reset / welcome mail. The invitee isn't a Mokosh user yet, so the row carries an explicit recipient, not a user_id.
  • Link target decision: acceptance stays login-driven (PMS-244), so the link is just the Mokosh login (the SPA origin / client_origin) - the invite resolves by verified email on the invitee's next sign-in. No token-accept page in this minimal cut. The body names the tenant + role and states the 14-day expiry.
  • InvitationsService::with_app_url(spa_origin) gates email on a configured SPA URL; the router enables it, existing tests (new(db)) send nothing.

On the "no mailer wired" note

The ticket flagged no mailer - but the notifications system already has one (utils::email::Mailer / SmtpMailer, drained by the dispatcher worker). This reuses it; no new transport needed.

Verification

cargo check --all-targets + cargo clippy --all-targets clean. tests/invitations.rs adds create_with_app_url_enqueues_invite_email (email row enqueued with the link when configured; none when unset). Full invitations suite (6) passes.

🤖 Generated with Claude Code

## What Closes the delivery gap from PMS-244: invites existed in the API but nobody was notified. Creating an invite now emails the invitee. ## How - The email is enqueued as a `notifications` row (channel=email, explicit recipient = the invitee, pre-rendered subject/body) in the same transaction as the invite. The existing dispatcher worker drains it via the SMTP mailer with its retry/backoff - the same path the codebase uses for password-reset / welcome mail. The invitee isn't a Mokosh user yet, so the row carries an explicit `recipient`, not a `user_id`. - **Link target decision**: acceptance stays login-driven (PMS-244), so the link is just the Mokosh login (the SPA origin / `client_origin`) - the invite resolves by verified email on the invitee's next sign-in. No token-accept page in this minimal cut. The body names the tenant + role and states the 14-day expiry. - `InvitationsService::with_app_url(spa_origin)` gates email on a configured SPA URL; the router enables it, existing tests (`new(db)`) send nothing. ## On the "no mailer wired" note The ticket flagged no mailer - but the notifications system already has one (`utils::email::Mailer` / `SmtpMailer`, drained by the dispatcher worker). This reuses it; no new transport needed. ## Verification `cargo check --all-targets` + `cargo clippy --all-targets` clean. `tests/invitations.rs` adds `create_with_app_url_enqueues_invite_email` (email row enqueued with the link when configured; none when unset). Full invitations suite (6) passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(invitations): email the invitee on invite create (PMS-246)
Some checks failed
Check / fmt + clippy + compile + tests (pull_request) Failing after 16s
E2E (staging) / Playwright against staging (pull_request) Successful in 59s
Build OCI container / Build and push mokosh-api image (push) Successful in 3m55s
9a752a6b55
Closes the delivery gap from PMS-244: invites existed in the API but nobody was told. Creating an invite now emails the invitee.

The email is enqueued as a `notifications` row (channel=email, explicit recipient = the invitee, pre-rendered subject/body) in the same transaction as the invite; the existing dispatcher worker drains it through the SMTP mailer, with its retry/backoff - the same path the codebase already uses for password-reset / welcome mail. The invitee is not a Mokosh user yet, so the row carries an explicit recipient rather than a user_id.

Acceptance stays login-driven (PMS-244): the link is just the Mokosh login (the SPA origin / `client_origin`), and the invite resolves by verified email on the invitee's next sign-in - no token-accept page in this minimal cut. The body names the tenant + role and states the 14-day expiry.

Wiring: `InvitationsService::with_app_url(spa_origin)` gates email on a configured SPA URL, so the router enables it while existing tests (which use `new(db)`) send nothing.

Verification: `cargo check --all-targets` + `cargo clippy --all-targets` clean; `tests/invitations.rs` adds `create_with_app_url_enqueues_invite_email` (email row enqueued with the link when configured; none when unset). Full invitations suite (6) passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into feat/mapps-246-invite-email
Some checks failed
Check / fmt + clippy + compile + tests (pull_request) Failing after 18s
Create release / Create release from merged PR (pull_request) Has been skipped
E2E (staging) / Playwright against staging (pull_request) Successful in 49s
Build OCI container / Build and push mokosh-api image (push) Successful in 3m57s
9d474ed7fe
longjacksonle deleted branch feat/mapps-246-invite-email 2026-06-11 19:14:12 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!181
No description provided.