From 1399be7e74393b3fc5ba1daf74de12209d107537 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:12:32 -0400 Subject: [PATCH] Support per-person shift pay rates 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. --- src/app.py | 2 +- src/blocks.py | 12 ++++++------ src/schedule.py | 12 ++++++++++-- src/weekly_post.py | 10 ++++++---- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/app.py b/src/app.py index 6adbfa0..8d0a303 100644 --- a/src/app.py +++ b/src/app.py @@ -171,7 +171,7 @@ def create_app(bot_token: str, signing_secret: str) -> App: prev_sunday = prev_monday + timedelta(days=6) week_label = f"{prev_monday.strftime('%b %-d')} to {prev_sunday.strftime('%b %-d')}" blocks = build_pay_summary_blocks( - week_label, pay_record["breakdown"], pay_record["totals"], float(pay_record["rate"]) + week_label, pay_record["breakdown"], pay_record["totals"] ) respond(blocks=blocks) else: diff --git a/src/blocks.py b/src/blocks.py index e54a771..c8b5998 100644 --- a/src/blocks.py +++ b/src/blocks.py @@ -120,22 +120,22 @@ def build_help_blocks() -> list[dict]: return [{"type": "section", "text": {"type": "mrkdwn", "text": text}}] -def build_pay_summary_blocks(week_label: str, pay_breakdown: list[dict], total_by_person: dict[str, dict], rate: float) -> list[dict]: +def build_pay_summary_blocks(week_label: str, pay_breakdown: list[dict], total_by_person: dict[str, dict]) -> list[dict]: """Build Block Kit blocks for the weekly pay summary. Args: week_label: e.g. "Mar 30 to Apr 5" pay_breakdown: list of {date, day, name, extension, rate} per shift - total_by_person: {name: {shifts: int, total: float, extension: str}} - rate: flat rate per shift + total_by_person: {name: {shifts: int, total: float, extension: str, rate: float}} """ - lines = [f"_Rate: ${rate:.2f} per shift_\n"] + lines = [] for entry in pay_breakdown: - lines.append(f"{entry['day']} {entry['date_label']} — {entry['name']} (Ext {entry['extension']})") + lines.append(f"{entry['day']} {entry['date_label']} — {entry['name']} (Ext {entry['extension']}) — ${entry['rate']:.2f}") lines.append("\n*Totals:*") for name, info in sorted(total_by_person.items()): - lines.append(f"• {name} — {info['shifts']} shift{'s' if info['shifts'] != 1 else ''} — *${info['total']:.2f}*") + rate_note = f" @ ${info['rate']:.2f}/shift" if info.get("rate") else "" + lines.append(f"• {name} — {info['shifts']} shift{'s' if info['shifts'] != 1 else ''}{rate_note} — *${info['total']:.2f}*") blocks = [ { diff --git a/src/schedule.py b/src/schedule.py index 148e26e..b98351a 100644 --- a/src/schedule.py +++ b/src/schedule.py @@ -127,7 +127,15 @@ class ShiftSchedule: resp = self.table.get_item(Key={"PK": "CONFIG", "SK": "CONFIG"}) return resp.get("Item", {}) - def get_shift_rate(self) -> float: - """Get the flat per-shift pay rate from config.""" + def get_shift_rate(self, extension: str | None = None) -> float: + """Get the per-shift pay rate. + + Checks the roster entry for a person-specific rate first, + then falls back to the global config rate. + """ + if extension: + employee = self.get_employee_by_extension(extension) + if employee and employee.get("shift_rate"): + return float(employee["shift_rate"]) config = self.get_config() return float(config.get("shift_rate", 0)) diff --git a/src/weekly_post.py b/src/weekly_post.py index 965d095..7d361f0 100644 --- a/src/weekly_post.py +++ b/src/weekly_post.py @@ -22,7 +22,7 @@ DAY_ORDER = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", def _calculate_weekly_pay(schedule: ShiftSchedule, week_start: datetime) -> dict: """Calculate pay for a Mon–Sun week. Returns pay record dict.""" - rate = schedule.get_shift_rate() + default_rate = schedule.get_shift_rate() breakdown = [] totals = {} @@ -35,6 +35,8 @@ def _calculate_weekly_pay(schedule: ShiftSchedule, week_start: datetime) -> dict if source == "available": continue + rate = schedule.get_shift_rate(ext) + breakdown.append({ "date": date_str, "date_label": date.strftime("%b %-d"), @@ -45,13 +47,13 @@ def _calculate_weekly_pay(schedule: ShiftSchedule, week_start: datetime) -> dict }) if name not in totals: - totals[name] = {"shifts": 0, "total": 0.0, "extension": ext} + 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"), - "rate": str(rate), + "default_rate": str(default_rate), "breakdown": breakdown, "totals": totals, } @@ -88,7 +90,7 @@ def handler(event, context): 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"], float(pay_record["rate"]) + week_label, pay_record["breakdown"], pay_record["totals"] ) slack.chat_postMessage( channel=channel_id,