mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-10-02 17:43:11 +00:00
Apply ruff formatting
This commit is contained in:
parent
10667c4362
commit
fa8f19660d
8 changed files with 510 additions and 184 deletions
|
|
@ -87,7 +87,11 @@ def handle_orders_aggregated(event):
|
|||
totals_text = (
|
||||
f"*${grand_total:.2f}* order total (bulk rate)\n"
|
||||
f"*${employee_total:.2f}* payroll deductions"
|
||||
+ (f"\n*${grand_total - employee_total:.2f}* company subsidy" if has_subsidy else "")
|
||||
+ (
|
||||
f"\n*${grand_total - employee_total:.2f}* company subsidy"
|
||||
if has_subsidy
|
||||
else ""
|
||||
)
|
||||
)
|
||||
|
||||
text = f"Meal orders closed for {week}. {total_employees} employees, {total_meals} meals, ${grand_total:.2f} total."
|
||||
|
|
@ -137,7 +141,9 @@ def handle_order_confirmed(event):
|
|||
for item in items:
|
||||
qty = int(item.get("quantity", 0))
|
||||
price = float(item.get("price", 0))
|
||||
item_lines.append(f"{_escape_mrkdwn(item['name'])} x{qty} — your cost: ${price * qty:.2f}")
|
||||
item_lines.append(
|
||||
f"{_escape_mrkdwn(item['name'])} x{qty} — your cost: ${price * qty:.2f}"
|
||||
)
|
||||
item_list = "\n".join(item_lines)
|
||||
|
||||
send_dm(
|
||||
|
|
|
|||
|
|
@ -161,7 +161,9 @@ def handle_submit(event):
|
|||
return response(403, {"error": "Google authentication is required"})
|
||||
user_info, verify_status = _verify_google_token(google_token)
|
||||
if verify_status == "unavailable":
|
||||
return response(503, {"error": "Authentication service temporarily unavailable"})
|
||||
return response(
|
||||
503, {"error": "Authentication service temporarily unavailable"}
|
||||
)
|
||||
if user_info is None:
|
||||
return response(403, {"error": "Invalid or unauthorized Google account"})
|
||||
name = user_info["name"]
|
||||
|
|
@ -170,7 +172,9 @@ def handle_submit(event):
|
|||
# Google auth not configured but token provided — verify it anyway
|
||||
user_info, verify_status = _verify_google_token(google_token)
|
||||
if verify_status == "unavailable":
|
||||
return response(503, {"error": "Authentication service temporarily unavailable"})
|
||||
return response(
|
||||
503, {"error": "Authentication service temporarily unavailable"}
|
||||
)
|
||||
if user_info is None:
|
||||
return response(403, {"error": "Invalid or unauthorized Google account"})
|
||||
name = user_info["name"]
|
||||
|
|
@ -216,7 +220,9 @@ def handle_submit(event):
|
|||
# Step 1: apply bulk discount and round
|
||||
bulk_price = (retail * bulk_mult).quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
|
||||
# Step 2: apply company subsidy and round
|
||||
emp_price = (bulk_price * subsidy_mult).quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
|
||||
emp_price = (bulk_price * subsidy_mult).quantize(
|
||||
TWO_PLACES, rounding=ROUND_HALF_UP
|
||||
)
|
||||
subtotal = (emp_price * qty).quantize(TWO_PLACES, rounding=ROUND_HALF_UP)
|
||||
# Convert back to float for JSON serialization
|
||||
item["retail_price"] = float(retail)
|
||||
|
|
@ -224,7 +230,11 @@ def handle_submit(event):
|
|||
item["price"] = float(emp_price)
|
||||
item["subtotal"] = float(subtotal)
|
||||
|
||||
total = float(sum(Decimal(str(i["subtotal"])) for i in filtered_items).quantize(TWO_PLACES, rounding=ROUND_HALF_UP))
|
||||
total = float(
|
||||
sum(Decimal(str(i["subtotal"])) for i in filtered_items).quantize(
|
||||
TWO_PLACES, rounding=ROUND_HALF_UP
|
||||
)
|
||||
)
|
||||
|
||||
slug = email.lower()
|
||||
|
||||
|
|
@ -244,14 +254,17 @@ def handle_submit(event):
|
|||
_lambda.invoke(
|
||||
FunctionName=os.environ["SLACK_NOTIFIER_ARN"],
|
||||
InvocationType="Event",
|
||||
Payload=json.dumps({
|
||||
"event": "order_confirmed",
|
||||
"employee_name": name,
|
||||
"employee_email": email,
|
||||
"items": filtered_items,
|
||||
"total": order_data["total"],
|
||||
"week": week,
|
||||
}, default=float),
|
||||
Payload=json.dumps(
|
||||
{
|
||||
"event": "order_confirmed",
|
||||
"employee_name": name,
|
||||
"employee_email": email,
|
||||
"items": filtered_items,
|
||||
"total": order_data["total"],
|
||||
"week": week,
|
||||
},
|
||||
default=float,
|
||||
),
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error("Slack notifier invocation failed (order already saved): %s", exc)
|
||||
|
|
|
|||
|
|
@ -161,7 +161,10 @@ def main():
|
|||
print(f"{meal['meal']:<45} {meal['quantity']:>4} ${meal['line_total']:>7.2f}")
|
||||
print("-" * 60)
|
||||
print(f"{'TOTAL':<45} {summary['total_meals']:>4} ${summary['grand_total']:>7.2f}")
|
||||
if summary.get("employee_total") is not None and summary["employee_total"] != summary["grand_total"]:
|
||||
if (
|
||||
summary.get("employee_total") is not None
|
||||
and summary["employee_total"] != summary["grand_total"]
|
||||
):
|
||||
print(f"{'PAYROLL DEDUCTIONS':<45} ${summary['employee_total']:>7.2f}")
|
||||
subsidy = round(summary["grand_total"] - summary["employee_total"], 2)
|
||||
print(f"{'COMPANY SUBSIDY':<45} ${subsidy:>7.2f}")
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ def generate_form(
|
|||
week = datetime.now().strftime("%Y-W%U")
|
||||
scraped_at = menu.get("scraped_at", "unknown")
|
||||
submit_url = f"{api_url}/api/submit-order" if api_url else "/api/submit-order"
|
||||
status_url = f"{api_url}/api/form-status/{week}" if api_url else f"/api/form-status/{week}"
|
||||
status_url = (
|
||||
f"{api_url}/api/form-status/{week}" if api_url else f"/api/form-status/{week}"
|
||||
)
|
||||
roster_url = f"{api_url}/api/roster" if api_url else "/api/roster"
|
||||
api_key_json = json.dumps(api_key).replace("</", "<\\/")
|
||||
has_discount = bulk_discount > 0 or company_subsidy > 0
|
||||
|
|
@ -299,7 +301,11 @@ body {{ padding-bottom: 80px; }}
|
|||
.countdown {{ font-size: 2rem; font-weight: 700; color: #1a1a2e; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }}
|
||||
.countdown-label {{ font-size: 0.75rem; color: #9ca3af; margin-top: 4px; }}
|
||||
</style>
|
||||
{'<script src="https://accounts.google.com/gsi/client" async defer></script>' if use_google_auth else ''}
|
||||
{
|
||||
'<script src="https://accounts.google.com/gsi/client" async defer></script>'
|
||||
if use_google_auth
|
||||
else ""
|
||||
}
|
||||
</head>
|
||||
<body>
|
||||
<div class="closed-overlay" id="closed-overlay">
|
||||
|
|
@ -311,8 +317,12 @@ body {{ padding-bottom: 80px; }}
|
|||
<div class="countdown-label">until orders open</div>
|
||||
</div>
|
||||
</div>
|
||||
{'<div class="auth-overlay" id="auth-overlay"><div class="auth-card"><div class="logo">🍽</div><h1>Sea Haven Meal Order</h1><p>Sign in with your company Google account to place your order.</p><div id="g-signin-btn" style="display:flex;justify-content:center;"></div></div></div>' if use_google_auth else ''}
|
||||
<div class="container" id="app" {'style="display:none;"' if use_google_auth else ''}>
|
||||
{
|
||||
'<div class="auth-overlay" id="auth-overlay"><div class="auth-card"><div class="logo">🍽</div><h1>Sea Haven Meal Order</h1><p>Sign in with your company Google account to place your order.</p><div id="g-signin-btn" style="display:flex;justify-content:center;"></div></div></div>'
|
||||
if use_google_auth
|
||||
else ""
|
||||
}
|
||||
<div class="container" id="app" {'style="display:none;"' if use_google_auth else ""}>
|
||||
<header>
|
||||
<h1>Sea Haven Meal Order</h1>
|
||||
<p>Week of {week} · Menu scraped {scraped_at[:10]}</p>
|
||||
|
|
@ -320,7 +330,15 @@ body {{ padding-bottom: 80px; }}
|
|||
|
||||
<div class="deadline">Order deadline: {deadline}</div>
|
||||
<div class="duplicate-warning" id="duplicate-warning" style="display:none;">You've already submitted an order this week. Submitting again will replace your previous order.</div>
|
||||
{'<div class="discount-banner">Prices reflect employee cost after ' + (f"{bulk_discount:g}% bulk discount" if bulk_discount > 0 else "") + (" + " if bulk_discount > 0 and company_subsidy > 0 else "") + (f"{company_subsidy:g}% company subsidy" if company_subsidy > 0 else "") + "</div>" if has_discount else ""}
|
||||
{
|
||||
'<div class="discount-banner">Prices reflect employee cost after '
|
||||
+ (f"{bulk_discount:g}% bulk discount" if bulk_discount > 0 else "")
|
||||
+ (" + " if bulk_discount > 0 and company_subsidy > 0 else "")
|
||||
+ (f"{company_subsidy:g}% company subsidy" if company_subsidy > 0 else "")
|
||||
+ "</div>"
|
||||
if has_discount
|
||||
else ""
|
||||
}
|
||||
|
||||
{auth_section_html}
|
||||
|
||||
|
|
@ -330,7 +348,9 @@ body {{ padding-bottom: 80px; }}
|
|||
|
||||
<div id="meals-list"></div>
|
||||
|
||||
<div class="sticky-footer" {'style="display:none;" id="sticky-footer"' if use_google_auth else ''}>
|
||||
<div class="sticky-footer" {
|
||||
'style="display:none;" id="sticky-footer"' if use_google_auth else ""
|
||||
}>
|
||||
<div class="inner">
|
||||
<div>
|
||||
<span class="total" id="total-display">$0.00</span>
|
||||
|
|
@ -359,7 +379,13 @@ const BULK_DISCOUNT = {bulk_discount};
|
|||
const COMPANY_SUBSIDY = {company_subsidy};
|
||||
const quantities = {{}};
|
||||
let formClosed = false;
|
||||
{'const GOOGLE_CLIENT_ID = ' + google_client_id_json + ';\nlet googleCredential = null;\nlet googleUser = null;' if use_google_auth else ''}
|
||||
{
|
||||
"const GOOGLE_CLIENT_ID = "
|
||||
+ google_client_id_json
|
||||
+ ";\nlet googleCredential = null;\nlet googleUser = null;"
|
||||
if use_google_auth
|
||||
else ""
|
||||
}
|
||||
|
||||
function escapeHtml(str) {{
|
||||
if (!str) return '';
|
||||
|
|
@ -388,7 +414,11 @@ function checkDuplicateOrder() {{
|
|||
|
||||
function init() {{
|
||||
checkFormStatus();
|
||||
{'waitForGoogleAuth();' if use_google_auth else 'loadRoster(); checkDuplicateOrder();'}
|
||||
{
|
||||
"waitForGoogleAuth();"
|
||||
if use_google_auth
|
||||
else "loadRoster(); checkDuplicateOrder();"
|
||||
}
|
||||
// Build filter buttons
|
||||
const tags = new Set();
|
||||
MEALS.forEach(m => (m.dietary_tags || []).forEach(t => tags.add(t)));
|
||||
|
|
@ -474,12 +504,17 @@ function updateTotal() {{
|
|||
}});
|
||||
document.getElementById('total-display').textContent = `$${{total.toFixed(2)}}`;
|
||||
document.getElementById('count-display').textContent = `${{count}} meal${{count !== 1 ? 's' : ''}}`;
|
||||
document.getElementById('submit-btn').disabled = count === 0{' || !googleCredential' if use_google_auth else ''};
|
||||
document.getElementById('submit-btn').disabled = count === 0{
|
||||
" || !googleCredential" if use_google_auth else ""
|
||||
};
|
||||
}}
|
||||
|
||||
{submit_order_js}
|
||||
|
||||
{"" if use_google_auth else """async function loadRoster() {{
|
||||
{
|
||||
""
|
||||
if use_google_auth
|
||||
else '''async function loadRoster() {{
|
||||
try {{
|
||||
const res = await fetch(ROSTER_URL);
|
||||
const data = await res.json();
|
||||
|
|
@ -508,7 +543,8 @@ function updateTotal() {{
|
|||
}});
|
||||
}}
|
||||
}}
|
||||
"""}
|
||||
'''
|
||||
}
|
||||
async function checkFormStatus() {{
|
||||
if (!STATUS_URL) return;
|
||||
try {{
|
||||
|
|
@ -616,6 +652,7 @@ def main():
|
|||
if not google_client_id:
|
||||
try:
|
||||
import boto3
|
||||
|
||||
ssm = boto3.client("ssm")
|
||||
resp = ssm.get_parameter(Name="/meal-order-manager/google-client-id")
|
||||
google_client_id = resp["Parameter"]["Value"]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,5 @@ import sys
|
|||
|
||||
# Add the shared layer source directory so `from shared.db import ...` works
|
||||
# without requiring a real Lambda layer or .aws-sam build.
|
||||
_shared_layer_dir = os.path.join(
|
||||
os.path.dirname(__file__), os.pardir, "src", "shared"
|
||||
)
|
||||
_shared_layer_dir = os.path.join(os.path.dirname(__file__), os.pardir, "src", "shared")
|
||||
sys.path.insert(0, os.path.abspath(_shared_layer_dir))
|
||||
|
|
|
|||
|
|
@ -14,11 +14,16 @@ import pytest
|
|||
# Helpers — reusable order builders
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_order(name: str, email: str, items: list[dict], total: float | None = None) -> dict:
|
||||
|
||||
def _make_order(
|
||||
name: str, email: str, items: list[dict], total: float | None = None
|
||||
) -> dict:
|
||||
"""Build a minimal order dict matching DynamoDB shape."""
|
||||
if total is None:
|
||||
total = sum(
|
||||
float(i.get("subtotal", float(i.get("price", 0)) * int(i.get("quantity", 0))))
|
||||
float(
|
||||
i.get("subtotal", float(i.get("price", 0)) * int(i.get("quantity", 0)))
|
||||
)
|
||||
for i in items
|
||||
)
|
||||
return {
|
||||
|
|
@ -50,11 +55,15 @@ def _make_item(
|
|||
# don't try to hit real AWS.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _patch_env(monkeypatch):
|
||||
monkeypatch.setenv("TABLE_NAME", "test-table")
|
||||
monkeypatch.setenv("REPORTS_BUCKET", "test-bucket")
|
||||
monkeypatch.setenv("SLACK_NOTIFIER_ARN", "arn:aws:lambda:us-east-1:123456789012:function:test-notifier")
|
||||
monkeypatch.setenv(
|
||||
"SLACK_NOTIFIER_ARN",
|
||||
"arn:aws:lambda:us-east-1:123456789012:function:test-notifier",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
|
@ -63,10 +72,14 @@ def handler_module():
|
|||
with patch("boto3.client") as mock_client, patch("boto3.resource"):
|
||||
mock_s3 = MagicMock()
|
||||
mock_lambda = MagicMock()
|
||||
mock_client.side_effect = lambda svc, **kw: {"s3": mock_s3, "lambda": mock_lambda}[svc]
|
||||
mock_client.side_effect = lambda svc, **kw: {
|
||||
"s3": mock_s3,
|
||||
"lambda": mock_lambda,
|
||||
}[svc]
|
||||
|
||||
import importlib
|
||||
import functions.aggregate_orders.handler as mod
|
||||
|
||||
importlib.reload(mod)
|
||||
|
||||
# Inject mocked clients so tests can assert on them
|
||||
|
|
@ -85,10 +98,16 @@ class TestBuildSummarySingleOrder:
|
|||
|
||||
def test_build_summary_single_order(self, handler_module):
|
||||
orders = [
|
||||
_make_order("Alice Smith", "alice@example.com", [
|
||||
_make_item("Chicken Parm", quantity=1, price=12.00, bulk_price=10.00),
|
||||
_make_item("Caesar Salad", quantity=1, price=8.00, bulk_price=6.50),
|
||||
]),
|
||||
_make_order(
|
||||
"Alice Smith",
|
||||
"alice@example.com",
|
||||
[
|
||||
_make_item(
|
||||
"Chicken Parm", quantity=1, price=12.00, bulk_price=10.00
|
||||
),
|
||||
_make_item("Caesar Salad", quantity=1, price=8.00, bulk_price=6.50),
|
||||
],
|
||||
),
|
||||
]
|
||||
summary = handler_module.build_summary(orders, "2026-W20")
|
||||
|
||||
|
|
@ -103,12 +122,20 @@ class TestBuildSummarySingleOrder:
|
|||
chicken = meals_by_name["Chicken Parm"]
|
||||
assert chicken["quantity"] == 1
|
||||
assert chicken["unit_price"] == 10.00, "unit_price should use bulk_price"
|
||||
assert chicken["employee_unit_price"] == 12.00, "employee_unit_price should use price"
|
||||
assert chicken["employee_unit_price"] == 12.00, (
|
||||
"employee_unit_price should use price"
|
||||
)
|
||||
assert chicken["line_total"] == 10.00, "line_total = bulk_price * qty"
|
||||
assert chicken["employee_line_total"] == 12.00, "employee_line_total = price * qty"
|
||||
assert chicken["employee_line_total"] == 12.00, (
|
||||
"employee_line_total = price * qty"
|
||||
)
|
||||
|
||||
assert summary["grand_total"] == 16.50, "grand_total should sum bulk line totals"
|
||||
assert summary["employee_total"] == 20.00, "employee_total should sum employee line totals"
|
||||
assert summary["grand_total"] == 16.50, (
|
||||
"grand_total should sum bulk line totals"
|
||||
)
|
||||
assert summary["employee_total"] == 20.00, (
|
||||
"employee_total should sum employee line totals"
|
||||
)
|
||||
|
||||
|
||||
class TestBuildSummaryMultipleOrdersSameMeal:
|
||||
|
|
@ -116,13 +143,27 @@ class TestBuildSummaryMultipleOrdersSameMeal:
|
|||
|
||||
def test_build_summary_multiple_orders_same_meal(self, handler_module):
|
||||
orders = [
|
||||
_make_order("Alice", "a@x.com", [_make_item("Burger", quantity=1, price=10.00, bulk_price=8.00)]),
|
||||
_make_order("Bob", "b@x.com", [_make_item("Burger", quantity=2, price=10.00, bulk_price=8.00)]),
|
||||
_make_order("Carol", "c@x.com", [_make_item("Burger", quantity=1, price=10.00, bulk_price=8.00)]),
|
||||
_make_order(
|
||||
"Alice",
|
||||
"a@x.com",
|
||||
[_make_item("Burger", quantity=1, price=10.00, bulk_price=8.00)],
|
||||
),
|
||||
_make_order(
|
||||
"Bob",
|
||||
"b@x.com",
|
||||
[_make_item("Burger", quantity=2, price=10.00, bulk_price=8.00)],
|
||||
),
|
||||
_make_order(
|
||||
"Carol",
|
||||
"c@x.com",
|
||||
[_make_item("Burger", quantity=1, price=10.00, bulk_price=8.00)],
|
||||
),
|
||||
]
|
||||
summary = handler_module.build_summary(orders, "2026-W20")
|
||||
|
||||
assert len(summary["meals"]) == 1, "All three ordered the same meal — should aggregate to 1 entry"
|
||||
assert len(summary["meals"]) == 1, (
|
||||
"All three ordered the same meal — should aggregate to 1 entry"
|
||||
)
|
||||
burger = summary["meals"][0]
|
||||
assert burger["quantity"] == 4, "Total quantity should be 1 + 2 + 1 = 4"
|
||||
assert burger["line_total"] == 32.00, "line_total = 8.00 * 4"
|
||||
|
|
@ -134,11 +175,15 @@ class TestBuildSummarySortedAlphabetically:
|
|||
|
||||
def test_build_summary_sorted_alphabetically(self, handler_module):
|
||||
orders = [
|
||||
_make_order("Alice", "a@x.com", [
|
||||
_make_item("Ziti", quantity=1, price=10.00),
|
||||
_make_item("Apple Pie", quantity=1, price=5.00),
|
||||
_make_item("Meatloaf", quantity=1, price=12.00),
|
||||
]),
|
||||
_make_order(
|
||||
"Alice",
|
||||
"a@x.com",
|
||||
[
|
||||
_make_item("Ziti", quantity=1, price=10.00),
|
||||
_make_item("Apple Pie", quantity=1, price=5.00),
|
||||
_make_item("Meatloaf", quantity=1, price=12.00),
|
||||
],
|
||||
),
|
||||
]
|
||||
summary = handler_module.build_summary(orders, "2026-W20")
|
||||
|
||||
|
|
@ -153,19 +198,31 @@ class TestBuildSummaryGrandTotalVsEmployeeTotal:
|
|||
|
||||
def test_build_summary_grand_total_vs_employee_total(self, handler_module):
|
||||
orders = [
|
||||
_make_order("Alice", "a@x.com", [
|
||||
_make_item("Steak", quantity=2, price=15.00, bulk_price=11.00),
|
||||
]),
|
||||
_make_order("Bob", "b@x.com", [
|
||||
_make_item("Pasta", quantity=1, price=9.00, bulk_price=7.00),
|
||||
]),
|
||||
_make_order(
|
||||
"Alice",
|
||||
"a@x.com",
|
||||
[
|
||||
_make_item("Steak", quantity=2, price=15.00, bulk_price=11.00),
|
||||
],
|
||||
),
|
||||
_make_order(
|
||||
"Bob",
|
||||
"b@x.com",
|
||||
[
|
||||
_make_item("Pasta", quantity=1, price=9.00, bulk_price=7.00),
|
||||
],
|
||||
),
|
||||
]
|
||||
summary = handler_module.build_summary(orders, "2026-W20")
|
||||
|
||||
# Steak: bulk 11*2=22, employee 15*2=30
|
||||
# Pasta: bulk 7*1=7, employee 9*1=9
|
||||
assert summary["grand_total"] == 29.00, "grand_total should use bulk_price (22 + 7)"
|
||||
assert summary["employee_total"] == 39.00, "employee_total should use employee_price (30 + 9)"
|
||||
assert summary["grand_total"] == 29.00, (
|
||||
"grand_total should use bulk_price (22 + 7)"
|
||||
)
|
||||
assert summary["employee_total"] == 39.00, (
|
||||
"employee_total should use employee_price (30 + 9)"
|
||||
)
|
||||
assert summary["grand_total"] != summary["employee_total"], (
|
||||
"grand_total and employee_total must differ when bulk != employee price"
|
||||
)
|
||||
|
|
@ -177,9 +234,13 @@ class TestBuildSummaryRounding:
|
|||
def test_build_summary_rounding(self, handler_module):
|
||||
# Use prices that produce repeating decimals when multiplied
|
||||
orders = [
|
||||
_make_order("Alice", "a@x.com", [
|
||||
_make_item("Soup", quantity=3, price=3.33, bulk_price=2.77),
|
||||
]),
|
||||
_make_order(
|
||||
"Alice",
|
||||
"a@x.com",
|
||||
[
|
||||
_make_item("Soup", quantity=3, price=3.33, bulk_price=2.77),
|
||||
],
|
||||
),
|
||||
]
|
||||
summary = handler_module.build_summary(orders, "2026-W20")
|
||||
|
||||
|
|
@ -187,7 +248,9 @@ class TestBuildSummaryRounding:
|
|||
# 2.77 * 3 = 8.31 (rounded)
|
||||
assert soup["line_total"] == 8.31, "line_total should be rounded to 2 decimals"
|
||||
# 3.33 * 3 = 9.99
|
||||
assert soup["employee_line_total"] == 9.99, "employee_line_total should be rounded to 2 decimals"
|
||||
assert soup["employee_line_total"] == 9.99, (
|
||||
"employee_line_total should be rounded to 2 decimals"
|
||||
)
|
||||
assert summary["grand_total"] == 8.31
|
||||
assert summary["employee_total"] == 9.99
|
||||
|
||||
|
|
@ -206,10 +269,14 @@ class TestBuildOrderSummaryCsv:
|
|||
|
||||
def test_build_order_summary_csv(self, handler_module):
|
||||
orders = [
|
||||
_make_order("Alice", "a@x.com", [
|
||||
_make_item("Burger", quantity=2, price=10.00, bulk_price=8.00),
|
||||
_make_item("Fries", quantity=1, price=5.00, bulk_price=4.00),
|
||||
]),
|
||||
_make_order(
|
||||
"Alice",
|
||||
"a@x.com",
|
||||
[
|
||||
_make_item("Burger", quantity=2, price=10.00, bulk_price=8.00),
|
||||
_make_item("Fries", quantity=1, price=5.00, bulk_price=4.00),
|
||||
],
|
||||
),
|
||||
]
|
||||
summary = handler_module.build_summary(orders, "2026-W20")
|
||||
csv_str = handler_module.build_order_summary_csv(summary)
|
||||
|
|
@ -245,13 +312,23 @@ class TestBuildPayrollCsv:
|
|||
|
||||
def test_build_payroll_csv(self, handler_module):
|
||||
orders = [
|
||||
_make_order("Zara Adams", "zara@x.com", [
|
||||
_make_item("Pasta", quantity=2, price=9.00, subtotal=18.00),
|
||||
], total=18.00),
|
||||
_make_order("Alice Brown", "alice@x.com", [
|
||||
_make_item("Burger", quantity=1, price=10.00, subtotal=10.00),
|
||||
_make_item("Fries", quantity=2, price=5.00, subtotal=10.00),
|
||||
], total=20.00),
|
||||
_make_order(
|
||||
"Zara Adams",
|
||||
"zara@x.com",
|
||||
[
|
||||
_make_item("Pasta", quantity=2, price=9.00, subtotal=18.00),
|
||||
],
|
||||
total=18.00,
|
||||
),
|
||||
_make_order(
|
||||
"Alice Brown",
|
||||
"alice@x.com",
|
||||
[
|
||||
_make_item("Burger", quantity=1, price=10.00, subtotal=10.00),
|
||||
_make_item("Fries", quantity=2, price=5.00, subtotal=10.00),
|
||||
],
|
||||
total=20.00,
|
||||
),
|
||||
]
|
||||
csv_str = handler_module.build_payroll_csv(orders)
|
||||
|
||||
|
|
@ -259,7 +336,12 @@ class TestBuildPayrollCsv:
|
|||
rows = list(reader)
|
||||
|
||||
# Header
|
||||
assert rows[0] == ["Employee Name", "Employee Email", "Items Ordered", "Total Deduction"]
|
||||
assert rows[0] == [
|
||||
"Employee Name",
|
||||
"Employee Email",
|
||||
"Items Ordered",
|
||||
"Total Deduction",
|
||||
]
|
||||
|
||||
# Sorted alphabetically by employee_name: Alice Brown before Zara Adams
|
||||
assert rows[1][0] == "Alice Brown", "Employees should be sorted by name"
|
||||
|
|
@ -280,9 +362,14 @@ class TestBuildPayrollCsvSubtotalFallback:
|
|||
|
||||
def test_build_payroll_csv_subtotal_fallback(self, handler_module):
|
||||
orders = [
|
||||
_make_order("Alice Brown", "alice@x.com", [
|
||||
_make_item("Burger", quantity=3, price=10.00), # no subtotal key
|
||||
], total=30.00),
|
||||
_make_order(
|
||||
"Alice Brown",
|
||||
"alice@x.com",
|
||||
[
|
||||
_make_item("Burger", quantity=3, price=10.00), # no subtotal key
|
||||
],
|
||||
total=30.00,
|
||||
),
|
||||
]
|
||||
csv_str = handler_module.build_payroll_csv(orders)
|
||||
|
||||
|
|
@ -313,7 +400,9 @@ class TestAggregateAlreadyAggregated:
|
|||
|
||||
result = handler_module.lambda_handler({}, None)
|
||||
|
||||
assert result["status"] == "already_aggregated", "Should return already_aggregated status"
|
||||
assert result["status"] == "already_aggregated", (
|
||||
"Should return already_aggregated status"
|
||||
)
|
||||
assert result["week"] == "2026-W20"
|
||||
handler_module._s3.put_object.assert_not_called()
|
||||
mock_orders.assert_not_called()
|
||||
|
|
@ -333,7 +422,9 @@ class TestAggregateNoOrders:
|
|||
|
||||
result = handler_module.lambda_handler({}, None)
|
||||
|
||||
assert result["status"] == "no_orders", "Should return no_orders when order list is empty"
|
||||
assert result["status"] == "no_orders", (
|
||||
"Should return no_orders when order list is empty"
|
||||
)
|
||||
assert result["week"] == "2026-W20"
|
||||
handler_module._s3.put_object.assert_not_called()
|
||||
|
||||
|
|
@ -350,12 +441,30 @@ class TestAggregateHappyPath:
|
|||
):
|
||||
mock_get_summary.return_value = None
|
||||
mock_orders.return_value = [
|
||||
_make_order("Alice", "alice@x.com", [
|
||||
_make_item("Burger", quantity=1, price=10.00, bulk_price=8.00, subtotal=10.00),
|
||||
], total=10.00),
|
||||
_make_order("Bob", "bob@x.com", [
|
||||
_make_item("Pasta", quantity=2, price=9.00, bulk_price=7.00, subtotal=18.00),
|
||||
], total=18.00),
|
||||
_make_order(
|
||||
"Alice",
|
||||
"alice@x.com",
|
||||
[
|
||||
_make_item(
|
||||
"Burger",
|
||||
quantity=1,
|
||||
price=10.00,
|
||||
bulk_price=8.00,
|
||||
subtotal=10.00,
|
||||
),
|
||||
],
|
||||
total=10.00,
|
||||
),
|
||||
_make_order(
|
||||
"Bob",
|
||||
"bob@x.com",
|
||||
[
|
||||
_make_item(
|
||||
"Pasta", quantity=2, price=9.00, bulk_price=7.00, subtotal=18.00
|
||||
),
|
||||
],
|
||||
total=18.00,
|
||||
),
|
||||
]
|
||||
|
||||
result = handler_module.lambda_handler({}, None)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ ET = ZoneInfo("America/New_York")
|
|||
# Helpers
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _make_datetime(year, month, day, hour, minute=0):
|
||||
"""Return a timezone-aware datetime in America/New_York."""
|
||||
return datetime(year, month, day, hour, minute, tzinfo=ET)
|
||||
|
|
@ -52,8 +53,8 @@ def _wednesday_10am():
|
|||
# Reminder Dedup Guard (Critical)
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestReminderDedupGuard:
|
||||
|
||||
class TestReminderDedupGuard:
|
||||
@patch("slack_notifier_handler.datetime")
|
||||
def test_reminder_skipped_wrong_hour(self, mock_dt):
|
||||
"""Invoked at 11am Thursday ET -> returns skipped."""
|
||||
|
|
@ -106,8 +107,8 @@ class TestReminderDedupGuard:
|
|||
# Reminder DMs (High)
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestReminderDMs:
|
||||
|
||||
class TestReminderDMs:
|
||||
@patch("slack_notifier_handler.send_dm")
|
||||
@patch("slack_notifier_handler.get_orders")
|
||||
@patch("slack_notifier_handler.get_roster")
|
||||
|
|
@ -187,8 +188,8 @@ class TestReminderDMs:
|
|||
# Order Confirmed (High)
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestOrderConfirmed:
|
||||
|
||||
class TestOrderConfirmed:
|
||||
@patch("slack_notifier_handler.send_dm")
|
||||
@patch("slack_notifier_handler.get_roster")
|
||||
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
||||
|
|
@ -217,12 +218,8 @@ class TestOrderConfirmed:
|
|||
blocks = call_kwargs.kwargs.get("blocks") or call_kwargs[1].get("blocks")
|
||||
body_text = blocks[1]["text"]["text"]
|
||||
|
||||
assert "your cost: $17.00" in body_text, (
|
||||
"Should show Grilled Chicken x2 cost"
|
||||
)
|
||||
assert "your cost: $6.00" in body_text, (
|
||||
"Should show Caesar Salad x1 cost"
|
||||
)
|
||||
assert "your cost: $17.00" in body_text, "Should show Grilled Chicken x2 cost"
|
||||
assert "your cost: $6.00" in body_text, "Should show Caesar Salad x1 cost"
|
||||
assert "$23.00" in body_text, "Should show payroll deduction total"
|
||||
assert "payroll deduction" in body_text.lower()
|
||||
|
||||
|
|
@ -267,23 +264,25 @@ class TestOrderConfirmed:
|
|||
|
||||
assert result["status"] == "confirmed", "Should not crash on empty name"
|
||||
|
||||
blocks = mock_dm.call_args.kwargs.get("blocks") or mock_dm.call_args[1].get("blocks")
|
||||
blocks = mock_dm.call_args.kwargs.get("blocks") or mock_dm.call_args[1].get(
|
||||
"blocks"
|
||||
)
|
||||
body_text = blocks[1]["text"]["text"]
|
||||
assert "Hey there!" in body_text, "Should greet with 'Hey there!' when name is empty"
|
||||
assert "Hey there!" in body_text, (
|
||||
"Should greet with 'Hey there!' when name is empty"
|
||||
)
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# Orders Aggregated (High)
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestOrdersAggregated:
|
||||
|
||||
class TestOrdersAggregated:
|
||||
@patch("slack_notifier_handler.post_channel_message")
|
||||
@patch("slack_notifier_handler.get_summary")
|
||||
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
||||
def test_orders_aggregated_with_subsidy(
|
||||
self, mock_week, mock_summary, mock_post
|
||||
):
|
||||
def test_orders_aggregated_with_subsidy(self, mock_week, mock_summary, mock_post):
|
||||
"""employee_total < grand_total -> message includes company subsidy line."""
|
||||
mock_summary.return_value = {
|
||||
"total_employees": 5,
|
||||
|
|
@ -339,9 +338,7 @@ class TestOrdersAggregated:
|
|||
@patch("slack_notifier_handler.post_channel_message")
|
||||
@patch("slack_notifier_handler.get_summary")
|
||||
@patch("slack_notifier_handler.current_week", return_value="2026-W19")
|
||||
def test_orders_aggregated_no_summary(
|
||||
self, mock_week, mock_summary, mock_post
|
||||
):
|
||||
def test_orders_aggregated_no_summary(self, mock_week, mock_summary, mock_post):
|
||||
"""No summary in DB -> returns {'status': 'no_summary'}."""
|
||||
mock_summary.return_value = None
|
||||
|
||||
|
|
@ -355,8 +352,8 @@ class TestOrdersAggregated:
|
|||
# Escaping (Low)
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestEscaping:
|
||||
|
||||
class TestEscaping:
|
||||
def test_escape_mrkdwn(self):
|
||||
"""'A & B <C>' -> 'A & B <C>'."""
|
||||
assert handler._escape_mrkdwn("A & B <C>") == "A & B <C>"
|
||||
|
|
@ -366,8 +363,8 @@ class TestEscaping:
|
|||
# Routing
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestRouting:
|
||||
|
||||
class TestRouting:
|
||||
def test_unknown_event_type(self):
|
||||
"""Unknown event type returns error message."""
|
||||
result = handler.lambda_handler({"event": "bogus_event"}, None)
|
||||
|
|
|
|||
|
|
@ -19,13 +19,20 @@ import pytest
|
|||
# ---------------------------------------------------------------------------
|
||||
os.environ.setdefault("TABLE_NAME", "test-orders-table")
|
||||
os.environ.setdefault("FORM_API_KEY_SECRET", "test/form-api-key")
|
||||
os.environ.setdefault("SLACK_NOTIFIER_ARN", "arn:aws:lambda:us-east-1:000000000000:function:test-slack-notifier")
|
||||
os.environ.setdefault(
|
||||
"SLACK_NOTIFIER_ARN",
|
||||
"arn:aws:lambda:us-east-1:000000000000:function:test-slack-notifier",
|
||||
)
|
||||
os.environ.setdefault("GOOGLE_CLIENT_ID_PARAM", "")
|
||||
|
||||
import importlib.util
|
||||
|
||||
_handler_path = os.path.join(os.path.dirname(__file__), os.pardir, "functions", "submit_order", "handler.py")
|
||||
_spec = importlib.util.spec_from_file_location("submit_order_handler", os.path.abspath(_handler_path))
|
||||
_handler_path = os.path.join(
|
||||
os.path.dirname(__file__), os.pardir, "functions", "submit_order", "handler.py"
|
||||
)
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"submit_order_handler", os.path.abspath(_handler_path)
|
||||
)
|
||||
submit_order_handler = importlib.util.module_from_spec(_spec)
|
||||
sys.modules["submit_order_handler"] = submit_order_handler
|
||||
_spec.loader.exec_module(submit_order_handler)
|
||||
|
|
@ -62,8 +69,13 @@ def _make_event(
|
|||
return event
|
||||
|
||||
|
||||
def _submit_event(items, api_key=TEST_API_KEY, employee_name="Test User",
|
||||
employee_email="test.user@seahavenind.com", extra_body=None):
|
||||
def _submit_event(
|
||||
items,
|
||||
api_key=TEST_API_KEY,
|
||||
employee_name="Test User",
|
||||
employee_email="test.user@seahavenind.com",
|
||||
extra_body=None,
|
||||
):
|
||||
"""Shortcut for a typical POST /submit event with items."""
|
||||
body = {
|
||||
"employee_name": employee_name,
|
||||
|
|
@ -89,11 +101,13 @@ def _make_items(retail_prices_and_qtys):
|
|||
"""Build item list from [(retail_price, quantity), ...]."""
|
||||
items = []
|
||||
for i, (price, qty) in enumerate(retail_prices_and_qtys):
|
||||
items.append({
|
||||
"name": f"Meal {i + 1}",
|
||||
"retail_price": price,
|
||||
"quantity": qty,
|
||||
})
|
||||
items.append(
|
||||
{
|
||||
"name": f"Meal {i + 1}",
|
||||
"retail_price": price,
|
||||
"quantity": qty,
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
|
|
@ -123,6 +137,7 @@ def _reset_handler_caches():
|
|||
def _reset_shared_caches():
|
||||
"""Reset shared.secrets cache before each test."""
|
||||
from shared import secrets
|
||||
|
||||
secrets._cache = {}
|
||||
yield
|
||||
|
||||
|
|
@ -131,11 +146,15 @@ def _reset_shared_caches():
|
|||
# PRICING PIPELINE (Critical)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@patch("submit_order_handler._lambda")
|
||||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 10, "company_subsidy_percent": 50})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 10, "company_subsidy_percent": 50},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_discount_two_step_rounding(
|
||||
|
|
@ -174,7 +193,10 @@ def test_discount_two_step_rounding(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 50, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 50, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_discount_rounding_half_up_boundary(
|
||||
|
|
@ -208,7 +230,10 @@ def test_discount_rounding_half_up_boundary(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": -5, "company_subsidy_percent": 150})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": -5, "company_subsidy_percent": 150},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_discount_clamping(
|
||||
|
|
@ -242,7 +267,10 @@ def test_discount_clamping(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_discount_both_zero(
|
||||
|
|
@ -261,7 +289,9 @@ def test_discount_both_zero(
|
|||
saved_order = mock_put.call_args[0][2]
|
||||
item = saved_order["items"][0]
|
||||
|
||||
assert item["retail_price"] == 15.99, f"retail_price mismatch: {item['retail_price']}"
|
||||
assert item["retail_price"] == 15.99, (
|
||||
f"retail_price mismatch: {item['retail_price']}"
|
||||
)
|
||||
assert item["bulk_price"] == 15.99, (
|
||||
f"bulk_price should equal retail when bulk discount is 0%, got {item['bulk_price']}"
|
||||
)
|
||||
|
|
@ -277,7 +307,10 @@ def test_discount_both_zero(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 10, "company_subsidy_percent": 25})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 10, "company_subsidy_percent": 25},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_total_summation_multiple_items(
|
||||
|
|
@ -301,10 +334,14 @@ def test_total_summation_multiple_items(
|
|||
saved_order = mock_put.call_args[0][2]
|
||||
|
||||
item_a = saved_order["items"][0]
|
||||
assert item_a["subtotal"] == 13.50, f"Item A subtotal expected 13.50, got {item_a['subtotal']}"
|
||||
assert item_a["subtotal"] == 13.50, (
|
||||
f"Item A subtotal expected 13.50, got {item_a['subtotal']}"
|
||||
)
|
||||
|
||||
item_b = saved_order["items"][1]
|
||||
assert item_b["subtotal"] == 4.95, f"Item B subtotal expected 4.95, got {item_b['subtotal']}"
|
||||
assert item_b["subtotal"] == 4.95, (
|
||||
f"Item B subtotal expected 4.95, got {item_b['subtotal']}"
|
||||
)
|
||||
|
||||
assert saved_order["total"] == 18.45, (
|
||||
f"total should be sum of rounded subtotals (13.50 + 4.95 = 18.45), got {saved_order['total']}"
|
||||
|
|
@ -315,13 +352,19 @@ def test_total_summation_multiple_items(
|
|||
# AUTHENTICATION (Critical + High)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@patch("submit_order_handler._lambda")
|
||||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID)
|
||||
@patch(
|
||||
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
||||
)
|
||||
def test_google_auth_required_when_configured(
|
||||
mock_gcid, mock_secret, mock_settings, mock_week, mock_status, mock_put, mock_lam
|
||||
):
|
||||
|
|
@ -345,9 +388,14 @@ def test_google_auth_required_when_configured(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID)
|
||||
@patch(
|
||||
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
||||
)
|
||||
def test_google_auth_bypass_prevention(
|
||||
mock_gcid, mock_secret, mock_settings, mock_week, mock_status, mock_put, mock_lam
|
||||
):
|
||||
|
|
@ -361,8 +409,10 @@ def test_google_auth_bypass_prevention(
|
|||
# No google_id_token — trying to bypass with manual name/email
|
||||
}
|
||||
event = _make_event(
|
||||
method="POST", path="/submit",
|
||||
body=body, headers={"x-api-key": TEST_API_KEY},
|
||||
method="POST",
|
||||
path="/submit",
|
||||
body=body,
|
||||
headers={"x-api-key": TEST_API_KEY},
|
||||
)
|
||||
result = lambda_handler(event, None)
|
||||
status, body_resp = _parse_response(result)
|
||||
|
|
@ -375,25 +425,38 @@ def test_google_auth_bypass_prevention(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID)
|
||||
@patch(
|
||||
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
||||
)
|
||||
@patch("submit_order_handler.urllib.request.urlopen")
|
||||
def test_google_token_audience_mismatch(
|
||||
mock_urlopen, mock_gcid, mock_secret, mock_settings,
|
||||
mock_week, mock_status, mock_put, mock_lam
|
||||
mock_urlopen,
|
||||
mock_gcid,
|
||||
mock_secret,
|
||||
mock_settings,
|
||||
mock_week,
|
||||
mock_status,
|
||||
mock_put,
|
||||
mock_lam,
|
||||
):
|
||||
"""Token with wrong audience -> 403."""
|
||||
from submit_order_handler import lambda_handler
|
||||
|
||||
# Simulate Google returning token info with wrong audience
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.read.return_value = json.dumps({
|
||||
"aud": "wrong-client-id.apps.googleusercontent.com",
|
||||
"hd": "seahavenind.com",
|
||||
"name": "Test User",
|
||||
"email": "test@seahavenind.com",
|
||||
}).encode()
|
||||
mock_resp.read.return_value = json.dumps(
|
||||
{
|
||||
"aud": "wrong-client-id.apps.googleusercontent.com",
|
||||
"hd": "seahavenind.com",
|
||||
"name": "Test User",
|
||||
"email": "test@seahavenind.com",
|
||||
}
|
||||
).encode()
|
||||
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
||||
mock_resp.__exit__ = MagicMock(return_value=False)
|
||||
mock_urlopen.return_value = mock_resp
|
||||
|
|
@ -411,24 +474,37 @@ def test_google_token_audience_mismatch(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID)
|
||||
@patch(
|
||||
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
||||
)
|
||||
@patch("submit_order_handler.urllib.request.urlopen")
|
||||
def test_google_token_domain_mismatch(
|
||||
mock_urlopen, mock_gcid, mock_secret, mock_settings,
|
||||
mock_week, mock_status, mock_put, mock_lam
|
||||
mock_urlopen,
|
||||
mock_gcid,
|
||||
mock_secret,
|
||||
mock_settings,
|
||||
mock_week,
|
||||
mock_status,
|
||||
mock_put,
|
||||
mock_lam,
|
||||
):
|
||||
"""Token with wrong hosted domain -> 403."""
|
||||
from submit_order_handler import lambda_handler
|
||||
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.read.return_value = json.dumps({
|
||||
"aud": VALID_GOOGLE_CLIENT_ID,
|
||||
"hd": "evil-corp.com",
|
||||
"name": "Evil User",
|
||||
"email": "evil@evil-corp.com",
|
||||
}).encode()
|
||||
mock_resp.read.return_value = json.dumps(
|
||||
{
|
||||
"aud": VALID_GOOGLE_CLIENT_ID,
|
||||
"hd": "evil-corp.com",
|
||||
"name": "Evil User",
|
||||
"email": "evil@evil-corp.com",
|
||||
}
|
||||
).encode()
|
||||
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
||||
mock_resp.__exit__ = MagicMock(return_value=False)
|
||||
mock_urlopen.return_value = mock_resp
|
||||
|
|
@ -446,13 +522,27 @@ def test_google_token_domain_mismatch(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID)
|
||||
@patch("submit_order_handler.urllib.request.urlopen", side_effect=urllib.error.URLError("Connection refused"))
|
||||
@patch(
|
||||
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
||||
)
|
||||
@patch(
|
||||
"submit_order_handler.urllib.request.urlopen",
|
||||
side_effect=urllib.error.URLError("Connection refused"),
|
||||
)
|
||||
def test_google_token_service_unavailable(
|
||||
mock_urlopen, mock_gcid, mock_secret, mock_settings,
|
||||
mock_week, mock_status, mock_put, mock_lam
|
||||
mock_urlopen,
|
||||
mock_gcid,
|
||||
mock_secret,
|
||||
mock_settings,
|
||||
mock_week,
|
||||
mock_status,
|
||||
mock_put,
|
||||
mock_lam,
|
||||
):
|
||||
"""URLError from Google tokeninfo -> 503."""
|
||||
from submit_order_handler import lambda_handler
|
||||
|
|
@ -473,24 +563,37 @@ def test_google_token_service_unavailable(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID)
|
||||
@patch(
|
||||
"submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
|
||||
)
|
||||
@patch("submit_order_handler.urllib.request.urlopen")
|
||||
def test_google_token_valid_success(
|
||||
mock_urlopen, mock_gcid, mock_secret, mock_settings,
|
||||
mock_week, mock_status, mock_put, mock_lam
|
||||
mock_urlopen,
|
||||
mock_gcid,
|
||||
mock_secret,
|
||||
mock_settings,
|
||||
mock_week,
|
||||
mock_status,
|
||||
mock_put,
|
||||
mock_lam,
|
||||
):
|
||||
"""Valid Google token -> order saved with token's name/email."""
|
||||
from submit_order_handler import lambda_handler
|
||||
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.read.return_value = json.dumps({
|
||||
"aud": VALID_GOOGLE_CLIENT_ID,
|
||||
"hd": "seahavenind.com",
|
||||
"name": "Adam Moussa",
|
||||
"email": "adam.moussa@seahavenind.com",
|
||||
}).encode()
|
||||
mock_resp.read.return_value = json.dumps(
|
||||
{
|
||||
"aud": VALID_GOOGLE_CLIENT_ID,
|
||||
"hd": "seahavenind.com",
|
||||
"name": "Adam Moussa",
|
||||
"email": "adam.moussa@seahavenind.com",
|
||||
}
|
||||
).encode()
|
||||
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
||||
mock_resp.__exit__ = MagicMock(return_value=False)
|
||||
mock_urlopen.return_value = mock_resp
|
||||
|
|
@ -521,7 +624,10 @@ def test_google_token_valid_success(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_manual_fallback_when_google_not_configured(
|
||||
|
|
@ -531,7 +637,9 @@ def test_manual_fallback_when_google_not_configured(
|
|||
from submit_order_handler import lambda_handler
|
||||
|
||||
items = _make_items([(12.00, 1)])
|
||||
event = _submit_event(items, employee_name="Manual User", employee_email="manual@seahavenind.com")
|
||||
event = _submit_event(
|
||||
items, employee_name="Manual User", employee_email="manual@seahavenind.com"
|
||||
)
|
||||
result = lambda_handler(event, None)
|
||||
status, body = _parse_response(result)
|
||||
|
||||
|
|
@ -550,7 +658,10 @@ def test_manual_fallback_when_google_not_configured(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_submit_invalid_api_key(
|
||||
|
|
@ -575,11 +686,15 @@ def test_submit_invalid_api_key(
|
|||
# SLUG GENERATION (Critical)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@patch("submit_order_handler._lambda")
|
||||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_slug_from_email(
|
||||
|
|
@ -589,7 +704,9 @@ def test_slug_from_email(
|
|||
from submit_order_handler import lambda_handler
|
||||
|
||||
items = _make_items([(10.00, 1)])
|
||||
event = _submit_event(items, employee_name="Adam Moussa", employee_email="Adam.Moussa@seahavenind.com")
|
||||
event = _submit_event(
|
||||
items, employee_name="Adam Moussa", employee_email="Adam.Moussa@seahavenind.com"
|
||||
)
|
||||
result = lambda_handler(event, None)
|
||||
status, _ = _parse_response(result)
|
||||
|
||||
|
|
@ -597,14 +714,19 @@ def test_slug_from_email(
|
|||
|
||||
# put_order is called with (week, slug, order_data)
|
||||
slug = mock_put.call_args[0][1]
|
||||
assert slug == "adam.moussa@seahavenind.com", f"Slug should be 'adam.moussa@seahavenind.com', got '{slug}'"
|
||||
assert slug == "adam.moussa@seahavenind.com", (
|
||||
f"Slug should be 'adam.moussa@seahavenind.com', got '{slug}'"
|
||||
)
|
||||
|
||||
|
||||
@patch("submit_order_handler._lambda")
|
||||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_slug_edge_cases(
|
||||
|
|
@ -615,7 +737,11 @@ def test_slug_edge_cases(
|
|||
|
||||
# Test 1: full email preserved
|
||||
items = _make_items([(10.00, 1)])
|
||||
event = _submit_event(items, employee_name="First Middle Last", employee_email="First.Middle.Last@x.com")
|
||||
event = _submit_event(
|
||||
items,
|
||||
employee_name="First Middle Last",
|
||||
employee_email="First.Middle.Last@x.com",
|
||||
)
|
||||
lambda_handler(event, None)
|
||||
slug_1 = mock_put.call_args[0][1]
|
||||
assert slug_1 == "first.middle.last@x.com", (
|
||||
|
|
@ -624,7 +750,9 @@ def test_slug_edge_cases(
|
|||
|
||||
# Test 2: different domains produce different slugs (no collision)
|
||||
mock_put.reset_mock()
|
||||
event = _submit_event(items, employee_name="Bob Smith", employee_email="Bob@other.com")
|
||||
event = _submit_event(
|
||||
items, employee_name="Bob Smith", employee_email="Bob@other.com"
|
||||
)
|
||||
lambda_handler(event, None)
|
||||
slug_2 = mock_put.call_args[0][1]
|
||||
assert slug_2 == "bob@other.com", (
|
||||
|
|
@ -637,13 +765,16 @@ def test_slug_edge_cases(
|
|||
# FORM STATUS (Critical + High)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
def test_form_status_open(mock_week, mock_status):
|
||||
"""Status 'open' — response has week and status, no reopen_at."""
|
||||
from submit_order_handler import lambda_handler
|
||||
|
||||
event = _make_event(method="GET", path="/form-status/2026-W20", path_parameters={"week": "2026-W20"})
|
||||
event = _make_event(
|
||||
method="GET", path="/form-status/2026-W20", path_parameters={"week": "2026-W20"}
|
||||
)
|
||||
result = lambda_handler(event, None)
|
||||
status, body = _parse_response(result)
|
||||
|
||||
|
|
@ -665,13 +796,19 @@ def test_form_status_closed_reopen_at(mock_week, mock_status):
|
|||
mock_dt.now.return_value = thursday
|
||||
mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
|
||||
|
||||
event = _make_event(method="GET", path="/form-status/2026-W20", path_parameters={"week": "2026-W20"})
|
||||
event = _make_event(
|
||||
method="GET",
|
||||
path="/form-status/2026-W20",
|
||||
path_parameters={"week": "2026-W20"},
|
||||
)
|
||||
result = lambda_handler(event, None)
|
||||
|
||||
status, body = _parse_response(result)
|
||||
|
||||
assert status == 200, f"Expected 200, got {status}: {body}"
|
||||
assert body["status"] == "closed", f"Expected status='closed', got '{body['status']}'"
|
||||
assert body["status"] == "closed", (
|
||||
f"Expected status='closed', got '{body['status']}'"
|
||||
)
|
||||
assert "reopen_at" in body, "reopen_at should be present when form is closed"
|
||||
|
||||
# Next Monday from Thursday 2026-05-14 is Monday 2026-05-18
|
||||
|
|
@ -694,7 +831,11 @@ def test_form_status_monday_before_8am(mock_week, mock_status):
|
|||
mock_dt.now.return_value = monday_early
|
||||
mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
|
||||
|
||||
event = _make_event(method="GET", path="/form-status/2026-W20", path_parameters={"week": "2026-W20"})
|
||||
event = _make_event(
|
||||
method="GET",
|
||||
path="/form-status/2026-W20",
|
||||
path_parameters={"week": "2026-W20"},
|
||||
)
|
||||
result = lambda_handler(event, None)
|
||||
|
||||
status, body = _parse_response(result)
|
||||
|
|
@ -715,11 +856,15 @@ def test_form_status_monday_before_8am(mock_week, mock_status):
|
|||
# VALIDATION (High)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@patch("submit_order_handler._lambda")
|
||||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_submit_missing_name(
|
||||
|
|
@ -729,7 +874,9 @@ def test_submit_missing_name(
|
|||
from submit_order_handler import lambda_handler
|
||||
|
||||
items = _make_items([(10.00, 1)])
|
||||
event = _submit_event(items, employee_name="", employee_email="test@seahavenind.com")
|
||||
event = _submit_event(
|
||||
items, employee_name="", employee_email="test@seahavenind.com"
|
||||
)
|
||||
result = lambda_handler(event, None)
|
||||
status, body = _parse_response(result)
|
||||
|
||||
|
|
@ -744,7 +891,10 @@ def test_submit_missing_name(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_submit_missing_email(
|
||||
|
|
@ -769,7 +919,10 @@ def test_submit_missing_email(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_submit_zero_quantity_only(
|
||||
|
|
@ -794,7 +947,10 @@ def test_submit_zero_quantity_only(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="closed")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_submit_form_closed(
|
||||
|
|
@ -819,7 +975,10 @@ def test_submit_form_closed(
|
|||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="not_found")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_submit_no_menu(
|
||||
|
|
@ -844,11 +1003,15 @@ def test_submit_no_menu(
|
|||
# RELIABILITY (High)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
@patch("submit_order_handler._lambda")
|
||||
@patch("submit_order_handler.put_order")
|
||||
@patch("submit_order_handler.get_form_status", return_value="open")
|
||||
@patch("submit_order_handler.current_week", return_value="2026-W20")
|
||||
@patch("submit_order_handler.get_settings", return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0})
|
||||
@patch(
|
||||
"submit_order_handler.get_settings",
|
||||
return_value={"bulk_discount_percent": 0, "company_subsidy_percent": 0},
|
||||
)
|
||||
@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
|
||||
@patch("submit_order_handler._get_google_client_id", return_value="")
|
||||
def test_slack_failure_does_not_fail_order(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue