Skip to content

feat(email): post-execution notification email with one-click revocation link valid for the AWS cancel window #291

@cristim

Description

@cristim

Background

Today purchases follow either the email-approval path (default; the user clicks "Send for Approval", an email goes out, an approver clicks the link, then the purchase executes) or — once the sibling "permission-gated direct execute" issue lands — a direct path where a privileged user fires execution from the modal without an email round-trip.

In both cases, once execution actually fires, no notification email goes out. The user knows because they clicked the button; the approver knows because they clicked the link; everyone else (account owners, finance approvers who didn't initiate the request, the broader team) finds out when the AWS bill changes shape next month.

This issue closes that loop: post-execution, fire a notification email with the rec details + a one-click cancellation link valid for the AWS revocation window.

What needs to happen

Trigger

After executePurchase succeeds (regardless of whether it came from approval-email-click OR direct-execute permission), enqueue a SendPurchaseExecutedNotification (analogous to the existing SendPurchaseApprovalRequest at internal/api/handler_purchases.go:1167).

Recipients

  • The configured notification_email per the global config (internal/config/types.go::GlobalConfig.NotificationEmail).
  • Any per-account contact_email (the same set today's approval emails go to).
  • The created_by_user_id's email (so the requester gets a "your purchase fired" record even if they weren't the one who clicked the approval link).

Dedupe so a recipient on multiple lists gets one email.

Body content

  • Subject: [CUDly] Purchase executed: <provider>/<service> <SKU> in <account>
  • Body (mirrors the rec summary block from sibling "Pretty email approval/cancel links"):
    • Account, service, SKU, region, term, payment.
    • Cost: $X upfront / $Y/mo. Total over term: $Z.
    • Who initiated: Requested by <name> <email> and (for direct-execute) Executed without approval per <permission-name> grant.
    • When: ISO timestamp.
  • Cancellation panel:
    • A primary <a class="btn btn-secondary">Revoke this purchase</a> linking to a token-authed /api/purchases/{id}/revoke?token=… route.
    • A one-line note: "You can revoke this purchase until 2026-05-05 14:22 UTC (24h) — after that, contact AWS Support." (Window from the AWS-revert sibling issue.)
    • Plain-text fallback URL.

Token + route

Reuse the token machinery the approve/cancel routes already use (AuthPublic token-only auth). New route /api/purchases/{id}/revoke (or extend the existing approve/cancel route family) gated by:

  • Token validity (signed, short-lived, single-use — same shape).
  • Time window — reject after revocation_window_closes_at (set by the AWS-revert issue).
  • Revoker-email check — same verifyApproverSession pattern at handler_purchases.go:1351-1391.

For session-authed users the route also accepts a session that holds revoke-any:purchases (defined in the AWS-revert issue) — same dual-auth shape as the cancel path.

Acceptance

  • After a purchase executes (via either approval-email or direct-execute), an email lands in each configured recipient's inbox within ~30s.
  • The email contains the rec summary, the requester identity, and a one-click revocation link.
  • Clicking the link within the AWS revocation window triggers the revocation flow (sibling issue: AWS revert).
  • Clicking the link past the window returns a friendly "the cancellation window has closed" page rather than a stack trace.
  • A test simulates an execution + asserts the notification email was enqueued with the expected recipients + body fingerprints.

Sequencing

Depends on:

  • "Pretty email approval/cancel links" — provides the templating + button styling this issue reuses.
  • "AWS RI/SP revocation via support case" — provides the revocation backend + the revocation_window_closes_at field this email exposes.

Doesn't strictly depend on "Permission-gated direct execute" — even today's approval-only path benefits from a post-execution notification (the approver clicked the link an hour ago and forgot, the requester wasn't watching their inbox; the post-execution email is the audit-trail email).

References

  • internal/api/handler_purchases.go:1167 (SendPurchaseApprovalRequest — the existing pattern to mirror)
  • internal/api/handler_purchases.go:1351-1391 (the verifyApproverSession + dual-auth pattern for the revoke route)
  • internal/config/types.go::GlobalConfig.NotificationEmail (recipient default)
  • Per-account contact_email (recipient secondary)
  • Sibling: "Pretty email approval/cancel links" — shared templating
  • Sibling: "AWS RI/SP revocation via support case" — the revoke backend + the window field
  • Sibling: "Permission-gated direct execute" — direct-execute path benefits especially
  • Sibling: "Approve from History" — admins who approve from dashboard never see today's email; this notification reaches them post-execution

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions