Merge pull request #7 from Sea-Haven-Industries/feature/per-person-pay-rate

feat: Support per-person shift pay rates
This commit is contained in:
Adam Moussa 2026-04-07 18:12:48 -04:00 • committed by GitHub
commit 7718feb17e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 23 additions and 13 deletions

View file

@ -171,7 +171,7 @@ def create_app(bot_token: str, signing_secret: str) -> App:
prev_sunday = prev_monday + timedelta(days=6) prev_sunday = prev_monday + timedelta(days=6)
week_label = f"{prev_monday.strftime('%b %-d')} to {prev_sunday.strftime('%b %-d')}" week_label = f"{prev_monday.strftime('%b %-d')} to {prev_sunday.strftime('%b %-d')}"
blocks = build_pay_summary_blocks( 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) respond(blocks=blocks)
else: else:

View file

@ -120,22 +120,22 @@ def build_help_blocks() -> list[dict]:
return [{"type": "section", "text": {"type": "mrkdwn", "text": text}}] 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. """Build Block Kit blocks for the weekly pay summary.
Args: Args:
week_label: e.g. "Mar 30 to Apr 5" week_label: e.g. "Mar 30 to Apr 5"
pay_breakdown: list of {date, day, name, extension, rate} per shift pay_breakdown: list of {date, day, name, extension, rate} per shift
total_by_person: {name: {shifts: int, total: float, extension: str}} total_by_person: {name: {shifts: int, total: float, extension: str, rate: float}}
rate: flat rate per shift
""" """
lines = [f"_Rate: ${rate:.2f} per shift_\n"] lines = []
for entry in pay_breakdown: 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:*") lines.append("\n*Totals:*")
for name, info in sorted(total_by_person.items()): 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 = [ blocks = [
{ {

View file

@ -127,7 +127,15 @@ class ShiftSchedule:
resp = self.table.get_item(Key={"PK": "CONFIG", "SK": "CONFIG"}) resp = self.table.get_item(Key={"PK": "CONFIG", "SK": "CONFIG"})
return resp.get("Item", {}) return resp.get("Item", {})
def get_shift_rate(self) -> float: def get_shift_rate(self, extension: str | None = None) -> float:
"""Get the flat per-shift pay rate from config.""" """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() config = self.get_config()
return float(config.get("shift_rate", 0)) return float(config.get("shift_rate", 0))

View file

@ -22,7 +22,7 @@ DAY_ORDER = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday",
def _calculate_weekly_pay(schedule: ShiftSchedule, week_start: datetime) -> dict: def _calculate_weekly_pay(schedule: ShiftSchedule, week_start: datetime) -> dict:
"""Calculate pay for a Mon–Sun week. Returns pay record dict.""" """Calculate pay for a Mon–Sun week. Returns pay record dict."""
rate = schedule.get_shift_rate() default_rate = schedule.get_shift_rate()
breakdown = [] breakdown = []
totals = {} totals = {}
@ -35,6 +35,8 @@ def _calculate_weekly_pay(schedule: ShiftSchedule, week_start: datetime) -> dict
if source == "available": if source == "available":
continue continue
rate = schedule.get_shift_rate(ext)
breakdown.append({ breakdown.append({
"date": date_str, "date": date_str,
"date_label": date.strftime("%b %-d"), "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: 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]["shifts"] += 1
totals[name]["total"] += rate totals[name]["total"] += rate
return { return {
"week_start": week_start.strftime("%Y-%m-%d"), "week_start": week_start.strftime("%Y-%m-%d"),
"rate": str(rate), "default_rate": str(default_rate),
"breakdown": breakdown, "breakdown": breakdown,
"totals": totals, "totals": totals,
} }
@ -88,7 +90,7 @@ def handler(event, context):
prev_sunday = prev_monday + timedelta(days=6) prev_sunday = prev_monday + timedelta(days=6)
week_label = f"{prev_monday.strftime('%b %-d')} to {prev_sunday.strftime('%b %-d')}" week_label = f"{prev_monday.strftime('%b %-d')} to {prev_sunday.strftime('%b %-d')}"
pay_blocks = build_pay_summary_blocks( 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( slack.chat_postMessage(
channel=channel_id, channel=channel_id,