mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 10:13:11 +00:00
Each roster entry can now have its own shift_rate, falling back to the global CONFIG rate when not set. Pay breakdowns show individual rates per shift and per-person totals.
113 lines
3.6 KiB
Python
113 lines
3.6 KiB
Python
"""Lambda handler — posts the weekly on-call schedule and previous week's pay summary
|
||
to Slack every Monday at 7am ET."""
|
||
|
||
import logging
|
||
import os
|
||
from datetime import datetime, timedelta
|
||
from zoneinfo import ZoneInfo
|
||
|
||
import boto3
|
||
from slack_sdk import WebClient
|
||
|
||
from src.blocks import build_pay_summary_blocks, build_week_schedule
|
||
from src.schedule import ShiftSchedule
|
||
|
||
logger = logging.getLogger()
|
||
logger.setLevel(logging.INFO)
|
||
|
||
EASTERN = ZoneInfo("America/New_York")
|
||
|
||
DAY_ORDER = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||
|
||
|
||
def _calculate_weekly_pay(schedule: ShiftSchedule, week_start: datetime) -> dict:
|
||
"""Calculate pay for a Mon–Sun week. Returns pay record dict."""
|
||
default_rate = schedule.get_shift_rate()
|
||
breakdown = []
|
||
totals = {}
|
||
|
||
for i in range(7):
|
||
date = week_start + timedelta(days=i)
|
||
date_str = date.strftime("%Y-%m-%d")
|
||
day_name = DAY_ORDER[i]
|
||
ext, name, source = schedule.resolve_shift(date_str, day_name)
|
||
|
||
if source == "available":
|
||
continue
|
||
|
||
rate = schedule.get_shift_rate(ext)
|
||
|
||
breakdown.append({
|
||
"date": date_str,
|
||
"date_label": date.strftime("%b %-d"),
|
||
"day": day_name[:3],
|
||
"name": name,
|
||
"extension": ext,
|
||
"rate": rate,
|
||
})
|
||
|
||
if name not in totals:
|
||
totals[name] = {"shifts": 0, "total": 0.0, "extension": ext, "rate": rate}
|
||
totals[name]["shifts"] += 1
|
||
totals[name]["total"] += rate
|
||
|
||
return {
|
||
"week_start": week_start.strftime("%Y-%m-%d"),
|
||
"default_rate": str(default_rate),
|
||
"breakdown": breakdown,
|
||
"totals": totals,
|
||
}
|
||
|
||
|
||
def handler(event, context):
|
||
now = datetime.now(EASTERN)
|
||
|
||
# DST guard — same pattern as the 3CX scheduler
|
||
if now.hour != 7:
|
||
logger.info("ET hour is %d, not 7am — skipping (wrong DST rule fired)", now.hour)
|
||
return {"skipped": True}
|
||
|
||
ssm = boto3.client("ssm")
|
||
bot_token = ssm.get_parameter(
|
||
Name=os.environ["SLACK_BOT_TOKEN_PARAM"], WithDecryption=True
|
||
)["Parameter"]["Value"]
|
||
channel_id = ssm.get_parameter(
|
||
Name=os.environ["SHIFT_CHANNEL_PARAM"], WithDecryption=True
|
||
)["Parameter"]["Value"]
|
||
|
||
schedule = ShiftSchedule()
|
||
slack = WebClient(token=bot_token)
|
||
|
||
# --- Previous week's pay summary ---
|
||
prev_monday = now - timedelta(days=7)
|
||
prev_monday = prev_monday.replace(hour=0, minute=0, second=0, microsecond=0)
|
||
pay_record = _calculate_weekly_pay(schedule, prev_monday)
|
||
|
||
if pay_record["breakdown"]:
|
||
week_key = prev_monday.strftime("%Y-%m-%d")
|
||
schedule.save_pay_record(week_key, pay_record)
|
||
|
||
prev_sunday = prev_monday + timedelta(days=6)
|
||
week_label = f"{prev_monday.strftime('%b %-d')} to {prev_sunday.strftime('%b %-d')}"
|
||
pay_blocks = build_pay_summary_blocks(
|
||
week_label, pay_record["breakdown"], pay_record["totals"]
|
||
)
|
||
slack.chat_postMessage(
|
||
channel=channel_id,
|
||
blocks=pay_blocks,
|
||
text=f"After-Hours Pay — {week_label}",
|
||
)
|
||
logger.info("Posted pay summary for week of %s", week_key)
|
||
|
||
# --- Two-week schedule ---
|
||
blocks = build_week_schedule(schedule, start_date=now)
|
||
|
||
end_date = now + timedelta(days=13)
|
||
slack.chat_postMessage(
|
||
channel=channel_id,
|
||
blocks=blocks,
|
||
text=f"After-Hours Schedule — {now.strftime('%b %-d')} to {end_date.strftime('%b %-d')}",
|
||
)
|
||
|
||
logger.info("Posted weekly schedule to channel %s", channel_id)
|
||
return {"posted": True, "channel": channel_id, "pay_calculated": bool(pay_record["breakdown"])}
|