Skip to content

feat(reports): WhatsApp sales and auto Z report - #1407

Merged
khaira777 merged 9 commits into
mainfrom
fm/flocafe-1336-whatsapp-reports
Oct 29, 2026
Merged

khaira777 merged 9 commits into
mainfrom
fm/flocafe-1336-whatsapp-reports

Conversation

@khaira777

@khaira777 khaira777 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1336

Summary & Behavior

  • Adds Send via WhatsApp to the Daily Sales export menu (POST /api/reports/daily-sales/whatsapp, formatDailySalesWhatsAppMessage, and shareDailySalesViaWhatsApp via SendWhatsAppReportModal) for a single business date or inclusive range up to 93 business dates from buildDailySalesExportDataset, formatted in the configured store language, regional currency/number/calendar preferences, store timezone, and day_close_hour boundaries.
  • Keeps plain-text report bodies within the 4096-character WhatsApp transport cap by shortening optional product/payment detail before omitting financial totals, never sends file/PDF attachments, and distinguishes confirmed connected transport sends from manual wa.me share-window fallbacks.
  • Enforces reports.view, reports.daily-sales.export, and whatsapp.use on daily-sales sends and retries, hides z_report history/retry from users without reports.view, hides daily_sales_report history/retry from users lacking reports.view or reports.daily-sales.export, and exempts owner/report sends from customer-campaign quiet hours and weekly caps while still obeying disabled WhatsApp, recipient blocklists, Offline mode, and transport cooldown/rate limits.
  • Adds an optional owner-only Z-on-day-close automation rule (z_report_day_close), disabled by default with an explicit E.164 recipient (never inferred from business_phone), triggered post-commit only on store-level day closes (scope === 'day', never per-staff shift closes) without ever failing or rolling back closing, persisted as at most one automatic attempt per closure (cash_closure_id) with manual retry instead of automatic retry on disconnection/timeout, suppressed with no delayed send in Offline mode, and never opening a manual share window.

Dependencies

No new runtime or development packages added; reuses existing WhatsApp service/history/retry infrastructure, SendWhatsAppReportModal, and buildDailySalesExportDataset.

Migration Implications

Adds additive SQLite schema migration v122 (add_whatsapp_daily_sales_and_day_close_dispatch) in main/db.ts that rebuilds whatsapp_messages to permit kind = 'daily_sales_report', adds nullable cash_closure_id INTEGER REFERENCES cash_closures(id) ON DELETE SET NULL, and creates partial unique index idx_whatsapp_messages_auto_z_closure on (cash_closure_id) where trigger_rule = 'z_report_day_close' AND cash_closure_id IS NOT NULL, preserving all existing customer data, attempts, and foreign keys across upgrades.

Tests Run

npm run lint, npm run lint:budget, npm run build, npm run build:frontend, npm run i18n:check, npm run docs:check, npm run audit:db, npm run test:script-coverage, git diff --check, node tests/run-electron-node-test.cjs tests/reports-whatsapp.test.ts, npm run test:whatsapp-schema, npm run test:upgrade-path, npm run test:daily-sales-export, npm run test:security, npm run test:backup, Playwright E2E (frontend/e2e/cash-close-whatsapp-recipient.spec.ts and frontend/e2e/database-export-import.spec.ts), and full GitHub Actions CI.

Remaining Limitations

Real end-to-end WhatsApp receipt on a physical linked WhatsApp account was not verified; external WhatsApp/Baileys transport was simulated at the socket boundary in isolated temporary directories without touching any host WhatsApp session.

Intent

Implement FloCafe issue #1336 (feat(reports): send daily sales via WhatsApp and optional auto-send of Z on day close):

  • Add Send via WhatsApp to the daily-sales report using the existing recipient modal.
  • Use the selected export range, with the existing 93-business-date maximum and canonical backend export dataset.
  • Send plain text containing the period, collected/refunded/net totals, tax/discount/charge totals, payment mix, and top five items by net item sales.
  • Use the store language, regional number/currency formatting, timezone, and business-date boundaries.
  • Keep messages within the existing transport limit. Shorten optional item detail before omitting financial totals. No PDF/file attachments.
  • Require reports.view, the existing daily-sales export authority, and whatsapp.use. Hide all report message kinds from users without report access.
  • Add an optional Z-on-day-close rule, off by default, with one explicit owner-configured recipient. Do not infer that recipient from the business phone.
  • Trigger after a successful day close, not a per-staff shift close. Sending failures must never fail or roll back closing.
  • Persist one automatic attempt per closure, preventing duplicate sends from repeated requests or restarts. Do not automatically retry after disconnection or uncertain delivery; expose failure/interruption and manual retry.
  • Explicit Offline mode suppresses automatic sending with no delayed send on reconnection.
  • Owner reports bypass customer-campaign quiet hours and weekly caps. They still obey disabled WhatsApp, blocked recipients, and transport cooldown/rate limits.
  • Reuse existing history and retry mechanisms. Distinguish a confirmed transport send from an opened manual-share window. Automatic sends never open a share window.

What Changed

  • Added POST /api/reports/daily-sales/whatsapp, formatDailySalesWhatsAppMessage, and dashboard export-menu WhatsApp sharing (shareDailySalesViaWhatsApp) to send plain-text daily sales summaries across 1-93 business dates in the store language and regional format (truncating top-item detail before financial totals within the 4096-character limit, with wa.me share fallback only when disconnected).
  • Added the owner-only, off-by-default z_report_day_close automation rule with explicit E.164 recipient configuration, SQLite migration v122 (daily_sales_report message kind, cash_closure_id, and idx_whatsapp_messages_auto_z_closure), and post-commit triggerDayCloseZReport dispatch for store-level day closes that suppresses cleanly in Offline mode or when WhatsApp is disabled and records single-attempt failures for manual retry.
  • Enforced reports.view and reports.daily-sales.export across WhatsApp send, pre-pagination message history, and retry paths, exempted report message kinds from customer quiet hours and weekly caps while keeping transport rate/blocklist/length guards, and updated all 24 locale catalogues, generated print labels, tests, and documentation.

Risk Assessment

✅ Low: The implementation and follow-up fixes cleanly enforce permission boundaries, migration safety, single-attempt day-close deduplication, Offline-mode suppression, and progressive 4096-character truncation across all 44 changed files with no remaining defects or unrequired components.

Testing

Built the backend and static desktop frontend, passed targeted backend integration, migration, schema, print-label, translation, daily-sales-export, and Playwright E2E suites, and drove seven live end-to-end browser and API scenarios against an isolated FloCafe server covering Dashboard Daily Sales WhatsApp sharing, 93-day range and 4096-character truncation boundaries, store-locale and 04:00 business-day formatting, Owner/Manager/Server history RBAC hiding, owner-only Z-on-day-close configuration, single-attempt day-close persistence and restart ack_timeout handling, and quiet-hour/weekly-cap/blocklist/Offline-mode guards with 11 UI screenshots and 7 JSON transcripts captured as evidence (only live paired Meta WhatsApp Web socket transmission remained untested without a physical mobile WhatsApp account).

  • Live validation: ✅ go - 7 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Store owner sends the Daily Sales report via WhatsApp from the Dashboard recipient modal for a single business date and an inclusive 93-business-date range, receiving plain text with period, gross/ref… ✅ pass live 01-dashboard-export-whatsapp-menu.png, 02-daily-sales-whatsapp-modal.png, 03-daily-sales-manual-share-toast.png, scenario-01-daily-sales-whatsapp.json
Adversarial Daily Sales requests reject ranges exceeding 93 business dates with 400 invalid_range, ignore query-string date/from/to parameters when omitted from the JSON body, and truncate oversized i… ✅ pass live scenario-02-boundaries-and-truncation.json
Daily Sales WhatsApp report formatting honors the configured store language over caller Accept-Language headers, regional digit and currency preferences, store timezone (America/Toronto), and 04:00 bu… ✅ pass live scenario-03-localization-and-boundaries.json
Role-based permissions require reports.view, reports.daily-sales.export, and whatsapp.use to send or retry Daily Sales reports, ignore any ?kind= query filter on GET /api/whatsapp/messages, hide daily… ✅ pass live 08-whatsapp-history-owner.png, 09-whatsapp-history-manager-hides-daily-sales.png, 10-whatsapp-history-staff-hides-all-reports.png, scenario-04-rbac-and-history-hiding.json
Z-Report on day close automation rule starts disabled by default without inferring business_phone, rejects enabling without a valid phone, normalizes owner-entered phone numbers to E.164, disables UI… ✅ pass live 04-whatsapp-z-rule-default-empty.png, 05-whatsapp-z-rule-owner-saved.png, 06-whatsapp-z-rule-manager-disabled.png, scenario-05-z-rule-config-and-guards.json
Closing a per-staff cash shift never triggers an automatic Z-Report, whereas completing a store Day Close succeeds (HTTP 201) even when WhatsApp fails, never opens a manual-share window, persists at m… ✅ pass live 07-dashboard-day-close-completed.png, 08-whatsapp-history-owner.png, scenario-06-day-close-and-restart.json
Owner reports bypass customer-campaign quiet hours and weekly automated caps (and do not consume customer weekly caps), still obey recipient blocklist and disabled WhatsApp without opening wa.me windo… ✅ pass live 11-dashboard-blocked-recipient-toast.png, scenario-07-offline-and-safety-guards.json
Connected Baileys WhatsApp Web socket delivers Daily Sales and Day Close Z-Report messages end-to-end, completes manual Retry from Sent history, and enforces post-JID transport cooldown and per-recipi… ⏸️ untested no Tried driving connected-socket transmission against the live dev-server.js backend, but whatsapp.ts only enters state.state === 'connected' and reaches resolveJid/isInCooldown/isOverRateLimit/socket.s…

Dashboard Export sales dropdown with Send to WhatsApp action
SendWhatsAppReportModal opened from Dashboard Daily Sales export
Dashboard manual-share toast distinguishing wa.me fallback from confirmed transport send
WhatsApp Templates and Rules showing Z-Report on day close disabled by default with empty recipient (not inferred from business phone)
WhatsApp Templates and Rules after owner enables Z-Report on day close with normalized E.164 recipient
WhatsApp Templates and Rules viewed by Manager showing disabled owner-only Z-Report rule controls
Completed store-level Day Close wizard succeeding without rollback or popup when WhatsApp is disconnected
Owner WhatsApp Sent history showing Z-Report, Daily sales report, ack_timeout status, and manual Retry actions
Manager WhatsApp Sent history hiding Daily sales report rows while keeping Z-Report and customer messages
Server staff WhatsApp Sent history hiding all report message kinds while keeping non-report messages
Dashboard error toast rejecting Daily Sales WhatsApp send to blocked recipient without opening wa.me window

Evidence: Live Scenario 1 API payload, formatted plain-text Daily Sales body, manual-share URL, and 93-day range response
{
  "singleDayRequest": {
    "date": "2026-10-29",
    "phone_e164": "+14165559876"
  },
  "singleDayResponse": {
    "fallback": true,
    "reason": "not_connected",
    "body": "*Cafe Sol*\nDaily Sales Report\nBusiness date: 2026-10-29 (America/Toronto)\n2 orders · 2 bills\n\n*SALES SUMMARY*\nGross Collections: $418.00\nRefunds: $15.00\n*Net Collections: $403.00*\nTax: $37.00\nDiscount: $35.00\nService Charge: $9.00\nPackaging: $2.50\nDelivery: $4.50\n\n*Payment Methods*\n- Cash: $235.00\n- Card: $120.00\n- Wallet: $48.00\n\n*Top Products*\n1. Artisan Cold Brew Tower (4) - $120.00\n2. Pistachio Cortado (4) - $90.00\n3. Pour Over Geisha (4) - $70.00\n4. Ceremonial Matcha (4) - $50.00\n5. Oat Flat White (3) - $30.00"
  },
  "openedManualShareUrl": "https://wa.me/14165559876?text=*Cafe%20Sol*%0ADaily%20Sales%20Report%0ABusiness%20date%3A%202026-10-29%20(America%2FToronto)%0A2%20orders%20%C2%B7%202%20bills%0A%0A*SALES%20SUMMARY*%0AGross%20Collections%3A%20%24418.00%0ARefunds%3A%20%2415.00%0A*Net%20Collections%3A%20%24403.00*%0ATax%3A%20%2437.00%0ADiscount%3A%20%2435.00%0AService%20Charge%3A%20%249.00%0APackaging%3A%20%242.50%0ADelivery%3A%20%244.50%0A%0A*Payment%20Methods*%0A-%20Cash%3A%20%24235.00%0A-%20Card%3A%20%24120.00%0A-%20Wallet%3A%20%2448.00%0A%0A*Top%20Products*%0A1.%20Artisan%20Cold%20Brew%20Tower%20(4)%20-%20%24120.00%0A2.%20Pistachio%20Cortado%20(4)%20-%20%2490.00%0A3.%20Pour%20Over%20Geisha%20(4)%20-%20%2470.00%0A4.%20Ceremonial%20Matcha%20(4)%20-%20%2450.00%0A5.%20Oat%20Flat%20White%20(3)%20-%20%2430.00",
  "maxAllowed93DayRangeResponse": {
    "fallback": true,
    "reason": "not_connected",
    "body": "*Cafe Sol*\nDaily Sales Report\nPeriod: 2026-07-29 to 2026-10-29 (America/Toronto)\n5 orders · 5 bills\n\n*SALES SUMMARY*\nGross Collections: $919.80\nRefunds: $15.00\n*Net Collections: $904.80*\nTax: $50.80\nDiscount: $35.00\nService Charge: $9.00\nPackaging: $2.50\nDelivery: $4.50\n\n*Payment Methods*\n- Cash: $290.00\n- Card: $216.80\n- Wallet: $48.00\n- CustomMethod35LongLabel: $10.00\n- CustomMethod34LongLabel: $10.00\n- CustomMethod33LongLabel: $10.00\n- CustomMethod32LongLabel: $10.00\n- CustomMethod31LongLabel: $10.00\n- CustomMethod30LongLabel: $10.00\n- CustomMethod29LongLabel: $10.00\n- CustomMethod28LongLabel: $10.00\n- CustomMethod27LongLabel: $10.00\n- CustomMethod26LongLabel: $10.00\n- CustomMethod25LongLabel: $10.00\n- CustomMethod24LongLabel: $10.00\n- CustomMethod23LongLabel: $10.00\n- CustomMethod22LongLabel: $10.00\n- CustomMethod21LongLabel: $10.00\n- CustomMethod20LongLabel: $10.00\n- CustomMethod19LongLabel: $10.00\n- CustomMethod18LongLabel: $10.00\n- CustomMethod17LongLabel: $10.00\n- CustomMethod16LongLabel: $10.00\n- CustomMethod15LongLabel: $10.00\n- CustomMethod14LongLabel: $10.00\n- CustomMethod13LongLabel: $10.00\n- CustomMethod12LongLabel: $10.00\n- CustomMethod11LongLabel: $10.00\n- CustomMethod10LongLabel: $10.00\n- CustomMethod09LongLabel: $10.00\n- CustomMethod08LongLabel: $10.00\n- CustomMethod07LongLabel: $10.00\n- CustomMethod06LongLabel: $10.00\n- CustomMethod05LongLabel: $10.00\n- CustomMethod04LongLabel: $10.00\n- CustomMethod03LongLabel: $10.00\n- CustomMethod02LongLabel: $10.00\n- CustomMethod01LongLabel: $10.00\n\n*Top Products*\n1. Pistachio Cortado (6) - $140.00\n2. Artisan Cold Brew Tower (4) - $120.00\n3. Late Night Geisha (2) - $88.00\n4. UltraVerboseItemTitle_1_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (5) - $70.00\n5. Pour Over Geisha (4) - $70.00"
  }
}
Evidence: Live Scenario 2 adversarial 94-day rejection, ignored query-string fallback, and 4096-character truncation transcript
{
  "oversized94DayRangeRejected": {
    "status": 400,
    "data": {
      "error": "Date range exceeds maximum of 93 days (requested 94 days)",
      "reason": "invalid_range"
    }
  },
  "queryStringFallbackIgnored": {
    "requestUrl": "/api/reports/daily-sales/whatsapp?date=2020-01-01&from=2020-01-01&to=2020-01-05",
    "requestBody": {
      "phone_e164": "+14165559876"
    },
    "status": 200,
    "periodLine": [
      "*Cafe Sol*",
      "Daily Sales Report",
      "Business date: 2026-10-29 (America/Toronto)",
      "2 orders · 2 bills"
    ]
  },
  "transportLimitTruncation": {
    "bodyLength": 1970,
    "maxChars": 4096,
    "body": "*Cafe Sol*\nDaily Sales Report\nBusiness date: 2026-10-10 (America/Toronto)\n1 orders · 1 bills\n\n*SALES SUMMARY*\nGross Collections: $350.00\nRefunds: $0.00\n*Net Collections: $350.00*\nTax: $0.00\nDiscount: $0.00\nService Charge: $0.00\nPackaging: $0.00\nDelivery: $0.00\n\n*Payment Methods*\n- CustomMethod35LongLabel: $10.00\n- CustomMethod34LongLabel: $10.00\n- CustomMethod33LongLabel: $10.00\n- CustomMethod32LongLabel: $10.00\n- CustomMethod31LongLabel: $10.00\n- CustomMethod30LongLabel: $10.00\n- CustomMethod29LongLabel: $10.00\n- CustomMethod28LongLabel: $10.00\n- CustomMethod27LongLabel: $10.00\n- CustomMethod26LongLabel: $10.00\n- CustomMethod25LongLabel: $10.00\n- CustomMethod24LongLabel: $10.00\n- CustomMethod23LongLabel: $10.00\n- CustomMethod22LongLabel: $10.00\n- CustomMethod21LongLabel: $10.00\n- CustomMethod20LongLabel: $10.00\n- CustomMethod19LongLabel: $10.00\n- CustomMethod18LongLabel: $10.00\n- CustomMethod17LongLabel: $10.00\n- CustomMethod16LongLabel: $10.00\n- CustomMethod15LongLabel: $10.00\n- CustomMethod14LongLabel: $10.00\n- CustomMethod13LongLabel: $10.00\n- CustomMethod12LongLabel: $10.00\n- CustomMethod11LongLabel: $10.00\n- CustomMethod10LongLabel: $10.00\n- CustomMethod09LongLabel: $10.00\n- CustomMethod08LongLabel: $10.00\n- CustomMethod07LongLabel: $10.00\n- CustomMethod06LongLabel: $10.00\n- CustomMethod05LongLabel: $10.00\n- CustomMethod04LongLabel: $10.00\n- CustomMethod03LongLabel: $10.00\n- CustomMethod02LongLabel: $10.00\n- CustomMethod01LongLabel: $10.00\n\n*Top Products*\n1. UltraVerboseItemTitle_1_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX... (5) - $70.00\n2. UltraVerboseItemTitle_2_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX... (4) - $70.00\n3. UltraVerboseItemTitle_3_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX... (3) - $70.00\n4. UltraVerboseItemTitle_4_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX... (2) - $70.00\n5. UltraVerboseItemTitle_5_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX... (1) - $70.00"
  }
}
Evidence: Live Scenario 3 business-day 04:00 cutoff and Spanish/Arabic store-language formatting transcript
{
  "oct05Includes0230LocalSaleBefore0400Cutoff": "*Cafe Sol*\nDaily Sales Report\nBusiness date: 2026-10-05 (America/Toronto)\n1 orders · 1 bills\n\n*SALES SUMMARY*\nGross Collections: $96.80\nRefunds: $0.00\n*Net Collections: $96.80*\nTax: $8.80\nDiscount: $0.00\nService Charge: $0.00\nPackaging: $0.00\nDelivery: $0.00\n\n*Payment Methods*\n- Card: $96.80\n\n*Top Products*\n1. Late Night Geisha (2) - $88.00",
  "oct06Excludes0230LocalSale": "*Cafe Sol*\nDaily Sales Report\nBusiness date: 2026-10-06 (America/Toronto)\n0 orders · 0 bills\n\n*SALES SUMMARY*\nGross Collections: $0.00\nRefunds: $0.00\n*Net Collections: $0.00*\nTax: $0.00\nDiscount: $0.00\nService Charge: $0.00\nPackaging: $0.00\nDelivery: $0.00\n\n*Payment Methods*\n- (none)\n\n*Top Products*\n- (none)",
  "spanishStoreLanguageReport": "*Cafe Sol*\nInforme de ventas diarias\nFecha comercial: 2026-10-29 (America/Toronto)\n2 pedidos · 2 facturas\n\n*RESUMEN DE VENTAS*\nCobros Brutos: $418.00\nReembolsos: $15.00\n*Cobros Netos: $403.00*\nIVA: $37.00\nDescuento: $35.00\nCargo por servicio: $9.00\nEnvase: $2.50\nDelivery: $4.50\n\n*Métodos de Pago*\n- Efectivo: $235.00\n- Tarjeta: $120.00\n- Billetera: $48.00\n\n*Productos Más Vendidos*\n1. Artisan Cold Brew Tower (4) - $120.00\n2. Pistachio Cortado (4) - $90.00\n3. Pour Over Geisha (4) - $70.00\n4. Ceremonial Matcha (4) - $50.00\n5. Oat Flat White (3) - $30.00",
  "arabicStoreLanguageReport": "*Cafe Sol*\nتقرير المبيعات اليومية\nتاريخ المعاملة: 2026-10-29 (America/Toronto)\nطلبات 2 · فواتير 2\n\n*ملخص المبيعات*\nإجمالي الإيرادات: $418.00\nعمليات استرداد الأموال: $15.00\n*إجمالي التحصيلات الصافية: $403.00*\nالضرائب: $37.00\nالخصم: $35.00\nرسوم الخدمة: $9.00\nالتغليف: $2.50\nتوصيل: $4.50\n\n*طرق الدفع*\n- النقد: $235.00\n- بطاقة: $120.00\n- المحفظة: $48.00\n\n*أفضل المنتجات*\n1. Artisan Cold Brew Tower (4) - $120.00\n2. Pistachio Cortado (4) - $90.00\n3. Pour Over Geisha (4) - $70.00\n4. Ceremonial Matcha (4) - $50.00\n5. Oat Flat White (3) - $30.00"
}
Evidence: Live Scenario 4 Owner, Manager, and Server RBAC and server-side history filtering transcript
{
  "ownerVisibleKindsWithIgnoredKindQueryParam": [
    "auto_followup",
    "bill_receipt",
    "daily_sales_report",
    "z_report"
  ],
  "managerVisibleKinds": [
    "auto_followup",
    "bill_receipt",
    "z_report"
  ],
  "managerDailySalesRoute403": {
    "status": 403,
    "data": {
      "error": "Insufficient permissions",
      "code": "permission_denied",
      "permission": "reports.daily-sales.export"
    }
  },
  "managerDirectSendDailySales403": {
    "status": 403,
    "data": {
      "error": "Insufficient permissions",
      "required": "reports.daily-sales.export",
      "code": "PERMISSION_DENIED"
    }
  },
  "managerRetryDailySales403": {
    "status": 403,
    "data": {
      "error": "Insufficient permissions",
      "required": "reports.daily-sales.export",
      "code": "PERMISSION_DENIED"
    }
  },
  "staffWithoutReportsViewVisibleKinds": [
    "auto_followup",
    "bill_receipt"
  ],
  "staffRetryZReport403": {
    "status": 403,
    "data": {
      "error": "Insufficient permissions",
      "required": "reports.view",
      "code": "PERMISSION_DENIED"
    }
  },
  "staffRetryDailySales403": {
    "status": 403,
    "data": {
      "error": "Insufficient permissions",
      "required": "reports.view",
      "code": "PERMISSION_DENIED"
    }
  },
  "seededBillReceiptId": 5
}
Evidence: Live Scenario 5 Z-Report rule defaults, phone validation, 403 owner_required guard, and rule-preservation transcript
{
  "businessPhoneConfigured": "+14165550000",
  "initialZRule": {
    "id": "z_report_day_close",
    "trigger": "z_report_day_close",
    "enabled": false,
    "templateId": "receipt",
    "recipientPhone": ""
  },
  "enableWithoutPhoneRejected": {
    "status": 400,
    "data": {
      "error": "Recipient phone is required when automatic Z-report delivery is enabled",
      "reason": "phone_required"
    }
  },
  "managerTamperRejected": {
    "status": 403,
    "data": {
      "error": "Only the store owner can configure automatic Z-report delivery",
      "reason": "owner_required",
      "code": "PERMISSION_DENIED"
    }
  },
  "managerOtherRuleSavePreservedOwnerZRule": {
    "id": "z_report_day_close",
    "trigger": "z_report_day_close",
    "enabled": true,
    "templateId": "receipt",
    "recipientPhone": "+14165557788"
  }
}
Evidence: Live Scenario 6 shift-close non-trigger, day-close single attempt persistence, unique-index deduplication, and restart ack_timeout transcript
{
  "shiftCloseTriggeredZReports": 0,
  "dayCloseStatus": 201,
  "dayCloseShareWindowsOpened": 0,
  "persistedAutoZMessage": {
    "id": 1,
    "phone_e164": "+14165557788",
    "bill_id": null,
    "customer_id": null,
    "cash_closure_id": 2,
    "direction": "outbound",
    "kind": "z_report",
    "status": "failed",
    "body": "📊 *Z report #2 - Cafe Sol*\n📅 *Business date:* 2026-10-29 (10/29/26, 4:00 AM - 10/30/26, 4:00 AM)\n👤 *Closed by:* Elena Vance\n\n💰 *SALES SUMMARY*\n• Gross Collections: $418.00\n• Refunds: $15.00\n• Net Collections: $403.00\n• 2 bills\n\n💳 *PAYMENTS*\n• Cash: $235.00 (2)\n• Card: $120.00 (1)\n• Wallet: $48.00 (1)\n\n💵 *DRAWER RECONCILIATION*\n• Opening float: $50.00\n• Expected cash: $285.00\n• Counted cash: $285.00\n• Difference: $0.00 (✅ Exact)\n\n📝 *Notes:* (none)",
    "error": "Flo is not connected to WhatsApp.",
    "queued_at": "2026-10-29 16:16:00",
    "seen_at": null,
    "typing_at": null,
    "sent_at": null,
    "delivered_at": null,
    "read_at": null,
    "failed_at": "2026-10-29 16:16:00",
    "created_by_user_id": "rule:z_report_day_close",
    "trigger_rule": "z_report_day_close"
  },
  "uniqueIndexBlockedDuplicateClosureAttempt": true,
  "interruptedMessageAfterServerRestart": {
    "id": 3,
    "phone_e164": "+14165557788",
    "bill_id": null,
    "customer_id": null,
    "cash_closure_id": null,
    "direction": "outbound",
    "kind": "z_report",
    "status": "failed",
    "body": "📊 *Z report #99 - Cafe Sol* (interrupted before ack)",
    "error": "ack_timeout",
    "queued_at": "2026-10-29 16:15:51",
    "seen_at": null,
    "typing_at": null,
    "sent_at": null,
    "delivered_at": null,
    "read_at": null,
    "failed_at": "2026-10-29 16:16:01",
    "created_by_user_id": "rule:z_report_day_close",
    "trigger_rule": "z_report_day_close"
  }
}
Evidence: Live Scenario 7 quiet-hours and weekly-cap bypass, blocklist/disabled obedience, and Offline mode suppression transcript
{
  "customerCampaignBlockedByQuietHours": {
    "status": 400,
    "data": {
      "error": "Automated messages are paused during quiet hours.",
      "reason": "quiet_hours"
    }
  },
  "ownerReportBypassedQuietHours": {
    "status": 200,
    "reason": "not_connected"
  },
  "customerCampaignBlockedByWeeklyCap": {
    "status": 400,
    "data": {
      "error": "Weekly automated message limit reached for this customer.",
      "reason": "weekly_cap"
    }
  },
  "ownerReportBypassedWeeklyCap": {
    "status": 200,
    "reason": "not_connected"
  },
  "priorZReportsDidNotConsumeWeeklyCap": {
    "status": 503,
    "data": {
      "error": "Flo is not connected to WhatsApp.",
      "reason": "not_connected"
    }
  },
  "blockedRecipientRejected": {
    "error": "This number asked to stop receiving messages.",
    "reason": "blocked"
  },
  "blockedRecipientPopupClosed": {
    "href": "",
    "closed": true
  },
  "disabledWhatsAppRejected": {
    "status": 400,
    "data": {
      "error": "WhatsApp is not enabled.",
      "reason": "feature_off"
    }
  },
  "manualSendDuringOfflineRejected": {
    "status": 400,
    "data": {
      "error": "Offline mode is on.",
      "reason": "offline_mode"
    }
  },
  "offlineDayCloseClosureId": 3,
  "autoZMessagesDuringOffline": 0,
  "autoZMessagesAfterExitingOfflineMode": 0
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 5 issues found → auto-fixed (4) ✅
  • ⚠️ main/routes/whatsapp.ts:406 - Missing access-policy decision on secondary financial disclosure in WhatsApp message history: POST /api/reports/daily-sales/whatsapp (main/routes/reports.ts:1800), POST /api/whatsapp/send (main/routes/whatsapp.ts:165), POST /api/whatsapp/messages/:id/retry (main/routes/whatsapp.ts:234), and the UI retry action (frontend/src/app/(dashboard)/whatsapp/page.tsx:1240) all require both reports.view and reports.daily-sales.export (owner-only by default in shared/permissions.ts:409) for daily_sales_report messages. However, GET /api/whatsapp/messages (main/routes/whatsapp.ts:406) and whatsapp.listMessages (main/services/whatsapp.ts:2650, documented at docs/reference/api.md:775 and docs/architecture/cloud-integrations.md:381) check only reports.view, so after an owner sends a daily sales report, a default manager (who holds whatsapp.use + reports.view but is denied reports.daily-sales.export and reports.financial.view) receives the full up-to-93-day daily_sales_report body containing gross/refund/net collections, tax/discount/charge totals, payment mix, and top-5 net item sales. Confirm whether GET /api/whatsapp/messages and whatsapp.listMessages should also hide kind = 'daily_sales_report' rows when the caller lacks reports.daily-sales.export.
  • ⚠️ main/routes/reports.ts:1867 - POST /api/reports/daily-sales/whatsapp swallows RegionalNotConfiguredError (statusCode = 409) as HTTP 500: when tenant country, currency, or timezone is missing, reportToday() (main/routes/reports.ts:1823) and buildDailySalesExportDataset() (main/routes/reports.ts:1828) throw RegionalNotConfiguredError with statusCode = 409, which FloCafe routes must surface as HTTP 409 per docs/reference/product-invariants.md:240, docs/reference/daily-sales-export.md:19, and sibling export handlers (main/routes/reports.ts:1968, main/routes/reports.ts:2022). The catch block at main/routes/reports.ts:1867 handles only statusCode === 400 and falls through to 500 on 409; sibling doc docs/reference/api.md:870 also omits 409 from the endpoint error list.
  • ⚠️ main/services/whatsapp.ts:3792 - formatZReportDateTime ignores tenant calendar and number_digits regional preferences when formatting closure.period_start and closure.period_end (main/services/whatsapp.ts:3792, called at main/services/whatsapp.ts:3854). formatStoredZReportWhatsAppMessage resolves preferences (digits, calendar) and applies them to money (fmtMoney, line 3843) and counts (fmtNum, line 3844), but formatZReportDateTime calls new Intl.DateTimeFormat(locale, { dateStyle: 'short', timeStyle: 'short', timeZone }) without numberingSystem or calendar. For example, an IR (fa-IR) store configured with calendar = 'gregorian' and number_digits = 'latin' gets Latin digits and Gregorian business_date everywhere else in the Z-report message while period_start/period_end render in the Persian Jalali calendar with Extended Arabic-Indic digits (and a store with calendar = 'persian' on a non-fa-IR locale gets Gregorian timestamps). Pass preferences (or delegate to formatDateForTenant in main/countries.ts:481) so period timestamps honor digits and calendar.
  • ⚠️ main/routes/whatsapp.ts:399 - Unrequired component (?kind= query filter on GET /api/whatsapp/messages): main/routes/whatsapp.ts:399 and main/routes/whatsapp.ts:411, main/services/whatsapp.ts:2607 and main/services/whatsapp.ts:2638, and docs/reference/api.md:775 add a client-supplied kind filter to WhatsApp message history. No User Intent requirement needs a ?kind= query parameter (intent requires only server-side hiding of report message kinds from users without report access and reusing existing history/retry), and no production caller in frontend/ or main/ uses it (only tests/reports-whatsapp.test.ts:516 and tests/reports-whatsapp.test.ts:1083). Recommend removing the ?kind= query filter and filtering returned messages in the test assertions instead.
  • ⚠️ main/routes/reports.ts:1820 - Unrequired fallback acceptance path (req.query date/range fallback on POST /api/reports/daily-sales/whatsapp): main/routes/reports.ts:1820, main/routes/reports.ts:1821, and main/routes/reports.ts:1822 fall back to req.query.date, req.query.from, and req.query.to when omitted from req.body. docs/reference/api.md:870 specifies JSON body inputs (phone_e164, optional date or from + to), phone_e164 itself is read only from req.body, and no production caller, test, or User Intent requirement uses query-string parameters on this POST route. Recommend narrowing range parsing to payload.date, payload.from, and payload.to only.

🔧 Fix applied.
8 warnings still open:

  • ⚠️ main/routes/whatsapp.ts:406 - Missing access-policy decision on secondary financial disclosure in WhatsApp message history: POST /api/reports/daily-sales/whatsapp (main/routes/reports.ts:1800), POST /api/whatsapp/send (main/routes/whatsapp.ts:165), POST /api/whatsapp/messages/:id/retry (main/routes/whatsapp.ts:234), and the UI retry action (frontend/src/app/(dashboard)/whatsapp/page.tsx:1240) all require both reports.view and reports.daily-sales.export (owner-only by default in shared/permissions.ts:409) for daily_sales_report messages. However, GET /api/whatsapp/messages (main/routes/whatsapp.ts:406) and whatsapp.listMessages (main/services/whatsapp.ts:2650, documented at docs/reference/api.md:775 and docs/architecture/cloud-integrations.md:381) check only reports.view, so after an owner sends a daily sales report, a default manager (who holds whatsapp.use + reports.view but is denied reports.daily-sales.export and reports.financial.view) receives the full up-to-93-day daily_sales_report body containing gross/refund/net collections, tax/discount/charge totals, payment mix, and top-5 net item sales. Confirm whether GET /api/whatsapp/messages and whatsapp.listMessages should also hide kind = 'daily_sales_report' rows when the caller lacks reports.daily-sales.export.
  • ⚠️ main/routes/reports.ts:1867 - POST /api/reports/daily-sales/whatsapp swallows RegionalNotConfiguredError (statusCode = 409) as HTTP 500: when tenant country, currency, or timezone is missing, reportToday() (main/routes/reports.ts:1823) and buildDailySalesExportDataset() (main/routes/reports.ts:1828) throw RegionalNotConfiguredError with statusCode = 409, which FloCafe routes must surface as HTTP 409 per docs/reference/product-invariants.md:240, docs/reference/daily-sales-export.md:19, and sibling export handlers (main/routes/reports.ts:1968, main/routes/reports.ts:2022). The catch block at main/routes/reports.ts:1867 handles only statusCode === 400 and falls through to 500 on 409; sibling doc docs/reference/api.md:870 also omits 409 from the endpoint error list.
  • ⚠️ main/services/whatsapp.ts:3792 - formatZReportDateTime ignores tenant calendar and number_digits regional preferences when formatting closure.period_start and closure.period_end (main/services/whatsapp.ts:3792, called at main/services/whatsapp.ts:3854). formatStoredZReportWhatsAppMessage resolves preferences (digits, calendar) and applies them to money (fmtMoney, line 3843) and counts (fmtNum, line 3844), but formatZReportDateTime calls new Intl.DateTimeFormat(locale, { dateStyle: 'short', timeStyle: 'short', timeZone }) without numberingSystem or calendar. For example, an IR (fa-IR) store configured with calendar = 'gregorian' and number_digits = 'latin' gets Latin digits and Gregorian business_date everywhere else in the Z-report message while period_start/period_end render in the Persian Jalali calendar with Extended Arabic-Indic digits (and a store with calendar = 'persian' on a non-fa-IR locale gets Gregorian timestamps). Pass preferences (or delegate to formatDateForTenant in main/countries.ts:481) so period timestamps honor digits and calendar.
  • ⚠️ main/routes/whatsapp.ts:399 - Unrequired component (?kind= query filter on GET /api/whatsapp/messages): main/routes/whatsapp.ts:399 and main/routes/whatsapp.ts:411, main/services/whatsapp.ts:2607 and main/services/whatsapp.ts:2638, and docs/reference/api.md:775 add a client-supplied kind filter to WhatsApp message history. No User Intent requirement needs a ?kind= query parameter (intent requires only server-side hiding of report message kinds from users without report access and reusing existing history/retry), and no production caller in frontend/ or main/ uses it (only tests/reports-whatsapp.test.ts:516 and tests/reports-whatsapp.test.ts:1083). Recommend removing the ?kind= query filter and filtering returned messages in the test assertions instead.
  • ⚠️ main/routes/reports.ts:1820 - Unrequired fallback acceptance path (req.query date/range fallback on POST /api/reports/daily-sales/whatsapp): main/routes/reports.ts:1820, main/routes/reports.ts:1821, and main/routes/reports.ts:1822 fall back to req.query.date, req.query.from, and req.query.to when omitted from req.body. docs/reference/api.md:870 specifies JSON body inputs (phone_e164, optional date or from + to), phone_e164 itself is read only from req.body, and no production caller, test, or User Intent requirement uses query-string parameters on this POST route. Recommend narrowing range parsing to payload.date, payload.from, and payload.to only.
  • ⚠️ main/services/whatsapp.ts:3938 - formatStoredZReportWhatsAppMessage (main/services/whatsapp.ts:3938) can truncate Drawer Reconciliation financial totals before shortening custom payment method rows. main/routes/payment-methods.ts:11 accepts custom payment method names up to 60 characters with no count cap (exercised with 48 60-character custom methods in tests/reports-whatsapp.test.ts:579-595). When a day close records 48 60-character payment methods, paymentLines (main/services/whatsapp.ts:3888-3892) renders ~3,648 characters ahead of Drawer Reconciliation (main/services/whatsapp.ts:3909-3913); even after notesCap = 0 and storeCap = 48 (main/services/whatsapp.ts:3928-3937), buildMessage(0, 48) is ~4,123 characters (> 4096), so line 3938 tail-slices buildMessage(0, 32) at 4,096 characters and drops Counted cash and Difference (variance) while leaving all 60-character payment method labels unshortened, violating the User Intent requirement to keep financial totals intact before shortening detail. Provenance: Round 1 modified formatStoredZReportWhatsAppMessage (main/services/whatsapp.ts:3868-3870) to forward preferences into formatZReportDateTime, leaving this pre-existing defect from the author's commit at main/services/whatsapp.ts:3889-3892 and main/services/whatsapp.ts:3928-3938; sibling formatter formatDailySalesWhatsAppMessage (main/services/daily-sales-export.ts:2471-2515) already caps payment method names and preserves all financial totals before fitting payment rows with ..., leaving formatStoredZReportWhatsAppMessage as the only remaining site.
  • ⚠️ main/routes/whatsapp.ts:406 - Missing access-policy decision on secondary financial disclosure in WhatsApp message history: POST /api/reports/daily-sales/whatsapp (main/routes/reports.ts:1800-1801), POST /api/whatsapp/send (main/routes/whatsapp.ts:165-166), POST /api/whatsapp/messages/:messageId/retry (main/routes/whatsapp.ts:234), and the UI retry action (frontend/src/app/(dashboard)/whatsapp/page.tsx:1240-1241) all require both reports.view and reports.daily-sales.export (owner-only by default in shared/permissions.ts:414-416, alongside reports.financial.view at shared/permissions.ts:407-409) for daily_sales_report messages. However, GET /api/whatsapp/messages (main/routes/whatsapp.ts:406) and whatsapp.listMessages (main/services/whatsapp.ts:2626 and main/services/whatsapp.ts:2650, documented at docs/reference/api.md:775 and docs/architecture/cloud-integrations.md:381) check only reports.view, so after an owner sends a daily sales report, a default manager (who holds whatsapp.use + reports.view but lacks reports.daily-sales.export and reports.financial.view) receives the full up-to-93-day daily_sales_report body containing gross/refund/net collections, tax/discount/charge totals, payment mix, and top-5 net item sales. Confirm whether GET /api/whatsapp/messages and whatsapp.listMessages should also hide kind = 'daily_sales_report' rows when the caller lacks reports.daily-sales.export.
  • ⚠️ main/routes/whatsapp.ts:399 - Unrequired component (?kind= query filter on GET /api/whatsapp/messages): main/routes/whatsapp.ts:399 and main/routes/whatsapp.ts:411, main/services/whatsapp.ts:2621 and main/services/whatsapp.ts:2638-2641, and docs/reference/api.md:775 add a client-supplied kind filter to WhatsApp message history. No User Intent requirement needs a ?kind= query parameter (intent requires only server-side hiding of report message kinds from users without report access and reusing existing history/retry), and no production caller in frontend/ or main/ uses it (only tests/reports-whatsapp.test.ts:516 and tests/reports-whatsapp.test.ts:1103). Recommend removing the ?kind= query filter and filtering returned messages in the test assertions instead.

🔧 Fix applied.
8 warnings still open:

  • ⚠️ main/routes/whatsapp.ts:406 - Missing access-policy decision on secondary financial disclosure in WhatsApp message history: POST /api/reports/daily-sales/whatsapp (main/routes/reports.ts:1800), POST /api/whatsapp/send (main/routes/whatsapp.ts:165), POST /api/whatsapp/messages/:id/retry (main/routes/whatsapp.ts:234), and the UI retry action (frontend/src/app/(dashboard)/whatsapp/page.tsx:1240) all require both reports.view and reports.daily-sales.export (owner-only by default in shared/permissions.ts:409) for daily_sales_report messages. However, GET /api/whatsapp/messages (main/routes/whatsapp.ts:406) and whatsapp.listMessages (main/services/whatsapp.ts:2650, documented at docs/reference/api.md:775 and docs/architecture/cloud-integrations.md:381) check only reports.view, so after an owner sends a daily sales report, a default manager (who holds whatsapp.use + reports.view but is denied reports.daily-sales.export and reports.financial.view) receives the full up-to-93-day daily_sales_report body containing gross/refund/net collections, tax/discount/charge totals, payment mix, and top-5 net item sales. Confirm whether GET /api/whatsapp/messages and whatsapp.listMessages should also hide kind = 'daily_sales_report' rows when the caller lacks reports.daily-sales.export.
  • ⚠️ main/routes/reports.ts:1867 - POST /api/reports/daily-sales/whatsapp swallows RegionalNotConfiguredError (statusCode = 409) as HTTP 500: when tenant country, currency, or timezone is missing, reportToday() (main/routes/reports.ts:1823) and buildDailySalesExportDataset() (main/routes/reports.ts:1828) throw RegionalNotConfiguredError with statusCode = 409, which FloCafe routes must surface as HTTP 409 per docs/reference/product-invariants.md:240, docs/reference/daily-sales-export.md:19, and sibling export handlers (main/routes/reports.ts:1968, main/routes/reports.ts:2022). The catch block at main/routes/reports.ts:1867 handles only statusCode === 400 and falls through to 500 on 409; sibling doc docs/reference/api.md:870 also omits 409 from the endpoint error list.
  • ⚠️ main/routes/whatsapp.ts:399 - Unrequired component (?kind= query filter on GET /api/whatsapp/messages): main/routes/whatsapp.ts:399 and main/routes/whatsapp.ts:411, main/services/whatsapp.ts:2607 and main/services/whatsapp.ts:2638, and docs/reference/api.md:775 add a client-supplied kind filter to WhatsApp message history. No User Intent requirement needs a ?kind= query parameter (intent requires only server-side hiding of report message kinds from users without report access and reusing existing history/retry), and no production caller in frontend/ or main/ uses it (only tests/reports-whatsapp.test.ts:516 and tests/reports-whatsapp.test.ts:1083). Recommend removing the ?kind= query filter and filtering returned messages in the test assertions instead.
  • ⚠️ main/routes/reports.ts:1820 - Unrequired fallback acceptance path (req.query date/range fallback on POST /api/reports/daily-sales/whatsapp): main/routes/reports.ts:1820, main/routes/reports.ts:1821, and main/routes/reports.ts:1822 fall back to req.query.date, req.query.from, and req.query.to when omitted from req.body. docs/reference/api.md:870 specifies JSON body inputs (phone_e164, optional date or from + to), phone_e164 itself is read only from req.body, and no production caller, test, or User Intent requirement uses query-string parameters on this POST route. Recommend narrowing range parsing to payload.date, payload.from, and payload.to only.
  • ⚠️ main/routes/whatsapp.ts:406 - Missing access-policy decision on secondary financial disclosure in WhatsApp message history: POST /api/reports/daily-sales/whatsapp (main/routes/reports.ts:1800-1801), POST /api/whatsapp/send (main/routes/whatsapp.ts:165-166), POST /api/whatsapp/messages/:messageId/retry (main/routes/whatsapp.ts:234), and the UI retry action (frontend/src/app/(dashboard)/whatsapp/page.tsx:1240-1241) all require both reports.view and reports.daily-sales.export (owner-only by default in shared/permissions.ts:414-416, alongside reports.financial.view at shared/permissions.ts:407-409) for daily_sales_report messages. However, GET /api/whatsapp/messages (main/routes/whatsapp.ts:406) and whatsapp.listMessages (main/services/whatsapp.ts:2626 and main/services/whatsapp.ts:2650, documented at docs/reference/api.md:775 and docs/architecture/cloud-integrations.md:381) check only reports.view, so after an owner sends a daily sales report, a default manager (who holds whatsapp.use + reports.view but lacks reports.daily-sales.export and reports.financial.view) receives the full up-to-93-day daily_sales_report body containing gross/refund/net collections, tax/discount/charge totals, payment mix, and top-5 net item sales. Confirm whether GET /api/whatsapp/messages and whatsapp.listMessages should also hide kind = 'daily_sales_report' rows when the caller lacks reports.daily-sales.export.
  • ⚠️ main/routes/whatsapp.ts:399 - Unrequired component (?kind= query filter on GET /api/whatsapp/messages): main/routes/whatsapp.ts:399 and main/routes/whatsapp.ts:411, main/services/whatsapp.ts:2621 and main/services/whatsapp.ts:2638-2641, and docs/reference/api.md:775 add a client-supplied kind filter to WhatsApp message history. No User Intent requirement needs a ?kind= query parameter (intent requires only server-side hiding of report message kinds from users without report access and reusing existing history/retry), and no production caller in frontend/ or main/ uses it (only tests/reports-whatsapp.test.ts:516 and tests/reports-whatsapp.test.ts:1103). Recommend removing the ?kind= query filter and filtering returned messages in the test assertions instead.
  • ⚠️ main/routes/whatsapp.ts:406 - Missing access-policy decision on secondary financial disclosure in WhatsApp message history: POST /api/reports/daily-sales/whatsapp (main/routes/reports.ts:1800-1801), POST /api/whatsapp/send (main/routes/whatsapp.ts:165-166), POST /api/whatsapp/messages/:messageId/retry (main/routes/whatsapp.ts:234), and the UI retry button (frontend/src/app/(dashboard)/whatsapp/page.tsx:1240-1241) all require both reports.view and reports.daily-sales.export (owner-only by default in shared/permissions.ts:414-416, alongside reports.financial.view at shared/permissions.ts:407-409) for daily_sales_report messages. However, GET /api/whatsapp/messages (main/routes/whatsapp.ts:406) and whatsapp.listMessages (main/services/whatsapp.ts:2626 and main/services/whatsapp.ts:2650, documented at docs/reference/api.md:775 and docs/architecture/cloud-integrations.md:381) check only reports.view, so after an owner sends a daily sales report, a shipped-default manager (who holds whatsapp.use and reports.view while denied reports.daily-sales.export and reports.financial.view) receives the full up-to-93-day daily_sales_report body containing gross/refund/net collections, tax/discount/charge totals, payment mix, and top-5 net item sales. Confirm whether GET /api/whatsapp/messages and whatsapp.listMessages should also hide kind = 'daily_sales_report' rows when the caller lacks reports.daily-sales.export.
  • ⚠️ main/routes/whatsapp.ts:399 - Unrequired component (?kind= query filter on GET /api/whatsapp/messages): main/routes/whatsapp.ts:399 and main/routes/whatsapp.ts:411, main/services/whatsapp.ts:2621 and main/services/whatsapp.ts:2638-2641, and docs/reference/api.md:775 add a client-supplied kind filter to WhatsApp message history. No User Intent requirement needs a ?kind= query parameter (intent requires only server-side hiding of report message kinds from users without report access and reusing existing history/retry), and no production caller in frontend/ or main/ uses it (only tests/reports-whatsapp.test.ts:516 and tests/reports-whatsapp.test.ts:1103-1105). Recommend removing the ?kind= query filter and asserting every((m) => m.kind !== 'daily_sales_report') on returned messages in tests/reports-whatsapp.test.ts:1103-1105 instead.

🔧 Fix applied.
1 warning still open:

  • ⚠️ main/services/whatsapp.ts:571 - Owner-only authorization bypass on z_report_day_close via non-string trigger coercion in PUT /api/whatsapp/automation: when the store owner enables z_report_day_close with a configured recipientPhone, an authenticated non-owner with whatsapp.manage (such as a default manager) can send PUT /api/whatsapp/automation with {"rules":[{"trigger":["z_report_day_close"]}]} (or {"rules":[{"id":["z_report_day_close"]}]}). In main/routes/whatsapp.ts:285-292, incomingZRule checks strict string equality (rec.trigger === 'z_report_day_close' || rec.id === 'z_report_day_close'), which evaluates to false for the array and skips the role !== 'owner' guard at main/routes/whatsapp.ts:320-327. Next, saveAutomationConfig (main/services/whatsapp.ts:571) coerces r.trigger ?? r.id with String(...) into 'z_report_day_close', adding it to providedTriggers and dropping the owner's stored z_report_day_close rule at main/services/whatsapp.ts:575, while normalizeWhatsAppAutomationConfig (shared/whatsapp-automation.ts:455) keys ruleByTrigger by the non-string array reference and resets z_report_day_close to { enabled: false, recipientPhone: '' } with HTTP 200. Accept only string trigger/id keys across main/services/whatsapp.ts:571, main/routes/whatsapp.ts:285-292, and shared/whatsapp-automation.ts:455 (and preserve currentZRule unconditionally when role !== 'owner' after rejecting explicit edits).

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 7 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Store owner sends the Daily Sales report via WhatsApp from the Dashboard recipient modal for a single business date and an inclusive 93-business-date range, receiving plain text with period, gross/ref… ✅ pass live 01-dashboard-export-whatsapp-menu.png, 02-daily-sales-whatsapp-modal.png, 03-daily-sales-manual-share-toast.png, scenario-01-daily-sales-whatsapp.json
Adversarial Daily Sales requests reject ranges exceeding 93 business dates with 400 invalid_range, ignore query-string date/from/to parameters when omitted from the JSON body, and truncate oversized i… ✅ pass live scenario-02-boundaries-and-truncation.json
Daily Sales WhatsApp report formatting honors the configured store language over caller Accept-Language headers, regional digit and currency preferences, store timezone (America/Toronto), and 04:00 bu… ✅ pass live scenario-03-localization-and-boundaries.json
Role-based permissions require reports.view, reports.daily-sales.export, and whatsapp.use to send or retry Daily Sales reports, ignore any ?kind= query filter on GET /api/whatsapp/messages, hide daily… ✅ pass live 08-whatsapp-history-owner.png, 09-whatsapp-history-manager-hides-daily-sales.png, 10-whatsapp-history-staff-hides-all-reports.png, scenario-04-rbac-and-history-hiding.json
Z-Report on day close automation rule starts disabled by default without inferring business_phone, rejects enabling without a valid phone, normalizes owner-entered phone numbers to E.164, disables UI… ✅ pass live 04-whatsapp-z-rule-default-empty.png, 05-whatsapp-z-rule-owner-saved.png, 06-whatsapp-z-rule-manager-disabled.png, scenario-05-z-rule-config-and-guards.json
Closing a per-staff cash shift never triggers an automatic Z-Report, whereas completing a store Day Close succeeds (HTTP 201) even when WhatsApp fails, never opens a manual-share window, persists at m… ✅ pass live 07-dashboard-day-close-completed.png, 08-whatsapp-history-owner.png, scenario-06-day-close-and-restart.json
Owner reports bypass customer-campaign quiet hours and weekly automated caps (and do not consume customer weekly caps), still obey recipient blocklist and disabled WhatsApp without opening wa.me windo… ✅ pass live 11-dashboard-blocked-recipient-toast.png, scenario-07-offline-and-safety-guards.json
Connected Baileys WhatsApp Web socket delivers Daily Sales and Day Close Z-Report messages end-to-end, completes manual Retry from Sent history, and enforces post-JID transport cooldown and per-recipi… ⏸️ untested no Tried driving connected-socket transmission against the live dev-server.js backend, but whatsapp.ts only enters state.state === 'connected' and reaches resolveJid/isInCooldown/isOverRateLimit/socket.s…
  • npm run build
  • HUSKY=0 npm run build:frontend
  • node tests/run-electron-node-test.cjs tests/reports-whatsapp.test.ts
  • npm run test:whatsapp-schema
  • npm run test:upgrade-path
  • npm run test:translations
  • npm run test:print-labels
  • npm run test:daily-sales-export
  • cd frontend && CI=1 E2E_BASE_URL=http://127.0.0.1:4301 E2E_KDS_BASE_URL=http://127.0.0.1:4302 E2E_SERVER_APP_BASE_URL=http://127.0.0.1:4303 npx playwright test --config=playwright.config.ts --project=chromium --retries=0 e2e/cash-close-whatsapp-recipient.spec.ts
  • Live end-to-end Playwright Chromium + HTTP API + SQLite verification against node dev-server.js serving built dist/ and frontend/out on isolated ports 14301/14302/14303 with disposable FLO_DEV_USER_DATA
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Send daily sales and day-close Z reports via WhatsApp

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add localized daily-sales WhatsApp summaries for selected ranges using the existing export
 dataset.
• Optionally send a stored Z report after day close, with an owner-configured recipient and one
 automatic attempt per closure.
• Extend permissions, message history, translations, documentation, and delivery-failure tests.
Diagram

graph TD
  D["Sales dashboard"] --> R["Report API"] --> E["Export dataset"] --> W["WhatsApp service"] --> H[("Message history")]
  C["Day close"] --> Z["Z dispatch"] --> W
  U["Owner rule"] --> Z
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Durable post-close job
  • ➕ Could recover dispatch after a process stops between close commit and attempt creation.
  • ➖ Requires cancellation and retry semantics to avoid delayed Offline-mode sends or duplicate delivery.
  • ➖ Adds infrastructure beyond the existing WhatsApp history and manual retry path.

Recommendation: Retain post-commit dispatch with a SQLite uniqueness claim: it reuses the existing transport and manual retry path without placing send failures inside the close transaction. Consider a durable job only if recovery from a process stop before the claim becomes a requirement.

Files changed (48) +4101 / -358

Enhancement (9) +1188 / -81
page.tsxAdd daily-sales WhatsApp action to the dashboard +57/-0

Add daily-sales WhatsApp action to the dashboard

• Shows the permission-gated export-menu action and reuses the recipient modal for the selected day or range, with distinct send and share-window feedback.

frontend/src/app/(dashboard)/dashboard/page.tsx

page.tsxConfigure and display WhatsApp report activity +125/-48

Configure and display WhatsApp report activity

• Adds owner-only Z-rule recipient controls, localized report kinds and errors, and permission-aware retry controls for failed reports.

frontend/src/app/(dashboard)/whatsapp/page.tsx

whatsapp-share.tsShare daily sales through transport or a manual window +65/-10

Share daily sales through transport or a manual window

• Calls the new report endpoint and opens a prefilled wa.me window only for a disconnected-session fallback; returns distinct sent and opened outcomes.

frontend/src/lib/whatsapp-share.ts

cash-closures.tsDispatch Z reports after committed day closes +9/-0

Dispatch Z reports after committed day closes

• Starts non-blocking WhatsApp dispatch after responding to a successful store-level close; dispatch errors cannot roll back the close.

main/routes/cash-closures.ts

reports.tsAdd the daily-sales WhatsApp endpoint +88/-0

Add the daily-sales WhatsApp endpoint

• Requires report-view, daily-sales export, and WhatsApp permissions; validates the recipient and export range, formats the canonical dataset, and returns send or disconnected-fallback outcomes.

main/routes/reports.ts

whatsapp.tsProtect report messages and owner Z configuration +113/-5

Protect report messages and owner Z configuration

• Gates report-kind sends, history, and retries by report permissions, and validates owner-only changes to the day-close rule and recipient.

main/routes/whatsapp.ts

daily-sales-export.tsFormat bounded daily-sales WhatsApp summaries +247/-0

Format bounded daily-sales WhatsApp summaries

• Formats localized financial totals, payment mix, and top items from the export dataset, shortening optional detail to meet the transport limit.

main/services/daily-sales-export.ts

whatsapp.tsSend and track closure-linked Z reports +450/-13

Send and track closure-linked Z reports

• Formats stored Z snapshots, claims one automatic message per closure, and records failures for manual retry. Also recognizes both report kinds in transport safeguards and permission-filtered history.

main/services/whatsapp.ts

whatsapp-automation.tsDefine the optional day-close automation rule +34/-5

Define the optional day-close automation rule

• Adds a disabled-by-default Z trigger with explicit recipient and tightens trigger normalization to recognized strings.

shared/whatsapp-automation.ts

Tests (4) +1965 / -100
cash-close-whatsapp-recipient.spec.tsExercise sharing and owner-recipient workflows +207/-4

Exercise sharing and owner-recipient workflows

• Tests confirmed sends versus manual-share fallback, and verifies that the Z rule starts disabled without inferring the business phone and saves a normalized recipient.

frontend/e2e/cash-close-whatsapp-recipient.spec.ts

reports-whatsapp.test.tsCover report delivery and failure paths +1610/-96

Cover report delivery and failure paths

• Expands integration tests for ranges, localization, formatting limits, permissions, transport fallback, Z idempotency, failure and ACK handling, retry, and Offline-mode suppression.

tests/reports-whatsapp.test.ts

upgrade-path.test.tsVerify v122 migration preserves WhatsApp history +133/-0

Verify v122 migration preserves WhatsApp history

• Checks preservation of pre-migration messages and attempts, the widened message kind, and post-upgrade schema health.

tests/upgrade-path.test.ts

whatsapp-schema.test.tsAssert new report message schema +15/-0

Assert new report message schema

• Checks the closure-reference column, daily-sales message kind, and unique automatic-Z closure index.

tests/whatsapp-schema.test.ts

Documentation (7) +68 / -41
authentication-and-authorization.mdDocument the owner-only automation policy +4/-2

Document the owner-only automation policy

• Adds the day-close Z recipient rule to the documented direct role-based context policies.

docs/architecture/authentication-and-authorization.md

cloud-integrations.mdDescribe report delivery and WhatsApp safeguards +28/-10

Describe report delivery and WhatsApp safeguards

• Documents automatic Z dispatch, closure idempotency, report access, Offline-mode suppression, retry behavior, and the distinction between report and campaign limits.

docs/architecture/cloud-integrations.md

internationalization.mdDocument backend report localization +10/-8

Document backend report localization

• Clarifies the supported language catalogues and explains how generated labels serve backend WhatsApp report formatting.

docs/architecture/internationalization.md

regional-settings.mdList daily-sales WhatsApp as a regional-format consumer +1/-1

List daily-sales WhatsApp as a regional-format consumer

• Adds the plain-text daily-sales formatter to the regional snapshot consumer documentation.

docs/architecture/regional-settings.md

api.mdSpecify WhatsApp report API contracts +7/-6

Specify WhatsApp report API contracts

• Documents the daily-sales send endpoint, post-close dispatch, automation configuration restrictions, and report-kind history and retry permissions.

docs/reference/api.md

daily-sales-export.mdExtend the daily-sales contract to plain-text sharing +15/-11

Extend the daily-sales contract to plain-text sharing

• Describes the WhatsApp summary’s dataset, range, contents, character limit, permissions, and test coverage.

docs/reference/daily-sales-export.md

roles-and-permissions.mdClarify documented role-context exceptions +3/-3

Clarify documented role-context exceptions

• Links direct role checks to the authorization policy documentation rather than listing an incomplete set of exceptions.

docs/reference/roles-and-permissions.md

Other (28) +880 / -136
ar.jsonLocalize WhatsApp report controls in Arabic +13/-5

Localize WhatsApp report controls in Arabic

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/ar.json

bn.jsonLocalize WhatsApp report controls in Bengali +13/-5

Localize WhatsApp report controls in Bengali

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/bn.json

de.jsonLocalize WhatsApp report controls in German +13/-5

Localize WhatsApp report controls in German

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/de.json

en.jsonAdd canonical English WhatsApp report messages +13/-5

Add canonical English WhatsApp report messages

• Defines daily-sales report, Z-rule, recipient, message-kind, and validation strings used by the other catalogues.

frontend/src/lib/i18n/messages/en.json

es.jsonLocalize WhatsApp report controls in Spanish +13/-5

Localize WhatsApp report controls in Spanish

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/es.json

fa.jsonLocalize WhatsApp report controls in Persian +13/-5

Localize WhatsApp report controls in Persian

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/fa.json

fil.jsonLocalize WhatsApp report controls in Filipino +13/-5

Localize WhatsApp report controls in Filipino

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/fil.json

fr.jsonLocalize WhatsApp report controls in French +13/-5

Localize WhatsApp report controls in French

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/fr.json

hi.jsonLocalize WhatsApp report controls in Hindi +13/-5

Localize WhatsApp report controls in Hindi

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/hi.json

id.jsonLocalize WhatsApp report controls in Indonesian +13/-5

Localize WhatsApp report controls in Indonesian

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/id.json

it.jsonLocalize WhatsApp report controls in Italian +13/-5

Localize WhatsApp report controls in Italian

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/it.json

ja.jsonLocalize WhatsApp report controls in Japanese +13/-5

Localize WhatsApp report controls in Japanese

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/ja.json

ko.jsonLocalize WhatsApp report controls in Korean +13/-5

Localize WhatsApp report controls in Korean

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/ko.json

ne.jsonLocalize WhatsApp report controls in Nepali +13/-5

Localize WhatsApp report controls in Nepali

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/ne.json

nl.jsonLocalize WhatsApp report controls in Dutch +13/-5

Localize WhatsApp report controls in Dutch

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/nl.json

pt.jsonLocalize WhatsApp report controls in Portuguese +13/-5

Localize WhatsApp report controls in Portuguese

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/pt.json

ru.jsonLocalize WhatsApp report controls in Russian +13/-5

Localize WhatsApp report controls in Russian

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/ru.json

sq.jsonLocalize WhatsApp report controls in Albanian +13/-5

Localize WhatsApp report controls in Albanian

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/sq.json

th.jsonLocalize WhatsApp report controls in Thai +13/-5

Localize WhatsApp report controls in Thai

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/th.json

tr.jsonLocalize WhatsApp report controls in Turkish +13/-5

Localize WhatsApp report controls in Turkish

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/tr.json

ur.jsonLocalize WhatsApp report controls in Urdu +13/-5

Localize WhatsApp report controls in Urdu

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/ur.json

vi.jsonLocalize WhatsApp report controls in Vietnamese +13/-5

Localize WhatsApp report controls in Vietnamese

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/vi.json

zh-tw.jsonLocalize WhatsApp report controls in Traditional Chinese +13/-5

Localize WhatsApp report controls in Traditional Chinese

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/zh-tw.json

zh.jsonLocalize WhatsApp report controls in Simplified Chinese +13/-5

Localize WhatsApp report controls in Simplified Chinese

• Adds daily-sales report, Z-rule, recipient, message-kind, and validation text.

frontend/src/lib/i18n/messages/zh.json

db.tsMigrate WhatsApp messages for report dispatch +103/-0

Migrate WhatsApp messages for report dispatch

• Migration v122 adds the daily-sales message kind and closure reference, preserves existing messages and attempts, and uniquely indexes automatic Z attempts by closure.

main/db.ts

print-labels.generated.tsGenerate multilingual backend report labels +384/-0

Generate multilingual backend report labels

• Includes dashboard sales, payment, reconciliation, and report-title labels for backend formatting across supported languages.

main/print/print-labels.generated.ts

generate-print-labels.cjsInclude report labels in generated translations +65/-16

Include report labels in generated translations

• Adds borrowed dashboard keys needed by backend WhatsApp report formatters; also reformats the generator.

scripts/generate-print-labels.cjs

concepts.tsDeclare backend report-label concepts +16/-0

Declare backend report-label concepts

• Adds typed concept IDs for daily-sales, payment, and drawer-reconciliation labels shared with generated backend translations.

shared/print/concepts.ts

@qodo-code-review

qodo-code-review Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. An acknowledged report can become retryable ✓ Resolved
Description
isAlreadyDeliveredOrSent checks retryMessageId but not the new preclaimedMessageId, so it
cannot recognize an acknowledged automatic report. If shutdown or an error occurs after the
acknowledgement but before the send operation finishes, the failure path can overwrite sent with
failed, allowing a manual retry of a report WhatsApp already accepted.
Code

main/services/whatsapp.ts[R2428-2429]

+  } else if (req.preclaimedMessageId != null) {
+    messageId = req.preclaimedMessageId;
Relevance

●●● Strong

Recent accepted WhatsApp precedent addresses ACK races, retryability, and persisted send-state
protection.

PR-#1384

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
An acknowledgement advances the persisted row to sent, while the guard returns false for a
preclaimed send because its retry ID is null. The shutdown and exception paths can then mark that
row failed, which the retry path accepts.

main/services/whatsapp.ts[4069-4079]
main/services/whatsapp.ts[1350-1365]
main/services/whatsapp.ts[2451-2474]
main/services/whatsapp.ts[2019-2027]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The automatic report uses a preclaimed message ID, but the existing acknowledgement guard only recognizes retry IDs. Shutdown and error handling can consequently turn an acknowledged report into a retryable failure.

## Fix Focus Areas
- main/services/whatsapp.ts[2428-2429]
- main/services/whatsapp.ts[2451-2474]

## Recommended Fix
Have the acknowledgement guard inspect either retryMessageId or preclaimedMessageId, and prevent failure handling from lowering a message already at sent, delivered, or read status. Cover acknowledgement followed by shutdown and by a send-path error.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Owners without report access can enable Z sends ✓ Resolved
Description
The PUT /automation guard for the z_report_day_close rule checks only role !== 'owner' and
never calls hasPermission(userId, 'reports.view'). An owner whose reports.view is denied but who
has whatsapp.manage can enable the rule, so the next day close sends a Z report through
triggerDayCloseZReport with no per-user report permission check.
Code

main/routes/whatsapp.ts[R318-325]

+      if ((nextEnabled !== prevEnabled || normalizedPhone !== prevPhone) && role !== 'owner') {
+        res.status(403).json({
+          error: 'Only the store owner can configure automatic Z-report delivery',
+          reason: 'owner_required',
+          code: 'PERMISSION_DENIED',
+        });
+        return;
+      }
Relevance

●●● Strong

Finding directly conflicts with the stated requirement that report sends require reports.view
permission.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Manual send and retry check reports.view for report kinds. The automation endpoint changes who can
cause a z_report to be sent, but its guard only checks the owner role, so the permission check can
be bypassed.

Enforce report-message permissions
main/routes/whatsapp.ts[318-325]
main/routes/whatsapp.ts[156-163]
main/services/whatsapp.ts[4013-4017]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `PUT /api/whatsapp/automation` handler lets any owner enable or change the `z_report_day_close` rule without checking `reports.view`. A user denied `reports.view` can therefore cause Z reports to be sent.

## Fix Focus Areas
- main/routes/whatsapp.ts[285-351]

## Recommended Fix
Before accepting a change to the enabled flag or recipient of the Z rule, require `hasPermission(userId, 'reports.view')`. Return 403 `PERMISSION_DENIED` with `required: 'reports.view'` when it is missing. Optionally also check `reports.view` for the configuring user in `triggerDayCloseZReport`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Long bill receipts can no longer be sent ✓ Resolved
Description
The new guard in sendMessageInternal rejects any body longer than MAX_BODY_CHARS (4096) with
content_blocked, and it now applies to every send kind, not just report messages. Bill receipts
list every item with no cap, and /whatsapp/send has no length limit, so receipts for large orders,
long manual replies and retries of older long messages, all of which sent before, now fail with a
generic "failed" toast.
Code

main/services/whatsapp.ts[R2199-2206]

+  if (typeof req.body !== 'string' || !req.body.trim() || req.body.length > MAX_BODY_CHARS) {
+    return {
+      ok: false,
+      ...retryIdPart,
+      error: `Message must be a non-empty string of at most ${MAX_BODY_CHARS} characters.`,
+      reason: 'content_blocked',
+    };
+  }
Relevance

●●● Strong

Accepted precedent flags incompatible 4096-character policies that reject previously valid WhatsApp
reports.

PR-#940

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Before this PR, the 4096 limit only lived in the cash-close report route (reports.ts:1763). This PR
moves it into the shared transport function used by all sends. The bill receipt body lists every
non-cancelled item with no truncation and goes through /whatsapp/send, which only checks that the
body is non-empty. The frontend maps content_blocked to a generic failure toast.

frontend/src/lib/whatsapp-share.ts[296-333]
frontend/src/lib/whatsapp-share.ts[384-390]
frontend/src/lib/whatsapp-share.ts[392-425]
main/routes/whatsapp.ts[135-153]
main/routes/reports.ts[1763-1768]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`sendMessageInternal` now rejects every outbound body over 4096 characters. Bill receipts (unbounded item list), manual replies and retries of older long messages used to send fine and now fail with `content_blocked`.

## Fix Focus Areas
- main/services/whatsapp.ts[2199-2206]

## Recommended Fix
Apply the `MAX_BODY_CHARS` length check only when `isReportMessageKind(req.kind)` is true; for other kinds keep only the non-empty check. The report formatters and the cash-close route already enforce 4096 for reports. If a global cap is wanted, use WhatsApp's real text limit (65536), or truncate bill receipts on the client before sending, and add a test that a bill receipt longer than 4096 characters still sends.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. A closed day can have no send attempt ✓ Resolved
Description
cash-closures commits and responds before triggerDayCloseZReport creates its whatsapp_messages
claim. If the process stops between those steps, the closure remains complete but has no attempt in
history for startup recovery or manual retry.
Code

main/routes/cash-closures.ts[R1444-1445]

+    try {
+      void whatsapp.triggerDayCloseZReport(result.id).catch((err) => {
Relevance

●●● Strong

Finding directly conflicts with the stated requirement to persist one automatic attempt per closure.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The route commits and sends HTTP 201 before invoking the trigger; the unique message row is inserted
only inside that later trigger. The unique index prevents duplicate claims but cannot recover a
missing claim.

main/routes/cash-closures.ts[1439-1447]
main/services/whatsapp.ts[4035-4049]
main/db.ts[7912-7914]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A process interruption after the day-close commit but before the asynchronous trigger claims a message leaves a closed day with no durable automatic-send attempt.

## Fix Focus Areas
- main/routes/cash-closures.ts[1439-1447]
- main/services/whatsapp.ts[4035-4049]

## Recommended Fix
Persist the eligible closure's unique report-send claim as part of the close transaction, before returning the response. Dispatch only after commit, retaining Offline-mode suppression and the rule that send failures never roll back the close.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
5. Owners see sent before acknowledgement ✓ Resolved
Description
shareDailySalesViaWhatsApp treats the endpoint's success response as a sent report, although
sendMessageInternal returns successfully after socket submission without waiting for the server
acknowledgement that marks the row sent. If that acknowledgement never arrives, the dashboard has
already shown a success toast while the message can later fail its acknowledgement timeout.
Code

frontend/src/lib/whatsapp-share.ts[R201-203]

+    if (data?.success === true) {
+      popup?.close();
+      return 'sent';
Relevance

●●● Strong

Accepted precedent treats reporting successful submission as distinct from confirmed completion.

PR-#647

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new endpoint returns success from the sender's successful socket submission, and the frontend
immediately maps it to sent. The service explicitly waits for a later status-2 event to mark the
stored message sent and sweeps messages that remain unacknowledged.

main/routes/reports.ts[1837-1858]
frontend/src/lib/whatsapp-share.ts[196-210]
frontend/src/app/(dashboard)/dashboard/page.tsx[590-595]
main/services/whatsapp.ts[2533-2557]
main/services/whatsapp.ts[950-978]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new daily-sales flow displays a sent confirmation on socket submission, before the server acknowledgement that establishes the message's sent status.

## Fix Focus Areas
- frontend/src/lib/whatsapp-share.ts[201-203]
- frontend/src/app/(dashboard)/dashboard/page.tsx[590-595]
- main/routes/reports.ts[1850-1858]

## Recommended Fix
Return or display a distinct submitted/pending outcome until the message reaches its acknowledged sent status. Show the confirmed-send toast only after acknowledgement, while preserving a separate outcome for an opened manual-share window.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. Report dates ignore the store's calendar ✓ Resolved
Description
formatStoredZReportWhatsAppMessage inserts closure.business_date as a raw ISO string, and
formatDailySalesWhatsAppMessage inserts summary.from_date and summary.to_date raw as well.
Only the Z-report period times go through Intl.DateTimeFormat with the store's calendar and digit
preferences. Stores set to Persian calendar or locale digits therefore get Gregorian Latin-digit
dates next to localized times and amounts in the same message.
Code

main/services/whatsapp.ts[R3888-3890]

+      `📊 *${zTitle} #${fmtNum(closure.z_number)} - ${storeDisplay}*`,
+      `📅 *${dateLabel}:* ${closure.business_date} (${period})`,
+      `👤 *${closedByLabel}:* ${truncateWhatsAppText(String(closedByName || ''), closedByCap)}`,
Relevance

●●● Strong

The PR explicitly promises store calendar and digit preferences, making raw ISO dates inconsistent
with its intent.

PR-#783

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The period times are formatted with numberingSystem and calendar options, but the business date on
the same line is printed raw. The daily-sales period line also substitutes the raw ISO dates into
the label.

main/services/whatsapp.ts[3788-3814]
main/services/whatsapp.ts[3868-3890]
main/services/daily-sales-export.ts[2370-2375]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Business dates in the WhatsApp Z-report and daily-sales messages are printed as raw ISO strings, which ignores the store's calendar and digit preferences that the times and amounts in the same messages follow.

## Fix Focus Areas
- main/services/whatsapp.ts[3888-3890]
- main/services/daily-sales-export.ts[2370-2375]

## Recommended Fix
Add a helper that formats a YYYY-MM-DD business date with `Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeZone: 'UTC', numberingSystem, calendar })`, using the same preference mapping as `formatZReportDateTime`, and use it for `closure.business_date`, `summary.from_date` and `summary.to_date`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Linked PRs:
  🌐 PR #39
  🌐 PR #2
  🌐 PR #99
⚠️ Tickets: not configured — No jira, linear integration is connected for the linked tickets
✅ Compliance rules (platform): 5 rules
✅ Cross-repo context — repo relationships
  Explored: repo: FreeOpenSourcePOS/RevFlo (sha: 37d47cb2) — View relationship
Review mode: Auto: 🧠 Deep: Large cross-cutting feature spans authorization, automation, reporting, migrations, and UI.

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread main/routes/whatsapp.ts Resolved
Comment thread main/routes/cash-closures.ts Resolved
Comment thread main/services/whatsapp.ts Resolved
Comment thread frontend/src/lib/whatsapp-share.ts Resolved
Comment thread main/services/whatsapp.ts Resolved
Comment thread main/services/whatsapp.ts Resolved
khaira777 added a commit that referenced this pull request Oct 29, 2026
: 1. **Core Test Suite (Shard 2/4) - `test:security` (`tests/security-hardening.test.ts:484`)** - **Invariant**: Canary secret suffixes asserted absent across serialized settings responses must use characters disjoint from randomly generated hex, base64url, and timestamp values present in those responses so substring leak assertions are deterministic. - **Cause**: `syntheticPairingCode = 'SYNTH-PAIR-9482'` gave `syntheticPairingSuffix = '9482'`, a 4-character hex/decimal string that collided (~1 in 1,700 runs) with random hex/base64url values in `GET /api/settings` (`cloud_pos_hash`, `jwt_secret` tail, `cloud_device_secret` tail) even while `mobile_pairing_code` itself was masked to `'****'`. - **Fix**: Updated `syntheticPairingCode` in `tests/security-hardening.test.ts` to `'SYNTH-PAIR-ZXQ!'` so `syntheticPairingSuffix` (`'ZXQ!'`) cannot collide with hex, base64url, timestamps, or seeded settings while still failing if `mobile_pairing_code` ever falls back to `****<last4>` masking. 2. **Core Test Suite (Shard 4/4) - `test:backup` (`tests/backup-restore-production.test.ts:974`) and Playwright E2E (Shard 1/4) (`frontend/e2e/database-export-import.spec.ts:289`)** - **Invariant**: Every test and same-version JSON import fixture that targets the live database schema version must match the latest schema version (`122`) registered in `MIGRATIONS` in `main/db.ts`. - **Cause**: Migration `v122` (`add_whatsapp_daily_sales_and_day_close_dispatch`) bumped the live schema version from `121` to `122`. `tests/backup-restore-production.test.ts` still asserted `121` after interrupted-restore recovery, and `frontend/e2e/database-export-import.spec.ts` still supplied `schema_version: '121'`, causing `DatabaseSettingsTab` and `POST /api/db/import` to treat the merge import as a cross-version schema mismatch requiring a Master PIN. - **Fix**: Updated `tests/backup-restore-production.test.ts` lines 976-977 and `frontend/e2e/database-export-import.spec.ts` line 214 from `121` to `122`. 3. **End-to-End Playwright Tests and Required Checks Gate** - **Cause**: Downstream aggregator gates failed solely because `Playwright E2E (Shard 1/4)` failed. **Verification run and passed**: - `npm run test:security` - `npm run test:backup` - `npm run test:schema-health && npm run test:migration-registry && npm run test:upgrade-path` - `npm run test:cash-closures && npm run test:whatsapp-schema && npm run test:whatsapp-service && npm run test:daily-sales-export && npm run test:reports-export && npm run test:rtl-kds-server-whatsapp && npm run test:expenses && npm run test:local-backup && npm run test:recovery-cloud` - `npm run build && npm run build:frontend` - `cd frontend && CI=true npx playwright test --config=playwright.config.ts --project=chromium --retries=0 e2e/database-export-import.spec.ts e2e/cash-close-whatsapp-recipient.spec.ts` (5/5 passed) - `cd frontend && CI=true npx playwright test --config=playwright.config.ts --project=chromium --retries=0 --shard=1/4 --grep='@ci-tier1|@ci-tier2' --grep-invert='FINDING:|a paid order offers a refund, and issuing it settles against the recorded sale'` (22/22 passed) - `npm run lint && npm run lint:budget && npm run i18n:check && npm run docs:check && npm run test:script-coverage` - `node scripts/ci/check-changed-format.cjs --base 1d04ad3 && git diff --check`
@khaira777 khaira777 changed the title feat(reports): send daily sales and auto Z report via WhatsApp feat(reports): WhatsApp sales and auto Z report Oct 29, 2026
khaira777 added a commit that referenced this pull request Oct 29, 2026
: 1. **Core Test Suite (Shard 2/4) - `test:security` (`tests/security-hardening.test.ts:484`)** - **Invariant**: Canary secret suffixes asserted absent across serialized settings responses must use characters disjoint from randomly generated hex, base64url, and timestamp values present in those responses so substring leak assertions are deterministic. - **Cause**: `syntheticPairingCode = 'SYNTH-PAIR-9482'` gave `syntheticPairingSuffix = '9482'`, a 4-character hex/decimal string that collided (~1 in 1,700 runs) with random hex/base64url values in `GET /api/settings` (`cloud_pos_hash`, `jwt_secret` tail, `cloud_device_secret` tail) even while `mobile_pairing_code` itself was masked to `'****'`. - **Fix**: Updated `syntheticPairingCode` in `tests/security-hardening.test.ts` to `'SYNTH-PAIR-ZXQ!'` so `syntheticPairingSuffix` (`'ZXQ!'`) cannot collide with hex, base64url, timestamps, or seeded settings while still failing if `mobile_pairing_code` ever falls back to `****<last4>` masking. 2. **Core Test Suite (Shard 4/4) - `test:backup` (`tests/backup-restore-production.test.ts:974`) and Playwright E2E (Shard 1/4) (`frontend/e2e/database-export-import.spec.ts:289`)** - **Invariant**: Every test and same-version JSON import fixture that targets the live database schema version must match the latest schema version (`122`) registered in `MIGRATIONS` in `main/db.ts`. - **Cause**: Migration `v122` (`add_whatsapp_daily_sales_and_day_close_dispatch`) bumped the live schema version from `121` to `122`. `tests/backup-restore-production.test.ts` still asserted `121` after interrupted-restore recovery, and `frontend/e2e/database-export-import.spec.ts` still supplied `schema_version: '121'`, causing `DatabaseSettingsTab` and `POST /api/db/import` to treat the merge import as a cross-version schema mismatch requiring a Master PIN. - **Fix**: Updated `tests/backup-restore-production.test.ts` lines 976-977 and `frontend/e2e/database-export-import.spec.ts` line 214 from `121` to `122`. 3. **End-to-End Playwright Tests and Required Checks Gate** - **Cause**: Downstream aggregator gates failed solely because `Playwright E2E (Shard 1/4)` failed. **Verification run and passed**: - `npm run test:security` - `npm run test:backup` - `npm run test:schema-health && npm run test:migration-registry && npm run test:upgrade-path` - `npm run test:cash-closures && npm run test:whatsapp-schema && npm run test:whatsapp-service && npm run test:daily-sales-export && npm run test:reports-export && npm run test:rtl-kds-server-whatsapp && npm run test:expenses && npm run test:local-backup && npm run test:recovery-cloud` - `npm run build && npm run build:frontend` - `cd frontend && CI=true npx playwright test --config=playwright.config.ts --project=chromium --retries=0 e2e/database-export-import.spec.ts e2e/cash-close-whatsapp-recipient.spec.ts` (5/5 passed) - `cd frontend && CI=true npx playwright test --config=playwright.config.ts --project=chromium --retries=0 --shard=1/4 --grep='@ci-tier1|@ci-tier2' --grep-invert='FINDING:|a paid order offers a refund, and issuing it settles against the recorded sale'` (22/22 passed) - `npm run lint && npm run lint:budget && npm run i18n:check && npm run docs:check && npm run test:script-coverage` - `node scripts/ci/check-changed-format.cjs --base 1d04ad3 && git diff --check`
@khaira777
khaira777 force-pushed the fm/flocafe-1336-whatsapp-reports branch from 81beaf4 to 6e667fa Compare October 29, 2026 19:48
@khaira777 khaira777 changed the title feat(reports): WhatsApp sales and auto Z report feat(reports): send daily sales via WhatsApp and auto-send Z on day close Oct 29, 2026
@khaira777 khaira777 changed the title feat(reports): send daily sales via WhatsApp and auto-send Z on day close feat(reports): WhatsApp sales and auto Z report Oct 29, 2026
Add Daily Sales WhatsApp summary delivery via POST /api/reports/daily-sales/whatsapp and the dashboard Export sales menu, formatted in the store language and regional currency/number conventions with 93-day range enforcement and progressive truncation that preserves financial totals.

Add optional owner-configured z_report_day_close automation rule with E.164 recipient validation, per-closure idempotency in migration v122, Offline mode suppression, non-blocking error handling without auto-retry on disconnect or ack timeout, and report-kind access gating across WhatsApp history and manual retry.
: 1. **Core Test Suite (Shard 2/4) - `test:security` (`tests/security-hardening.test.ts:484`)** - **Invariant**: Canary secret suffixes asserted absent across serialized settings responses must use characters disjoint from randomly generated hex, base64url, and timestamp values present in those responses so substring leak assertions are deterministic. - **Cause**: `syntheticPairingCode = 'SYNTH-PAIR-9482'` gave `syntheticPairingSuffix = '9482'`, a 4-character hex/decimal string that collided (~1 in 1,700 runs) with random hex/base64url values in `GET /api/settings` (`cloud_pos_hash`, `jwt_secret` tail, `cloud_device_secret` tail) even while `mobile_pairing_code` itself was masked to `'****'`. - **Fix**: Updated `syntheticPairingCode` in `tests/security-hardening.test.ts` to `'SYNTH-PAIR-ZXQ!'` so `syntheticPairingSuffix` (`'ZXQ!'`) cannot collide with hex, base64url, timestamps, or seeded settings while still failing if `mobile_pairing_code` ever falls back to `****<last4>` masking. 2. **Core Test Suite (Shard 4/4) - `test:backup` (`tests/backup-restore-production.test.ts:974`) and Playwright E2E (Shard 1/4) (`frontend/e2e/database-export-import.spec.ts:289`)** - **Invariant**: Every test and same-version JSON import fixture that targets the live database schema version must match the latest schema version (`122`) registered in `MIGRATIONS` in `main/db.ts`. - **Cause**: Migration `v122` (`add_whatsapp_daily_sales_and_day_close_dispatch`) bumped the live schema version from `121` to `122`. `tests/backup-restore-production.test.ts` still asserted `121` after interrupted-restore recovery, and `frontend/e2e/database-export-import.spec.ts` still supplied `schema_version: '121'`, causing `DatabaseSettingsTab` and `POST /api/db/import` to treat the merge import as a cross-version schema mismatch requiring a Master PIN. - **Fix**: Updated `tests/backup-restore-production.test.ts` lines 976-977 and `frontend/e2e/database-export-import.spec.ts` line 214 from `121` to `122`. 3. **End-to-End Playwright Tests and Required Checks Gate** - **Cause**: Downstream aggregator gates failed solely because `Playwright E2E (Shard 1/4)` failed. **Verification run and passed**: - `npm run test:security` - `npm run test:backup` - `npm run test:schema-health && npm run test:migration-registry && npm run test:upgrade-path` - `npm run test:cash-closures && npm run test:whatsapp-schema && npm run test:whatsapp-service && npm run test:daily-sales-export && npm run test:reports-export && npm run test:rtl-kds-server-whatsapp && npm run test:expenses && npm run test:local-backup && npm run test:recovery-cloud` - `npm run build && npm run build:frontend` - `cd frontend && CI=true npx playwright test --config=playwright.config.ts --project=chromium --retries=0 e2e/database-export-import.spec.ts e2e/cash-close-whatsapp-recipient.spec.ts` (5/5 passed) - `cd frontend && CI=true npx playwright test --config=playwright.config.ts --project=chromium --retries=0 --shard=1/4 --grep='@ci-tier1|@ci-tier2' --grep-invert='FINDING:|a paid order offers a refund, and issuing it settles against the recorded sale'` (22/22 passed) - `npm run lint && npm run lint:budget && npm run i18n:check && npm run docs:check && npm run test:script-coverage` - `node scripts/ci/check-changed-format.cjs --base 1d04ad3 && git diff --check`
- Q1: Preserve sent/delivered status against post-ACK errors and shutdown
- Q2: Check reports.view permission when configuring z_report_day_close rule
- Q3: Restrict 4096-char length cap to report message kinds
- Q4: Atomically claim queued Z-report attempt inside day close transaction
- Q5: Track deliveryStatus (sent vs submitted) with message ACK waiter
- Q6: Format business dates in WhatsApp reports with calendar and digit preferences
@khaira777
khaira777 force-pushed the fm/flocafe-1336-whatsapp-reports branch from 6e667fa to a057a38 Compare October 29, 2026 20:04
@khaira777
khaira777 merged commit b27799e into main Oct 29, 2026
23 checks passed
@khaira777
khaira777 deleted the fm/flocafe-1336-whatsapp-reports branch October 29, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(reports): send daily sales via WhatsApp and optional auto-send of Z on day close

1 participant