# Auction Socket.IO Events (application clients)

**Public bidding contract:** Socket.IO only.
**Canonical domain service:** `placeBidAtomic` (never duplicated in the Socket handler).

HTTP Auction reads/creation/join/settlement are documented under the Swagger **Auctions** tag
(`docs/openapi/paths/auctions.yaml` → `public/api-docs/openapi.json`). Socket events in this
file are **not** Swagger HTTP paths.

Agora / Haraj live streaming is an **optional overlay** and is not required for core bidding.

---

## Connection authentication

Auction actions require a persisted API bearer token. Send it through Socket.IO
`auth.token` (preferred) or the `Authorization: Bearer <token>` handshake header.
Never put the bearer token in the URL/query string.

The existing connection query is still validated for compatibility, but it does
not authorize Auction access and must match the active profile resolved from the token:

| Field | Required | Notes |
|---|---|---|
| `userId` | yes | Current active profile Mongo ObjectId; must match bearer session |
| `userType` | yes | `client` for bidding |
| `lang` | yes | `ar` \| `en` |
| `deviceType` | yes | |
| `deviceId` | yes | |

Missing, revoked, expired, blocked, mismatched, or invalid Auction credentials →
`AUTH_REQUIRED`; no Auction room is joined and no Bid is persisted.

**Never trust from event payloads or unverified query claims:** `userId`, `bidderId`,
`customerId`, `providerId`, `sellerId`, `role`, `userType`.

---

## Rooms

Room name: `auction:<auctionId>` (24-hex ObjectId only).

### `auction:enter`

```json
{ "auctionId": "..." }
```

- Validates `auctionId`
- Joins `auction:<auctionId>`
- Ack returns authoritative snapshot (no `reservePrice`, wallet, admin notes)
- **Does not** create `AuctionSubscription` and **does not** grant bidding rights

Participation / deposit enrollment is HTTP only:

`POST /api/auctions/join` with multipart `auctionId` and `paymentMethod=wallet|online`
→ durable paid participation only after payment succeeds. Online requests stay
pending until verifiable gateway confirmation.

Bidding requires that durable PAID row inside `placeBidAtomic`.

```json
{
  "success": true,
  "data": {
    "auctionId": "...",
    "status": "current",
    "lifecycleStatus": "live",
    "currentPrice": 100,
    "minimumNextBid": 110,
    "endAt": "...",
    "serverNow": "...",
    "remainingSeconds": 120,
    "extensionCount": 0,
    "maxExtensions": 3,
    "bidsCount": 4,
    "depositStatus": "confirmed",
    "canBid": true
  }
}
```

Also emits `auction:entered` to the joining socket with the same snapshot.

`canBid` reflects durable `AuctionSubscription` (`PAID`) — not room membership.

### `auction:leave`

```json
{ "auctionId": "..." }
```

Leaves the room. **Does not** mutate Auction state.

Disconnect cleanup may clear live-viewer overlays only — never finalizes Auctions.

---

## Bidding — `auction:bid`

```json
{
  "auctionId": "...",
  "amount": 1500,
  "idempotencyKey": "client-generated-unique-key"
}
```

| Field | Rules |
|---|---|
| `auctionId` | required, Mongo ObjectId |
| `amount` | required, absolute money (≤ 2 decimals); `price` accepted as absolute alias |
| `idempotencyKey` | **required**, 1–128 chars `[A-Za-z0-9_:-]`; no control chars, dots, `$`, prototype names |

Handler calls:

```js
placeBidAtomic({
  auctionId,
  amount,
  idempotencyKey,
  clientId: socket.userId, // authenticated
  requireIdempotencyKey: true,
});
```

### Idempotency

Same authenticated customer + auction + amount + key → stable replay:

- no second Bid
- no `bidsCount` / `currentPrice` / `endAt` / `extensionCount` change
- no accepted Bid broadcast
- no outbid notifications again

Same key with different amount (or different bidder on same auction) → `IDEMPOTENCY_CONFLICT`.

Do **not** use `socket.id` as the key.
Retry the same user action with the **same** key; generate a new key only for a new amount/action.

### Acknowledgement

New Bid:

```json
{
  "success": true,
  "data": {
    "bid": { "id": "...", "amount": 1500, "createdAt": "..." },
    "auction": {
      "id": "...",
      "currentPrice": 1500,
      "minimumNextBid": 1510,
      "endAt": "...",
      "extensionCount": 1,
      "bidsCount": 5
    },
    "extended": true,
    "replayed": false
  }
}
```

Replay: `"replayed": true`, `"extended": false`.

Failure (ack and `auction:error` share the same `error` object):

```json
{
  "success": false,
  "error": {
    "code": "STALE_BID",
    "message": "...",
    "currentPrice": 1500,
    "minimumNextBid": 1510,
    "endAt": "...",
    "serverNow": "..."
  }
}
```

### Error codes

`AUTH_REQUIRED`, `FORBIDDEN`, `INVALID_AUCTION_ID`, `VALIDATION_ERROR`,
`IDEMPOTENCY_KEY_REQUIRED`, `IDEMPOTENCY_CONFLICT`,
`AUCTION_NOT_FOUND`, `AUCTION_NOT_STARTED`, `AUCTION_ENDED`, `AUCTION_CANCELLED`,
`OWN_AUCTION`, `DEPOSIT_REQUIRED`, `DEPOSIT_NOT_CONFIRMED`,
`BID_TOO_LOW`, `STALE_BID`, `INTERNAL_ERROR`

---

## Broadcasts (after new Bid commit only)

### Canonical: `auction:bid-updated`

```json
{
  "auctionId": "...",
  "currentPrice": 1500,
  "minimumNextBid": 1510,
  "highestBid": { "id": "...", "amount": 1500, "createdAt": "..." },
  "bidsCount": 5,
  "endAt": "...",
  "extensionCount": 1,
  "serverNow": "..."
}
```

Legacy aliases (compatibility only): `auction.bidAccepted`, `auction.priceUpdated`.

### Canonical: `auction:extended` (only when persisted extension occurred)

```json
{
  "auctionId": "...",
  "previousEndAt": "...",
  "endAt": "...",
  "extensionCount": 1,
  "maxExtensions": 3,
  "serverNow": "..."
}
```

Legacy alias: `auction.extended`.

Replays do **not** re-broadcast.

---

## Auction ending

Clients **cannot** finalize an Auction.

Flow:

```
cron/worker → finalizeAuction (CAS) → persist → emit auction:ended
```

Server broadcast (after commit; skipped on `alreadyFinalized`):

```json
{
  "auctionId": "...",
  "status": "finished",
  "settlementStatus": "awaiting_winner_payment",
  "finalPrice": 1500,
  "endedAt": "...",
  "serverNow": "..."
}
```

Client events `auction:ended` / `auction:sync-ended` are **observe-only**: they succeed only if the Auction is already `finished`/`cancelled`, leave the room, and never mutate settlement.

Core finalization still works if Socket.IO is unavailable.

---

## Server-authoritative time

Use `serverNow` + `endAt` from enter/ack/broadcasts.
Local UI timers are display-only. After reconnect: `auction:enter` again (and refetch HTTP details if needed).

---

## Legacy Haraj / seller / Agora

| Kind | Status |
|---|---|
| Live overlay (`start-live`, `enter-live`, comments) | Optional; must not place bids or settle |
| `seller` dual-read | Compatibility for older documents / live host checks |
| Agora tokens | Optional / currently partial — not a merge blocker |

Any legacy bidding event still required by a client must normalize and call `placeBidAtomic` — never a second bidding implementation.

---

## Client migration checklist

1. Connect with `auth: { token }` plus the compatibility handshake fields
2. `auction:enter`
3. Store authoritative snapshot
4. One `idempotencyKey` per user Bid action
5. `auction:bid`
6. Disable submit while ack pending
7. Retry same key on network retry
8. New key only for new amount
9. Listen `auction:bid-updated`
10. Listen `auction:extended`
11. Listen `auction:ended`
12. Re-enter after reconnect
13. Use server `endAt` / `serverNow`
14. Never compute the authoritative winner locally

Do **not** call `POST /place-bid` (removed).
