-
Notifications
You must be signed in to change notification settings - Fork 0
Fix tip pooling: POS tips query and settings persistence #361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
de36182
14a0d97
07f1654
3e7537d
3d993c7
451e4b2
fc7bb49
4d03689
1678ca2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| -- Fix get_pos_tips_by_date to include uncategorized tips from unified_sales | ||
| -- | ||
| -- ISSUE: The function only looked at unified_sales_splits (already categorized items), | ||
| -- but POS systems like Toast sync tips directly to unified_sales with item_type='tip' | ||
| -- BEFORE they are categorized. This caused "No POS tips found" message even when tips existed. | ||
| -- | ||
| -- SOLUTION: Query both: | ||
| -- 1. Categorized tips (unified_sales_splits with "tip" in account name/subtype) | ||
| -- 2. Uncategorized tips (unified_sales where item_type='tip' OR adjustment_type='tip') | ||
|
|
||
| CREATE OR REPLACE FUNCTION get_pos_tips_by_date( | ||
| p_restaurant_id UUID, | ||
| p_start_date DATE, | ||
| p_end_date DATE | ||
| ) | ||
| RETURNS TABLE ( | ||
| tip_date DATE, | ||
| total_amount_cents INTEGER, | ||
| transaction_count INTEGER, | ||
| pos_source TEXT | ||
| ) | ||
| LANGUAGE plpgsql | ||
| SECURITY INVOKER | ||
| AS $$ | ||
| BEGIN | ||
| -- Authorization check | ||
| IF NOT EXISTS ( | ||
| SELECT 1 FROM user_restaurants | ||
| WHERE restaurant_id = p_restaurant_id | ||
| AND user_id = auth.uid() | ||
| ) THEN | ||
| RAISE EXCEPTION 'Access denied: User does not have access to restaurant %', p_restaurant_id; | ||
| END IF; | ||
|
|
||
| RETURN QUERY | ||
| WITH categorized_tips AS ( | ||
| SELECT | ||
| us.sale_date AS t_date, | ||
| SUM(uss.amount * 100)::INTEGER AS t_cents, | ||
| COUNT(DISTINCT us.external_order_id)::INTEGER AS t_count, | ||
| us.pos_system AS t_source | ||
| FROM unified_sales us | ||
| INNER JOIN unified_sales_splits uss ON us.id = uss.sale_id | ||
| INNER JOIN chart_of_accounts coa ON uss.category_id = coa.id | ||
| WHERE us.restaurant_id = p_restaurant_id | ||
| AND us.sale_date >= p_start_date | ||
| AND us.sale_date <= p_end_date | ||
| AND ( | ||
| LOWER(COALESCE(coa.account_name, '')) LIKE '%tip%' | ||
| OR LOWER(COALESCE(coa.account_subtype::TEXT, '')) LIKE '%tip%' | ||
| ) | ||
| GROUP BY us.sale_date, us.pos_system | ||
| ), | ||
| uncategorized_tips AS ( | ||
| -- item_type='tip' or adjustment_type='tip' without a tip-account split | ||
| SELECT | ||
| us.sale_date AS t_date, | ||
| SUM(COALESCE(us.total_price, us.unit_price * us.quantity, 0) * 100)::INTEGER AS t_cents, | ||
| COUNT(DISTINCT us.external_order_id)::INTEGER AS t_count, | ||
| us.pos_system AS t_source | ||
| FROM unified_sales us | ||
| WHERE us.restaurant_id = p_restaurant_id | ||
| AND us.sale_date >= p_start_date | ||
| AND us.sale_date <= p_end_date | ||
| AND (us.item_type = 'tip' OR us.adjustment_type = 'tip') | ||
| AND NOT EXISTS ( | ||
| SELECT 1 FROM unified_sales_splits uss | ||
| INNER JOIN chart_of_accounts coa ON uss.category_id = coa.id | ||
| WHERE uss.sale_id = us.id | ||
| AND ( | ||
| LOWER(COALESCE(coa.account_name, '')) LIKE '%tip%' | ||
| OR LOWER(COALESCE(coa.account_subtype::TEXT, '')) LIKE '%tip%' | ||
| ) | ||
| ) | ||
| GROUP BY us.sale_date, us.pos_system | ||
| ), | ||
| combined_tips AS ( | ||
| SELECT t_date, t_cents, t_count, t_source FROM categorized_tips | ||
| UNION ALL | ||
| SELECT t_date, t_cents, t_count, t_source FROM uncategorized_tips | ||
| ) | ||
| SELECT | ||
| ct.t_date, | ||
| SUM(ct.t_cents)::INTEGER, | ||
| SUM(ct.t_count)::INTEGER, | ||
| ct.t_source | ||
| FROM combined_tips ct | ||
| GROUP BY ct.t_date, ct.t_source | ||
| ORDER BY ct.t_date DESC; | ||
| END; | ||
| $$; | ||
|
Comment on lines
+11
to
+91
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # Check for existing pgTAP tests related to this function
rg "get_pos_tips_by_date" --type=sql
# List test files in supabase/tests
fd . supabase/tests/ --type f --extension sql 2>/dev/null | head -20
# Check if there's a test file for this migration
ls -la supabase/tests/ 2>/dev/null | head -20Repository: toyiyo/nimble-pnl Length of output: 4509 🏁 Script executed: cat supabase/tests/26_get_pos_tips_by_date.sqlRepository: toyiyo/nimble-pnl Length of output: 6668 🏁 Script executed: # Check unified_sales table definition for item_type and adjustment_type
rg "CREATE TABLE.*unified_sales" -A 20 supabase/migrations/
# Also check if test data includes uncategorized items
grep -n "item_type\|adjustment_type" supabase/tests/26_get_pos_tips_by_date.sqlRepository: toyiyo/nimble-pnl Length of output: 4704 🏁 Script executed: # Search for item_type and adjustment_type in all migrations
rg "item_type|adjustment_type" supabase/migrations/ -A 2 -B 2
# Also check the specific migration file being reviewed
grep -n "item_type\|adjustment_type" supabase/migrations/20260210234900_fix_get_pos_tips_by_date.sqlRepository: toyiyo/nimble-pnl Length of output: 43264 🏁 Script executed: # Check the test data more carefully - look for items without splits
grep -A 5 "INSERT INTO unified_sales" supabase/tests/26_get_pos_tips_by_date.sql
# Also check if any test items are inserted with item_type or adjustment_type='tip'
grep -E "item_type|adjustment_type" supabase/tests/26_get_pos_tips_by_date.sql
# Check the comment at the start of the migration to understand what's being fixed
head -15 supabase/migrations/20260210234900_fix_get_pos_tips_by_date.sqlRepository: toyiyo/nimble-pnl Length of output: 2034 Test coverage for uncategorized tips scenario is missing. While pgTAP tests exist for this function, they don't cover the core new functionality added by this migration. The Add test fixture data with at least one uncategorized tip item (no splits, with 🤖 Prompt for AI Agents |
||
|
|
||
| COMMENT ON FUNCTION get_pos_tips_by_date IS | ||
| 'Aggregates POS tips from both categorized (unified_sales_splits) and uncategorized (unified_sales) sources. | ||
| Used by tip pooling system to display POS-imported tips. | ||
| Returns daily totals for: | ||
| 1. Categorized tips (splits with account name/subtype containing ''tip'') | ||
| 2. Uncategorized tips (item_type=''tip'' or adjustment_type=''tip'')'; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| -- Fix Toast payment dates: use paidBusinessDate (restaurant business day) instead of UTC date | ||
| -- | ||
| -- Toast provides paidBusinessDate as a YYYYMMDD integer representing the restaurant's | ||
| -- actual business day. The sync was using the UTC date from paidDate/closedDate, causing | ||
| -- late-night payments to appear on the next calendar day. | ||
|
|
||
| -- Step 1: Fix toast_payments.payment_date from raw_json->>'paidBusinessDate' | ||
| UPDATE toast_payments | ||
| SET payment_date = CONCAT( | ||
| SUBSTRING(CAST((raw_json->>'paidBusinessDate')::BIGINT AS TEXT), 1, 4), '-', | ||
| SUBSTRING(CAST((raw_json->>'paidBusinessDate')::BIGINT AS TEXT), 5, 2), '-', | ||
| SUBSTRING(CAST((raw_json->>'paidBusinessDate')::BIGINT AS TEXT), 7, 2) | ||
| )::DATE | ||
| WHERE raw_json->>'paidBusinessDate' IS NOT NULL | ||
| AND LENGTH(CAST((raw_json->>'paidBusinessDate')::BIGINT AS TEXT)) = 8 | ||
| AND payment_date != CONCAT( | ||
| SUBSTRING(CAST((raw_json->>'paidBusinessDate')::BIGINT AS TEXT), 1, 4), '-', | ||
| SUBSTRING(CAST((raw_json->>'paidBusinessDate')::BIGINT AS TEXT), 5, 2), '-', | ||
| SUBSTRING(CAST((raw_json->>'paidBusinessDate')::BIGINT AS TEXT), 7, 2) | ||
| )::DATE; | ||
|
|
||
| -- Step 2: Fix unified_sales.sale_date for Toast tip entries | ||
| -- These have external_item_id ending in '_tip' and reference toast_payments via external_order_id | ||
| UPDATE unified_sales us | ||
| SET sale_date = tp.payment_date | ||
| FROM toast_payments tp | ||
| WHERE us.pos_system = 'toast' | ||
| AND us.external_order_id = tp.toast_order_guid | ||
| AND us.external_item_id = tp.toast_payment_guid || '_tip' | ||
| AND us.sale_date != tp.payment_date; | ||
|
|
||
| -- Step 3: Fix toast_orders.order_date from raw_json->>'businessDate' | ||
| -- (order_date was also derived from UTC closedDate) | ||
| UPDATE toast_orders | ||
| SET order_date = CONCAT( | ||
| SUBSTRING(CAST((raw_json->>'businessDate')::BIGINT AS TEXT), 1, 4), '-', | ||
| SUBSTRING(CAST((raw_json->>'businessDate')::BIGINT AS TEXT), 5, 2), '-', | ||
| SUBSTRING(CAST((raw_json->>'businessDate')::BIGINT AS TEXT), 7, 2) | ||
| )::DATE | ||
| WHERE raw_json->>'businessDate' IS NOT NULL | ||
| AND LENGTH(CAST((raw_json->>'businessDate')::BIGINT AS TEXT)) = 8 | ||
| AND order_date != CONCAT( | ||
| SUBSTRING(CAST((raw_json->>'businessDate')::BIGINT AS TEXT), 1, 4), '-', | ||
| SUBSTRING(CAST((raw_json->>'businessDate')::BIGINT AS TEXT), 5, 2), '-', | ||
| SUBSTRING(CAST((raw_json->>'businessDate')::BIGINT AS TEXT), 7, 2) | ||
| )::DATE; | ||
|
|
||
| -- Step 4: Fix unified_sales.sale_date for regular Toast items using (now-corrected) toast_orders | ||
| UPDATE unified_sales us | ||
| SET sale_date = to_ord.order_date | ||
| FROM toast_orders to_ord | ||
| WHERE us.pos_system = 'toast' | ||
| AND us.external_order_id = to_ord.toast_order_guid | ||
| AND us.restaurant_id = to_ord.restaurant_id | ||
| AND us.item_type IS DISTINCT FROM 'tip' | ||
| AND us.adjustment_type IS DISTINCT FROM 'tip' | ||
| AND to_ord.raw_json->>'businessDate' IS NOT NULL | ||
| AND LENGTH(CAST((to_ord.raw_json->>'businessDate')::BIGINT AS TEXT)) = 8 | ||
| AND us.sale_date != to_ord.order_date; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential gap: tip row with a non-tip split is excluded from both CTEs.
The
NOT EXISTScheck excludes anyunified_salestip row that has any split, regardless of whether that split maps to a tip account. If someone miscategorizes a tip record (e.g., assigns it to a "food" account), it will have a split → excluded fromuncategorized_tips, but the split's account won't match'%tip%'→ excluded fromcategorized_tipstoo. The tip silently disappears.Consider narrowing the exclusion to tip-related splits only:
Proposed fix
AND NOT EXISTS ( SELECT 1 FROM unified_sales_splits uss + INNER JOIN chart_of_accounts coa ON uss.category_id = coa.id WHERE uss.sale_id = us.id + AND ( + LOWER(COALESCE(coa.account_name, '')) LIKE '%tip%' + OR LOWER(COALESCE(coa.account_subtype::TEXT, '')) LIKE '%tip%' + ) )This keeps the tip in the uncategorized CTE until it's specifically categorized as a tip, preventing silent data loss from miscategorization.
📝 Committable suggestion
🤖 Prompt for AI Agents