# Chat Socket.IO Events

This is the realtime contract implemented by the current chat socket handlers.
Chat history and image upload remain HTTP operations under the Swagger **Chat**
tag. The events below are Socket.IO events, not OpenAPI HTTP paths.

Swagger UI now shows a **Test Chat Socket.IO** action in the top bar and an
interactive tester link inside the **Chat** tag. Both open:
[/api-docs/chat-socket-events.html#socket-tester](/api-docs/chat-socket-events.html#socket-tester).

Start the application and supply one Client `userId` plus one Provider `userId`.
The tester opens two isolated Socket.IO connections and guides the full cycle:
Client and Provider enter the same existing `chatId`, both
sides exchange messages, each view renders the server's
`chat:message-received` broadcast, and either side can emit `chat:exit`.

The tester intentionally exposes only the `client` and `provider` actor types.
It displays every emitted and received event without saving supplied identifiers
in browser storage.

## Connection

Connect to the same host as the API on fixed port `30123`. Socket.IO uses its
default path:

~~~js
const socketOrigin = new URL(baseOrigin);
socketOrigin.port = "30123";

const socket = io(socketOrigin.origin, {
  path: "/socket.io/",
  query: {
    userId: "665f1c2a9b4e1d0012ab34b2",
    lang: "ar",
    userType: "client",
    deviceType: "web",
    deviceId: "browser-session-id"
  }
});
~~~

### Required handshake query

| Field | Rules |
|---|---|
| userId | Required 24-character Mongo ObjectId |
| lang | Required; ar or en |
| userType | Required; admin, client, store, haraj, or provider |
| deviceType | Required; ios, android, or web |
| deviceId | Required non-empty device identifier |

The server validates these query values, attaches them to the socket, and joins
the socket to user:&lt;userId&gt;. Chat event payloads do not accept userId,
senderId, or userType as an authority source.

Current implementation note: the Socket.IO connection follows the existing
query-based identity contract. HTTP Swagger bearer schemes are not Socket.IO
handshake parameters. Chat membership is checked before entering an existing
chat, sending a message, or exiting a room.

An invalid handshake registers no domain handlers and currently emits
auction:error with this shape:

~~~json
{ "message": "Localized validation message" }
~~~

### Successful connection — connected

The connecting socket receives:

~~~json
{
  "userId": "665f1c2a9b4e1d0012ab34b2",
  "deviceId": "browser-session-id",
  "deviceType": "web"
}
~~~

## Room naming

Chat rooms are named chat:&lt;chatId&gt;. The chatId is a 24-character Mongo
ObjectId. A socket must enter the room before it sends a message.

## Client → server events

Chat events do not currently use Socket.IO acknowledgement callbacks. Success is
observed through the corresponding server broadcast. Failures are sent as
chat:error to the emitting socket.

### chat:enter

Enter an existing chat:

~~~json
{ "chatId": "665f1c2a9b4e1d0012ab34c1" }
~~~

Or find/create a direct 1:1 chat with another user:

~~~json
{ "receiverId": "665f1c2a9b4e1d0012ab34b1" }
~~~

Rules:

- Supply chatId or receiverId.
- When both are supplied, chatId wins.
- The selected identifier must be a valid Mongo ObjectId. When both fields are
  sent, receiverId is ignored and only chatId is validated.
- chatId only opens an existing chat and checks membership.
- receiverId finds the newest direct chat between both users or creates one.
- A user cannot create a direct chat with themself.
- Successful entry joins chat:&lt;resolvedChatId&gt; and broadcasts
  chat:participant-joined to the room, including the joining socket.
- For receiverId mode, read the newly resolved chatId from that broadcast.

chat:participant-joined payload:

~~~json
{
  "chatId": "665f1c2a9b4e1d0012ab34c1",
  "targetId": null,
  "type": null,
  "totalParticipants": 1,
  "user": {
    "id": "665f1c2a9b4e1d0012ab34b2",
    "name": "Example user",
    "userType": "client"
  }
}
~~~

targetId and type are populated for chats linked to an Auction,
Advertisement, or another supported business record.

### chat:message

~~~json
{
  "chatId": "665f1c2a9b4e1d0012ab34c1",
  "message": "Hello"
}
~~~

Rules:

- chatId is required and must be a Mongo ObjectId.
- The Socket server rejects messages containing decimal digits through `chat:error`
  before persistence or broadcast,
  including Western (`123`), Arabic-Indic (`١٢٣`), and Persian (`۱۲۳`) digits.
- The socket user must be a chat member.
- The socket must have completed chat:enter for this room.
- The current socket handler creates text messages only.
- The sender identity is taken from the socket, never from the event payload.

Successful messages are persisted and broadcast to the whole room as
chat:message-received using the standard ApiResponse envelope:

~~~json
{
  "key": "success",
  "message": "Success",
  "status": 200,
  "data": {
    "id": "665f1c2a9b4e1d0012ab34d1",
    "type": "text",
    "message": "Hello",
    "senderPath": "client",
    "senderId": "665f1c2a9b4e1d0012ab34b2",
    "senderName": "Example user",
    "senderAvatar": "https://example.test/assets/uploads/default-user.png",
    "date": "a few seconds ago",
    "chatId": "665f1c2a9b4e1d0012ab34c1",
    "direction": "left"
  }
}
~~~

Users who are not connected to the room may receive the existing push
notification for the new message. After the FCM phase completes, the server
emits `chat:notification-status` to the sending socket. The chat message is
persisted and broadcast before this notification work is awaited, so FCM does
not delay `chat:message-received`.

`chat:notification-status` uses the standard ApiResponse envelope:

~~~json
{
  "key": "success",
  "message": "Success",
  "status": 200,
  "data": {
    "chatId": "665f1c2a9b4e1d0012ab34c1",
    "messageId": "665f1c2a9b4e1d0012ab34d1",
    "status": "accepted",
    "recipients": 1,
    "attempted": 1,
    "accepted": 1,
    "failed": 0
  }
}
~~~

Status values are `accepted`, `partial`, `failed`, `skipped`, and
`not-required`. `accepted` means the FCM send request was accepted by the
server-side SDK; it is not proof that the operating system displayed it on the
device. The Swagger tester mirrors a notification only for `accepted` or
`partial` results where `accepted` is greater than zero.

To exercise this branch in the interactive tester, keep the receiving actor
outside the chat room while the sender emits the message. FCM delivery uses the
receiver's device token already persisted during sign-in. The Socket.IO
handshake `deviceId` is required for the connection but is not registered as a
new FCM token by the socket layer.

### chat:exit

~~~json
{ "chatId": "665f1c2a9b4e1d0012ab34c1" }
~~~

Rules:

- chatId is required and must be a Mongo ObjectId.
- The socket user must be a member and must currently be in the room.
- On success the server emits chat:participant-left to the other sockets, then
  removes the emitting socket from the room.
- There is no success acknowledgement or success event sent back only to the
  exiting socket.

chat:participant-left payload:

~~~json
{
  "chatId": "665f1c2a9b4e1d0012ab34c1",
  "totalParticipants": 1,
  "user": {
    "id": "665f1c2a9b4e1d0012ab34b2",
    "name": "Example user",
    "userType": "client"
  }
}
~~~

## Server → client events

| Event | Recipient | Payload |
|---|---|---|
| connected | Connecting socket | userId, deviceId, deviceType |
| chat:participant-joined | Every socket in the chat room | chatId, targetId, type, totalParticipants, user |
| chat:message-received | Every socket in the chat room | ApiResponse envelope containing the message DTO |
| chat:notification-status | Sending socket only | ApiResponse envelope with server-side FCM attempt/acceptance counts |
| chat:participant-left | Other sockets in the room after chat:exit | chatId, totalParticipants, user |
| chat:participants-updated | Remaining room sockets after disconnect cleanup | chatId, totalParticipants |
| chat:error | Socket that emitted the invalid/failed event | message |
| auction:error | Socket with an invalid initial handshake | message |

Disconnect cleanup emits:

~~~json
{
  "chatId": "665f1c2a9b4e1d0012ab34c1",
  "totalParticipants": 1
}
~~~

## Error contract

chat:error currently contains a localized message only:

~~~json
{ "message": "Join the chat room before sending messages." }
~~~

Possible failures include:

- Invalid or missing payload, chatId, receiverId, or message.
- Chat or receiver not found.
- Attempt to open a direct chat with the same user.
- The socket user is not a chat member.
- chat:message before chat:enter.
- A message longer than 4000 characters.
- A message containing any decimal digit.
- chat:exit when the socket is not in the room.
- Unexpected chat lookup or persistence failure.

There is currently no stable error code, status field, or acknowledgement
object for chat events. Clients should display the localized message and may
re-enter/refetch the chat after a connection loss.

## HTTP boundary

The Swagger **Chat** tag documents:

- GET /api/chats — chat list.
- GET /api/chat/messages — paginated history.
- POST /api/chat/upload — image file upload.

POST /api/chat/upload returns an uploaded file URL, but the current
chat:message event accepts text only. There is no implemented chat:image or
socket image-message event; clients must not invent one from this contract.

## Recommended client flow

1. Load GET /api/chats or identify a receiver.
2. Connect Socket.IO with all required handshake query values.
3. Wait for connected.
4. Register all server event listeners.
5. Emit chat:enter with chatId or receiverId.
6. Read the resolved chatId from chat:participant-joined.
7. Emit chat:message only after room entry.
8. Render the outgoing message immediately as pending, then confirm it from chat:message-received.
9. Treat chat:notification-status as an FCM server result, not a device delivery receipt.
10. Use GET /api/chat/messages for history and pagination.
11. Emit chat:exit when leaving the screen.
12. After reconnect, emit chat:enter again before sending.
