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
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
executePurchasesucceeds (regardless of whether it came from approval-email-click OR direct-execute permission), enqueue aSendPurchaseExecutedNotification(analogous to the existingSendPurchaseApprovalRequestatinternal/api/handler_purchases.go:1167).Recipients
notification_emailper the global config (internal/config/types.go::GlobalConfig.NotificationEmail).contact_email(the same set today's approval emails go to).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
[CUDly] Purchase executed: <provider>/<service> <SKU> in <account>$X upfront / $Y/mo. Total over term:$Z.Requested by <name> <email>and (for direct-execute)Executed without approval per <permission-name> grant.<a class="btn btn-secondary">Revoke this purchase</a>linking to a token-authed/api/purchases/{id}/revoke?token=…route.Token + route
Reuse the token machinery the approve/cancel routes already use (
AuthPublictoken-only auth). New route/api/purchases/{id}/revoke(or extend the existing approve/cancel route family) gated by:revocation_window_closes_at(set by the AWS-revert issue).verifyApproverSessionpattern athandler_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
Sequencing
Depends on:
revocation_window_closes_atfield 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)contact_email(recipient secondary)