# Guest Survey Phone Session Review

## Scope

This review covers the guest-facing survey prompt rendered as:

`Share quick feedback`

Example context line:

`Shawarma Sandwich · Order · Room 101`

The requested behavior is: the rating prompt must only appear for the phone/contact that created the request, order, or service through the current guest session.

## Current Entry Points

### Global mini banner

File: `resources/views/layouts/guest.blade.php`

The survey widget is mounted globally in the guest layout:

`@livewire('guest.guest-survey-widget')`

Because it is part of the layout, it can appear on any guest page that uses `layouts.guest`.

Component: `app/Http/Livewire/Guest/GuestSurveyWidget.php`

Current lookup:

- Reads current guest session from `GuestSessionService::getContext()`.
- Extracts `contact.id` and `room.unit_id`.
- Calls `SurveyTriggerService::latestPendingForGuest($contactId, $unitId)`.
- Hides the widget only if the same contact dismissed the same survey token within the last 24 hours.

Current visibility rule:

- Show the latest active survey if either the current `contact_id` matches or the current `unit_id` matches.

### Inline survey row

Component: `app/Http/Livewire/Guest/GuestSurveyRow.php`

Used from:

- `resources/views/livewire/guest/order-detail.blade.php`
- `resources/views/livewire/guest/order-history.blade.php`
- `resources/views/livewire/guest/request-status.blade.php`
- `resources/views/livewire/guest/ticket-history.blade.php`
- `resources/views/livewire/guest/ticket-status.blade.php`

Current lookup:

- Reads current `contact.id` and `room.unit_id`.
- Calls `SurveyTriggerService::findPendingForSource($sourceType, $sourceId, $contactId, $unitId)`.

Current visibility rule:

- Show an active survey for the specific source if either the current `contact_id` matches or the current `unit_id` matches.

### Direct survey page

Component: `app/Http/Livewire/Guest/GuestSurveyShow.php`

Current lookup:

- Loads the survey by token.
- Uses `HandlesGuestSurveySubmission::canAccessSurvey()`.

Current access rule:

- Allow access if either the current `contact_id` matches or the current `unit_id` matches.

## Survey Creation Cases

Survey instances are created by observers when a source reaches a completed status.

### Orders

Observer: `app/Observers/OrderSurveyObserver.php`

Service path: `SurveyTriggerService::normalizeOrder()`

Creates a survey when:

- The order status becomes `InboxStatus::Done`.
- Surveys are enabled.
- No survey already exists for the same order.
- A matching active survey template exists.
- The contact is not within the template/global cooldown window.

Stored identity:

- `contact_id` is copied from `orders.contact_id`.
- `unit_id` is copied from `orders.unit_id`.
- `source_context.guest_phone` is copied from `orders.guest_phone` or the contact mobile.

Current conflict:

- A different guest in the same room can see the order survey because lookup allows `unit_id` as an alternative to `contact_id`.

### Guest service requests

Observer: `app/Observers/ServiceRequestSurveyObserver.php`

Service path: `SurveyTriggerService::normalizeServiceRequest()`

Creates a survey when:

- The request status becomes `InboxStatus::Done`.
- The request has origin `guest_web` or `guest_qr`.
- `requested_by` points to an existing contact.
- Surveys are enabled.
- No survey already exists for the same request.
- A matching active survey template exists.
- The contact is not within the template/global cooldown window.

Stored identity:

- `contact_id` is copied from `service_requests.requested_by` only for guest-origin requests.
- `unit_id` is copied from `service_requests.unit_id`.
- `source_context.guest_phone` is copied from the request contact.

Current conflict:

- A different guest in the same room can see the request survey because lookup allows `unit_id` as an alternative to `contact_id`.

### Tickets

Observer: `app/Observers/TicketSurveyObserver.php`

Service path: `SurveyTriggerService::normalizeTicket()`

Creates a survey when:

- Ticket status becomes `TicketResolved` or `TicketClosed`.
- Surveys are enabled.
- No survey already exists for the same ticket.
- A matching active survey template exists.
- The contact is not within the template/global cooldown window.

Stored identity:

- For guest tickets, `contact_id` is copied from `tickets.requester_id`.
- For non-guest tickets, `contact_id` is copied from the first related contact.
- `unit_id` is copied from `tickets.unit_id`.
- `source_context.guest_phone` is copied from the selected contact.

Current conflict:

- Staff-created tickets or room-scoped tickets may intentionally rely on `unit_id`; enforcing phone/contact-only visibility will hide those surveys unless a valid contact is attached.

## Current Guest Session Identity

Service: `app/Services/GuestSessionService.php`

The session stores:

- `contact.id`
- `contact.phone`
- `contact.name`
- `room.unit_id`
- `room.unit_number`
- `zone_id`

The session can be restored from `guest_context_payload` cookie, so the survey check is not only the live Laravel session; it can also come from the persisted guest context cookie.

## Main Issue

The current survey access model is room-or-contact based:

`contact_id matches OR unit_id matches`

The requested behavior is contact/phone-owner based:

`contact_id/phone must match the guest that created the source`

This means the `unit_id` fallback is the source of the unwanted behavior. In the example `Shawarma Sandwich · Order · Room 101`, another guest with Room 101 in their session can see the survey even if they did not place that order.

## Proposed Rule

For guest-created orders, guest-created service requests, and guest-created tickets:

- Require a current session contact.
- Require the survey instance to have a `contact_id`.
- Show/open/submit the survey only when `guest_context.contact.id === guest_survey_instances.contact_id`.
- Optionally also compare normalized phone values against `source_context.guest_phone` as a defense-in-depth check, but `contact_id` should be the primary key.

## Code Areas To Change

Centralize the rule in `SurveyTriggerService` and `HandlesGuestSurveySubmission` so all guest surfaces behave the same.

Suggested changes:

- Update `SurveyTriggerService::latestPendingForGuest()` to stop using `orWhere('unit_id', $unitId)` for contact-owned surveys.
- Update `SurveyTriggerService::findPendingForSource()` the same way.
- Update `HandlesGuestSurveySubmission::canAccessSurvey()` to require matching `contact_id` for contact-owned surveys.
- Update `OrderHistory::pendingSurveySourceIds()` and `TicketHistory::pendingSurveySourceIds()` so completed items are not pulled back into history only because the room matches.

## Compatibility Decision Needed

There is one product decision before implementation:

Should room-only surveys still exist?

Option A: strict phone/contact-only

- Any survey without `contact_id` is hidden from guest UI.
- This exactly matches the requested behavior.
- Staff-created or room-only operational sources may no longer receive guest feedback unless they are linked to a contact.

Option B: strict for guest-created sources, room fallback only for staff-created sources

- Orders/service requests/tickets created from guest session require contact match.
- Staff-created sources can still use room fallback when no contact exists.
- This preserves old room-based behavior for operational cases, but it does not fully match “only the phone that made the request/order/service” for every source.

Recommended direction:

Use Option A for the global banner and direct submission access. If staff-created room surveys are still required, they should be treated as a separate product flow with explicit copy and eligibility rules, not mixed with phone-owned guest surveys.

## Implemented Decision

The implementation uses strict phone/contact ownership:

- Survey instances are not created unless the completed source resolves to a `contact_id`.
- The global widget only looks up active surveys for the current session `contact.id`.
- Inline survey rows only look up active surveys for the current session `contact.id`.
- Direct survey-token access only allows the matching `contact.id`.
- `unit_id` remains stored on the survey instance for room display and reporting, but it is no longer an access fallback.
