
Added a request-accept flow for adding contacts via phone number search.
QR scanning remains instant and automatic (no change).
Phone number lookup now requires the other person to accept before the
contact is saved.

WHAT WAS BUILT


1. DATABASE
   Table: contact_requests
   Columns:
     - id                BIGINT UNSIGNED  PK AUTO_INCREMENT
     - sender_user_id    BIGINT UNSIGNED  FK → users.id
     - receiver_user_id  BIGINT UNSIGNED  FK → users.id
     - status            ENUM('pending','accepted','declined')  DEFAULT 'pending'
     - created_at        TIMESTAMP
     - updated_at        TIMESTAMP (auto-updates on change)
   Constraints:
     - UNIQUE on (sender_user_id, receiver_user_id) — no duplicate requests
     - CASCADE DELETE if either user is deleted
   Migration: already run on nsamhihp_tag_db ✓

2. NEW FILES
   app/Models/ContactRequest.php
     - create(senderId, receiverId)
     - findById(id)
     - findPendingBetween(senderId, receiverId)
     - listIncoming(receiverId)   — pending requests received
     - listSent(senderId)         — all requests sent + status
     - updateStatus(id, status)

   app/Controllers/ContactRequestController.php
     - send()      POST   /api/v1/contacts/request
     - incoming()  GET    /api/v1/contacts/requests
     - sent()      GET    /api/v1/contacts/requests/sent
     - accept()    PATCH  /api/v1/contacts/requests/{id}/accept
     - decline()   PATCH  /api/v1/contacts/requests/{id}/decline

3. MODIFIED FILES
   app/Models/User.php
     - Added: findByPhone(phone) — looks up active user by phone number

   app/routes/api.php
     - Added: use App\Controllers\ContactRequestController
     - Added: $contactRequests = new ContactRequestController()
     - Added: 5 new route blocks (see endpoints below)

   db.sql
     - Added: CREATE TABLE contact_requests definition (for fresh installs)


API ENDPOINTS
-------------

POST /api/v1/contacts/request
  Auth: required
  Body: { "phone": "0712345678" }
  What it does:
    - Finds user by phone number
    - Guards: can't send to self, already a contact, already pending
    - Creates pending request
    - Sends FCM push to receiver: "[tag_id] wants to add you as a contact"
  Response 201:
    { "request_id": 12 }
  Errors:
    404 — phone not found
    422 — self / already contact / already pending
    422 — validation (phone missing)

GET /api/v1/contacts/requests
  Auth: required (receiver's view)
  What it does: Returns all incoming PENDING requests for the logged-in user
  Response 200:
    [
      {
        "id": 12,
        "created_at": "...",
        "sender": {
          "user_id": 5,
          "full_name": "John Doe",
          "public_tag_id": "johndoe",
          "phone": "0712345678",
          "avatar_url": "https://..."
        }
      }
    ]

GET /api/v1/contacts/requests/sent
  Auth: required (sender's view)
  What it does: Returns all requests the logged-in user has sent (all statuses)
  Response 200:
    [
      {
        "id": 12,
        "status": "pending" | "accepted" | "declined",
        "created_at": "...",
        "updated_at": "...",
        "receiver": {
          "user_id": 7,
          "full_name": "Jane Smith",
          "public_tag_id": "janesmith",
          "phone": "0798765432",
          "avatar_url": "https://..."
        }
      }
    ]

PATCH /api/v1/contacts/requests/{id}/accept
  Auth: required (must be the receiver)
  What it does:
    - Adds receiver to sender's tag-book (sender gets receiver as contact)
    - Updates request status to 'accepted'
    - Sends FCM push to sender: "[tag_id] accepted your tag request"
    - Returns sender's full profile for the "save them back?" prompt
  Response 200:
    {
      "sender": {
        "user_id": 5,
        "full_name": "John Doe",
        "public_tag_id": "johndoe",
        "phone": "0712345678",
        "avatar_url": "https://..."
      }
    }
  Errors:
    403 — you are not the receiver
    404 — request not found
    422 — already accepted or declined

PATCH /api/v1/contacts/requests/{id}/decline
  Auth: required (must be the receiver)
  What it does:
    - Updates request status to 'declined'
    - Sends FCM push to sender: "[tag_id] declined your tag request"
  Response 200: { "message": "Tag request declined." }
  Errors:
    403 — you are not the receiver
    404 — request not found
    422 — already accepted or declined


PUSH NOTIFICATION TYPES
-----------------------
  type = "contact_request"           → receiver gets this when a request arrives
  type = "contact_request_accepted"  → sender gets this when accepted
  type = "contact_request_declined"  → sender gets this when declined

  All payloads include: { "request_id": "12" }


BUSINESS RULES
--------------
  - QR scan: still instant, no request needed
  - Phone form: always goes through request → accept flow
  - On accept: receiver is added to SENDER's tag-book automatically
  - On accept: receiver's app should prompt "Do you want to save [sender] too?"
    (the accept response returns the sender's profile for this purpose)
  - Saving back is optional — receiver taps yes → use POST /api/v1/contacts/scan-qr
    or a separate save endpoint (frontend decision)
  - A user can only have one pending request to the same person at a time
  - Declined requests: sender can try again (new row can be created after decline
    because the unique key only prevents duplicates in 'pending', not per-status)

  NOTE on re-requesting after decline: the current unique key is on
  (sender_user_id, receiver_user_id) regardless of status, so a declined
  request cannot be re-sent unless the old row is deleted. If re-requesting
  after decline is needed, the model will need a cleanup step.

================================================================================
  FRONTEND ENGINEER DIRECTIVE
================================================================================

You need to build UI for the following flows. All endpoints are authenticated
(Bearer JWT in Authorization header).

─────────────────────────────────────────────────────────────────────────────
FLOW 1 — SEND A TAG REQUEST (User A's side)
─────────────────────────────────────────────────────────────────────────────
Screen: "Add Contact" form (phone number input)

1. User types a phone number and taps "Send Tag Request"
2. Call: POST /api/v1/contacts/request  { "phone": "..." }
3. On 201 → show "Tag request sent! Waiting for them to accept."
4. On 404 → show "No user found with that number."
5. On 422 (already contact) → show "This person is already in your tag-book."
6. On 422 (already pending) → show "You already sent a request to this person."

Optional: show a "Sent Requests" section so User A can track status.
Call: GET /api/v1/contacts/requests/sent
Show each row with receiver name/avatar and a status badge:
  pending  → 🕐 Waiting
  accepted → ✓ Added
  declined → ✗ Declined

─────────────────────────────────────────────────────────────────────────────
FLOW 2 — RECEIVE & RESPOND (User B's side)
─────────────────────────────────────────────────────────────────────────────
Entry points:
  a) FCM push notification (type = "contact_request") → deep-link to requests screen
  b) A badge/tab in the Contacts screen showing pending count

Screen: "Tag Requests" list
Call: GET /api/v1/contacts/requests
Show each request with sender name, tag ID, avatar, and two buttons:
  [Accept]  [Decline]

On Accept:
  Call: PATCH /api/v1/contacts/requests/{id}/accept
  On 200 → the response includes the sender's profile.
  IMPORTANT: Show a bottom sheet / dialog:
    "Do you want to save [sender name] to your tag-book too?"
    [Save]  [Not now]
  If "Save" → call POST /api/v1/contacts/scan-qr is NOT applicable here
    since there's no QR. Use a direct add endpoint if one exists, or
    discuss with backend to add POST /api/v1/contacts/save { user_id: ... }.
    (Backend note: this endpoint does not exist yet — flag to backend engineer.)
  Remove the request card from the list either way.

On Decline:
  Call: PATCH /api/v1/contacts/requests/{id}/decline
  On 200 → remove the card from the list.

─────────────────────────────────────────────────────────────────────────────
FLOW 3 — PUSH NOTIFICATION HANDLING
─────────────────────────────────────────────────────────────────────────────
Handle these FCM data payloads:

  type = "contact_request"
    → Show notification: "[name] wants to add you as a contact"
    → On tap: navigate to GET /api/v1/contacts/requests screen

  type = "contact_request_accepted"
    → Show notification: "[name] accepted your tag request"
    → On tap: navigate to contacts list (they are now in your tag-book)

  type = "contact_request_declined"
    → Show notification: "[name] declined your tag request"
    → On tap: navigate to sent requests screen

─────────────────────────────────────────────────────────────────────────────
OUTSTANDING ITEM — FLAG TO BACKEND
─────────────────────────────────────────────────────────────────────────────
When User B accepts and wants to save User A back, there is currently no
direct "add by user_id" endpoint. The existing add flow only works via QR
token. Backend needs to add:

  POST /api/v1/contacts/save   { "user_id": 5 }

This would add the given user_id to the caller's tag-book directly (no QR,
no request needed — because consent was already given by accepting the request).

================================================================================
