mirror of
https://github.com/Sea-Haven-Industries/afterhours-shift-manager.git
synced 2026-09-30 14:53:12 +00:00
Saturday night stays in the week that ends Saturday, and the first Flex close skips dates already sent.
236 lines
10 KiB
Python
236 lines
10 KiB
Python
"""Tests for weekly-post pay calculation helpers."""
|
|
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
|
|
# 2026-06-01 is a Monday. Day names come from the date, so a week may start any day.
|
|
WEEK_START = datetime(2026, 6, 1)
|
|
# Sunday that opens the week containing Monday 2026-06-01.
|
|
SUNDAY_WEEK = datetime(2026, 5, 31)
|
|
|
|
|
|
class TestAddShiftToPay:
|
|
def test_assigned_shift_is_added(self, weeklypost_app, schedule, seed):
|
|
seed.config(shift_rate="50")
|
|
seed.weekly("Monday", "114", "Alice")
|
|
breakdown, totals = [], {}
|
|
weeklypost_app._add_shift_to_pay(
|
|
schedule, breakdown, totals, datetime(2026, 6, 1), "Monday"
|
|
)
|
|
assert len(breakdown) == 1
|
|
assert breakdown[0]["name"] == "Alice"
|
|
assert breakdown[0]["rate"] == Decimal("50")
|
|
assert totals["Alice"]["shifts"] == 1
|
|
assert totals["Alice"]["total"] == Decimal("50")
|
|
|
|
def test_available_shift_is_skipped(self, weeklypost_app, schedule, seed):
|
|
seed.config(shift_rate="50")
|
|
breakdown, totals = [], {}
|
|
weeklypost_app._add_shift_to_pay(
|
|
schedule, breakdown, totals, datetime(2026, 6, 1), "Monday"
|
|
)
|
|
assert breakdown == [] and totals == {}
|
|
|
|
def test_weekend_day_label(self, weeklypost_app, schedule, seed):
|
|
seed.config(shift_rate="50")
|
|
seed.weekly("Saturday", "200", "Alice", shift_type="day")
|
|
breakdown, totals = [], {}
|
|
weeklypost_app._add_shift_to_pay(
|
|
schedule, breakdown, totals, datetime(2026, 6, 6), "Saturday", "day"
|
|
)
|
|
assert breakdown[0]["day"] == "Sat (Day)"
|
|
|
|
|
|
class TestCalculateWeeklyPay:
|
|
def test_single_weekday_shift(self, weeklypost_app, schedule, seed):
|
|
seed.config(shift_rate="50")
|
|
seed.weekly("Monday", "114", "Alice")
|
|
record = weeklypost_app._calculate_weekly_pay(schedule, WEEK_START)
|
|
assert record["week_start"] == "2026-06-01"
|
|
assert len(record["breakdown"]) == 1
|
|
assert record["totals"]["Alice"]["total"] == Decimal("50")
|
|
|
|
def test_per_employee_rate_applied(self, weeklypost_app, schedule, seed):
|
|
seed.config(shift_rate="50")
|
|
seed.roster("114", "Alice", shift_rate="75")
|
|
seed.weekly("Monday", "114", "Alice")
|
|
record = weeklypost_app._calculate_weekly_pay(schedule, WEEK_START)
|
|
assert record["totals"]["Alice"]["total"] == Decimal("75.0")
|
|
|
|
def test_weekend_counts_day_and_night(self, weeklypost_app, schedule, seed):
|
|
seed.config(shift_rate="50")
|
|
seed.weekly("Saturday", "200", "Alice", shift_type="day")
|
|
seed.weekly("Saturday", "200", "Alice", shift_type="night")
|
|
record = weeklypost_app._calculate_weekly_pay(schedule, WEEK_START)
|
|
# Alice worked both Saturday shifts → 2 shifts, $100.
|
|
assert record["totals"]["Alice"]["shifts"] == 2
|
|
assert record["totals"]["Alice"]["total"] == Decimal("100")
|
|
|
|
def test_saturday_night_stays_in_week_ending_saturday(
|
|
self, weeklypost_app, schedule, seed
|
|
):
|
|
# Sat 2026-06-06 night runs into Sunday morning and stays on Saturday.
|
|
# Sun 2026-06-07 day opens the next week. Sun 2026-05-31 day is this week.
|
|
seed.config(shift_rate="50")
|
|
seed.weekly("Saturday", "200", "Alice", shift_type="night")
|
|
seed.weekly("Sunday", "201", "Bob", shift_type="day")
|
|
record = weeklypost_app._calculate_weekly_pay(schedule, SUNDAY_WEEK)
|
|
by_date = {line["date"]: line for line in record["breakdown"]}
|
|
assert by_date["2026-06-06"]["name"] == "Alice"
|
|
assert by_date["2026-06-06"]["day"].startswith("Sat")
|
|
assert by_date["2026-05-31"]["name"] == "Bob"
|
|
assert "2026-06-07" not in by_date
|
|
|
|
def test_sunday_day_opens_the_next_week(self, weeklypost_app, schedule, seed):
|
|
seed.config(shift_rate="50")
|
|
seed.weekly("Saturday", "200", "Alice", shift_type="night")
|
|
seed.weekly("Sunday", "201", "Bob", shift_type="day")
|
|
record = weeklypost_app._calculate_weekly_pay(schedule, datetime(2026, 6, 7))
|
|
by_date = {line["date"]: line for line in record["breakdown"]}
|
|
assert by_date["2026-06-07"]["name"] == "Bob"
|
|
assert by_date["2026-06-13"]["name"] == "Alice"
|
|
assert "2026-06-06" not in by_date
|
|
|
|
def test_empty_week_has_no_breakdown(self, weeklypost_app, schedule, seed):
|
|
seed.config(shift_rate="50")
|
|
record = weeklypost_app._calculate_weekly_pay(schedule, WEEK_START)
|
|
assert record["breakdown"] == [] and record["totals"] == {}
|
|
|
|
|
|
class TestHolidayPay:
|
|
def test_single_holiday_assignee(self, weeklypost_app, schedule, seed):
|
|
# Wed 2026-06-03 holiday; one assignee at base $50 * 1.5 = $75.
|
|
seed.config(shift_rate="50")
|
|
seed.holiday(
|
|
"2026-06-03",
|
|
slots=2,
|
|
label="Founders Day",
|
|
assignees={"114": {"name": "Alice", "claimed_at": "x"}},
|
|
)
|
|
breakdown, totals = [], {}
|
|
weeklypost_app._add_shift_to_pay(
|
|
schedule, breakdown, totals, datetime(2026, 6, 3), "Wednesday", "day"
|
|
)
|
|
assert len(breakdown) == 1
|
|
line = breakdown[0]
|
|
assert line["name"] == "Alice"
|
|
assert line["is_holiday"] is True
|
|
assert line["holiday_label"] == "Founders Day"
|
|
assert line["multiplier"] == Decimal("1.5")
|
|
assert line["base_rate"] == Decimal("50")
|
|
# rate is the effective (multiplied) pay for Slack-block compatibility.
|
|
assert line["rate"] == Decimal("75.0")
|
|
assert line["amount"] == Decimal("75.0")
|
|
assert isinstance(line["multiplier"], Decimal)
|
|
assert isinstance(line["amount"], Decimal)
|
|
assert totals["Alice"]["shifts"] == 1
|
|
assert totals["Alice"]["holiday_shifts"] == 1
|
|
assert totals["Alice"]["total"] == Decimal("75.0")
|
|
|
|
def test_two_assignees_same_holiday(self, weeklypost_app, schedule, seed):
|
|
# Two slots filled on the same holiday; each paid base * 1.5.
|
|
seed.config(shift_rate="40")
|
|
seed.roster("114", "Alice", shift_rate="40")
|
|
seed.roster("200", "Bob", shift_rate="60")
|
|
seed.holiday(
|
|
"2026-06-03",
|
|
slots=2,
|
|
label="Founders Day",
|
|
assignees={
|
|
"114": {"name": "Alice", "claimed_at": "x"},
|
|
"200": {"name": "Bob", "claimed_at": "y"},
|
|
},
|
|
)
|
|
breakdown, totals = [], {}
|
|
weeklypost_app._add_shift_to_pay(
|
|
schedule, breakdown, totals, datetime(2026, 6, 3), "Wednesday", "day"
|
|
)
|
|
assert len(breakdown) == 2
|
|
assert all(line["is_holiday"] for line in breakdown)
|
|
by_name = {line["name"]: line for line in breakdown}
|
|
assert by_name["Alice"]["amount"] == Decimal("60.0") # 40 * 1.5
|
|
assert by_name["Bob"]["amount"] == Decimal("90.0") # 60 * 1.5
|
|
assert totals["Alice"]["total"] == Decimal("60.0")
|
|
assert totals["Bob"]["total"] == Decimal("90.0")
|
|
assert totals["Alice"]["holiday_shifts"] == 1
|
|
assert totals["Bob"]["holiday_shifts"] == 1
|
|
|
|
def test_custom_multiplier(self, weeklypost_app, schedule, seed):
|
|
seed.config(shift_rate="50")
|
|
seed.holiday(
|
|
"2026-06-03",
|
|
slots=1,
|
|
label="Double Day",
|
|
assignees={"114": {"name": "Alice", "claimed_at": "x"}},
|
|
multiplier=Decimal("2"),
|
|
)
|
|
breakdown, totals = [], {}
|
|
weeklypost_app._add_shift_to_pay(
|
|
schedule, breakdown, totals, datetime(2026, 6, 3), "Wednesday", "day"
|
|
)
|
|
assert breakdown[0]["multiplier"] == Decimal("2")
|
|
assert breakdown[0]["amount"] == Decimal("100")
|
|
assert totals["Alice"]["total"] == Decimal("100")
|
|
|
|
def test_empty_holiday_adds_nothing(self, weeklypost_app, schedule, seed):
|
|
seed.config(shift_rate="50")
|
|
seed.holiday("2026-06-03", slots=2, label="Empty", assignees={})
|
|
breakdown, totals = [], {}
|
|
weeklypost_app._add_shift_to_pay(
|
|
schedule, breakdown, totals, datetime(2026, 6, 3), "Wednesday", "day"
|
|
)
|
|
assert breakdown == [] and totals == {}
|
|
|
|
def test_mixed_week(self, weeklypost_app, schedule, seed):
|
|
# A week with a weekday night shift, a weekend day+night, and a holiday.
|
|
seed.config(shift_rate="50")
|
|
# Regular Monday night for Alice.
|
|
seed.weekly("Monday", "114", "Alice")
|
|
# Holiday on Wednesday (day shift) with Alice + Bob.
|
|
seed.roster("200", "Bob", shift_rate="50")
|
|
seed.holiday(
|
|
"2026-06-03",
|
|
slots=2,
|
|
label="Founders Day",
|
|
assignees={
|
|
"114": {"name": "Alice", "claimed_at": "x"},
|
|
"200": {"name": "Bob", "claimed_at": "y"},
|
|
},
|
|
)
|
|
# Saturday day + night for Bob (regular weekend).
|
|
seed.weekly("Saturday", "200", "Bob", shift_type="day")
|
|
seed.weekly("Saturday", "200", "Bob", shift_type="night")
|
|
|
|
record = weeklypost_app._calculate_weekly_pay(schedule, WEEK_START)
|
|
|
|
# Alice: Mon night $50 + Wed holiday $75 = $125 over 2 shifts.
|
|
assert record["totals"]["Alice"]["shifts"] == 2
|
|
assert record["totals"]["Alice"]["total"] == Decimal("125.0")
|
|
assert record["totals"]["Alice"]["holiday_shifts"] == 1
|
|
# Bob: Wed holiday $75 + Sat day $50 + Sat night $50 = $175 over 3 shifts.
|
|
assert record["totals"]["Bob"]["shifts"] == 3
|
|
assert record["totals"]["Bob"]["total"] == Decimal("175.0")
|
|
assert record["totals"]["Bob"]["holiday_shifts"] == 1
|
|
# Exactly two holiday breakdown lines (Alice + Bob on Wed).
|
|
holiday_lines = [b for b in record["breakdown"] if b.get("is_holiday")]
|
|
assert len(holiday_lines) == 2
|
|
assert {line["name"] for line in holiday_lines} == {"Alice", "Bob"}
|
|
|
|
def test_night_on_holiday_date_is_regular(self, weeklypost_app, schedule, seed):
|
|
# A holiday only applies to the day shift; night falls through to weekly.
|
|
seed.config(shift_rate="50")
|
|
seed.weekly("Wednesday", "114", "Alice")
|
|
seed.holiday(
|
|
"2026-06-03",
|
|
slots=1,
|
|
label="Founders Day",
|
|
assignees={"200": {"name": "Bob", "claimed_at": "y"}},
|
|
)
|
|
breakdown, totals = [], {}
|
|
weeklypost_app._add_shift_to_pay(
|
|
schedule, breakdown, totals, datetime(2026, 6, 3), "Wednesday", "night"
|
|
)
|
|
assert len(breakdown) == 1
|
|
assert breakdown[0]["name"] == "Alice"
|
|
assert breakdown[0].get("is_holiday") is None
|
|
assert breakdown[0]["amount"] == Decimal("50")
|