Email weekly pay summary to payroll via SES on Mondays

Sends an HTML pay summary email to configured payroll recipients
alongside the existing Slack post. Uses SES with noreply@seahaven.com
as sender. Recipients configured via PAYROLL_RECIPIENTS env var
(set to adam@seahaven.com for testing, switch to payroll@seahaven.com
for production).

Closes #3
This commit is contained in:
Adam Moussa 2026-04-07 18:54:19 -04:00
parent 093bb43e71
commit 9deb8bf251
2 changed files with 72 additions and 2 deletions

View file

@ -1,5 +1,5 @@
"""Lambda handler — posts the weekly on-call schedule and previous week's pay summary
to Slack every Monday at 7am ET."""
to Slack every Monday at 7am ET, and emails the pay summary to payroll."""
import logging
import os
@ -74,6 +74,66 @@ def _calculate_weekly_pay(schedule: ShiftSchedule, week_start: datetime) -> dict
}
def _build_pay_email_html(week_label: str, pay_record: dict) -> str:
"""Build an HTML email body for the weekly pay summary."""
rows = ""
for entry in pay_record["breakdown"]:
rows += (
f"<tr><td>{entry['day']}</td><td>{entry['date_label']}</td>"
f"<td>{entry['name']}</td><td>Ext {entry['extension']}</td>"
f"<td>${entry['rate']:.2f}</td></tr>\n"
)
totals_rows = ""
for name, info in sorted(pay_record["totals"].items()):
totals_rows += (
f"<tr><td>{name}</td><td>{info['shifts']}</td>"
f"<td>${info.get('rate', 0):.2f}</td><td><strong>${info['total']:.2f}</strong></td></tr>\n"
)
return f"""<html>
<body style="font-family: Arial, sans-serif; color: #333;">
<h2>After-Hours Pay Summary &mdash; {week_label}</h2>
<h3>Shift Breakdown</h3>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse: collapse;">
<tr style="background: #f0f0f0;"><th>Day</th><th>Date</th><th>Name</th><th>Extension</th><th>Rate</th></tr>
{rows}</table>
<h3>Totals by Person</h3>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse: collapse;">
<tr style="background: #f0f0f0;"><th>Name</th><th>Shifts</th><th>Rate</th><th>Total</th></tr>
{totals_rows}</table>
<p style="color: #888; font-size: 12px;">This is an automated report from the After-Hours Shift Manager.</p>
</body>
</html>"""
def _send_pay_email(week_label: str, pay_record: dict) -> None:
"""Send the weekly pay summary email via SES."""
sender = os.environ.get("SES_SENDER", "noreply@seahaven.com")
recipients = os.environ.get("PAYROLL_RECIPIENTS", "").split(",")
recipients = [r.strip() for r in recipients if r.strip()]
if not recipients:
logger.warning("No PAYROLL_RECIPIENTS configured — skipping email")
return
ses = boto3.client("ses")
html_body = _build_pay_email_html(week_label, pay_record)
ses.send_email(
Source=sender,
Destination={"ToAddresses": recipients},
Message={
"Subject": {"Data": f"After-Hours Pay Summary — {week_label}"},
"Body": {"Html": {"Data": html_body}},
},
)
logger.info("Sent pay email to %s", recipients)
def handler(event, context):
now = datetime.now(EASTERN)
@ -114,6 +174,9 @@ def handler(event, context):
)
logger.info("Posted pay summary for week of %s", week_key)
# Email pay summary to payroll
_send_pay_email(week_label, pay_record)
# --- Two-week schedule ---
blocks = build_week_schedule(schedule, start_date=now)

View file

@ -89,9 +89,11 @@ Resources:
SHIFT_TABLE: !Ref ShiftTable
SLACK_BOT_TOKEN_PARAM: /afterhours-shift-manager/slack-bot-token
SHIFT_CHANNEL_PARAM: /afterhours-shift-manager/channel-id
SES_SENDER: noreply@seahaven.com
PAYROLL_RECIPIENTS: adam@seahaven.com
TZ: !Ref Timezone
Policies:
- DynamoDBReadPolicy:
- DynamoDBCrudPolicy:
TableName: !Ref ShiftTable
- Statement:
- Effect: Allow
@ -106,6 +108,11 @@ Resources:
Condition:
StringEquals:
"kms:ViaService": !Sub "ssm.${AWS::Region}.amazonaws.com"
- Effect: Allow
Action:
- ses:SendEmail
Resource:
- !Sub "arn:aws:ses:${AWS::Region}:${AWS::AccountId}:identity/seahaven.com"
Events:
# EST: 7am ET = 12:00 UTC (Nov-Mar)
WeeklyPostEST: