# Customer favorites and dine-in history

## Behavior

Logged-in customers see inferred favorites first in each restaurant menu in both the portal and dine-in app. Original categories remain in place. Guests see the normal menu. An empty favorites list adds no category.

The starting rule, approved by the product owner:
- Ordered on at least **3 distinct completed occasions at the same restaurant**.
- Ordered on at least **2 of the last 5 completed occasions**, and at least 40% of that available window.
- Most recently ordered **within 180 days**.
- Several rows or quantities of an item during one occasion count once.

A completed occasion is a finalized valid dine-in receipt or a paid, completed online order. Bookings without completed orders, open sessions, unpaid orders, cancelled/voided dine-in quantities and invalidated receipts do not supply item occurrences. The latest-five denominator includes completed linked visits without personally attributed items. Ordering history is independent of payment attribution. Customer ordering identity is captured on session items; paying for another diner's items does not make them the payer's favorites.

Eligible items are ranked by recent occurrence count, lifetime occurrence count, most recent order, then item ID for stable ties. The menu shows up to 12 enabled items currently available to purchase in that menu, respecting item, subcategory and category time restrictions. The account favorites drawer supports persistent hide/show preferences for eligible items. Hiding an item does not erase its history.

Completed dine-in history shows the latest 50 visits, the table receipt, and a separate amount paid by this account through Stripe. Table total is never treated as personal spend. The active account-to-participant link still controls rejoining; historical participation survives device transfer and session cleanup.

## Persistence and API

The schema is in [step 44](step-44-add-customer-visit-history-and-favorites.sql), following the unique active participant link migration. **The user confirmed step 44 is already applied: preserve it unchanged, and put future schema changes in a new migration.**

- customerDineInVisits: durable customer/vendor/session membership and finalized receipt reference; no foreign key to the ephemeral session.
- sessionItems.customerUserId: permanent ordering account, separate from device/participant customerId. Explicit linking claims that participant's previously unattributed items without replacing existing attribution.
- receiptItems.customerUserId: ownership copied before active items are deleted.
- customerFavoritePreferences: account/vendor/item hide/show preference.
- customerStripePayments: payer snapshot per vendor/PaymentIntent, including NULL anonymous owner, source app/order/session, currency, captured amount in integer minor units and capture timestamp. The refundedAmountMinor column from the applied migration is retained but **unused**.

Shared CustomerActivityHandler and CustomerFavoritePolicy serve both API namespaces:
- POST customer/favorites.php: account authentication required; returns vendors and menuLimit.
- POST customer/update-favorite.php: account authentication and customer CSRF; accepts vendorId, menuItemId and enabled; returns refreshed favorites.
- POST customer/order-history.php: includes completed dine-ins rows and stripeSpend entries grouped by vendorId and currency, with amountMinor.

Favorites are calculated from persisted history on demand. Frontend services clear data on logout/account change, ignore stale responses, and expose failures for retry. Menu presentation copies have independent option selection state and DOM IDs while retaining canonical item/category identity for the cart.

Finalization locks the session and commits receipt accounting, historical attribution and active-session deletion together. Its callers must finish their own transactions first. A failed history write rolls back receipt creation and cleanup. Repeated finalization after success returns session-not-found.

Session merges consolidate membership and payment source references, and reconcile duplicate active account links. Item splits and moves preserve ordering ownership. Finalization also preserves memberships for owners of moved items.

## Stripe spend and refund policy

The server snapshots the authenticated customer when creating an intent, never from order ownership, table membership, receipt total, device storage or request-supplied user IDs. Dine-in intent reuse requires the same participant and account owner. A previously anonymous intent is not claimed after login.

Only a successful Stripe callback records amount_received. Creating, failing or cancelling an intent earns nothing. Captures set the amount rather than incrementing it, so replayed events do not double customer spend. The ledger is scoped by vendor and currency.

**Stripe refunds are not implemented.** Customer issues are handled by the manager; any refund is cash. These manual cash refunds do not change recorded Stripe spend. There is no refund API, refund webhook handling, refund deduction or new Stripe event subscription in this implementation. This is a captured-spend basis for future loyalty calculation, not a net-spend/refund ledger. Points and reward issuance are outside this change.

The captured amount follows Stripe's [PaymentIntent reference](https://docs.stripe.com/api/payment_intents/object). Existing session-payment webhook processing remains separate from the idempotent customer spend record.

## Rollout and data limits

Step 44 is already applied; do not rerun or edit it. Deploy the corresponding API and both apps. This work does not change remote Stripe settings or deploy code.

Backfill is conservative: preserve currently open linked visits and their personal items. Existing account-owned completed online orders can qualify immediately. Completed dine-in visits whose participant links were already deleted, and old intents without a verified payer snapshot, are not reconstructed or credited.

Staging acceptance should cover a signed-in split payer, an anonymous payer, receipt finalization, and three qualifying completed occasions. Browser/staging verification is still required before production rollout.

## Local validation

Use only the dedicated resettable coconut_ai_api_testing database for the API harness. Focused coverage includes policy boundaries, distinct occasion counting, recent-window expiry, account isolation/CSRF, guest and signed-in ordering, payer isolation, exact captured spend, webhook callbacks, durable history, transaction rollback, session merging and item moves.

In each Angular app, run:
- node --test tests/customer-favorites.test.cjs
- node node_modules/@angular/compiler-cli/bundles/src/bin/ngc.js -p tsconfig.app.json --noEmit

The Node tests load production TypeScript classes and exercise menu order, item eligibility, independent options, stale account responses and preference failures. Angular compilation checks templates and types. These checks do not replace a browser/staging acceptance pass.

Verified locally:
- API regression selection: 83 tests, 782 assertions, all passed.
- Follow-up checks after removing refund handling: six existing payment/history tests passed; the two new portal payer tests passed with 14 assertions after correcting their fixture.
- Each Angular app: six frontend regression tests passed, including separate expansion of favorite and ordinary menu copies.
- Both Angular apps: template/type compilation passed.
- Changed PHP files: syntax checks passed. Step 44 matches its original applied contents.
- No deployment, remote Stripe configuration change or browser acceptance test was performed.
