# Seat Transfer — Implementation & API Reference

## Overview

Allows an admin to reassign an existing paid registration from one user to another. The original payment history, registration status, and sequence numbers are preserved. Only the linked user changes. Room, pair, and group allocations are cleared automatically on transfer.

---

## Endpoints

### 1. Validate Target User

Pre-validates the target user before submitting the transfer. Checks the user exists and is not already registered for the same program.

```http
GET /registration/:id/seat-transfer/validate-user?phone=<phone>&email=<email>&countryCode=<countryCode>
```

**Auth:** Bearer token required. Roles: `admin`, `operational_manager`, `coordinator` (Shoba).

**Path param:**

| Param | Type   | Description                                        |
|-------|--------|----------------------------------------------------|
| `id`  | number | Source registration ID (the one being transferred) |

**Query params:**

| Param         | Type   | Required | Description                            |
|---------------|--------|----------|----------------------------------------|
| `phone`       | string | Yes      | Phone number (without country code)    |
| `email`       | string | Yes      | Email address of the target user       |
| `countryCode` | string | Yes      | Country code, e.g. `+91`              |

**Success `200`:**

```json
{
  "success": true,
  "data": {
    "user": {
      "id": 9271,
      "fullName": "Madhuri Karedla",
      "email": "madhuri@example.com",
      "phoneNumber": "9493791985"
    },
    "countryCode": "+91",
    "alreadyRegistered": false,
    "proxyAllowed": false,
    "existingRegistrationId": null,
    "answerFields": [
      { "questionId": 7468, "label": "Full Name", "type": "text", "answerLocation": "registration.fullName", "isEditable": true },
      { "questionId": 7472, "label": "Email", "type": "text", "answerLocation": "registration.emailAddress", "isEditable": true },
      { "questionId": 7480, "label": "Payment Reference", "type": "text", "answerLocation": "payment.reference", "isEditable": false }
    ]
  }
}
```

`answerFields` lists every question configured for the registration's program, with `isEditable: false` on any field the UI should render read-only in the transfer form:
- The question's `answerLocation` maps to a `payment`/`invoice` table (belongs to the original payer), or
- The question maps to a `registration.proForma*` column **and** the registration's payment is already completed (locked once paid).

This is computed server-side (`isAnswerFieldEditable()` in `registration-action.service.ts`) and **is also enforced** on submit — `PATCH :id/seat-transfer` rejects any answer targeting a non-editable field with `400 ST_BR_002`. The UI should still disable these fields so the rejection is never actually hit in normal use.

When `alreadyRegistered: true` and `proxyAllowed: true` — show a warning in the UI before proceeding:

```json
{
  "success": true,
  "data": {
    "user": { "id": 9271, "fullName": "Madhuri Karedla" },
    "countryCode": "+91",
    "alreadyRegistered": true,
    "proxyAllowed": true,
    "existingRegistrationId": 88
  }
}
```

**Error responses:**

| HTTP  | Code        | When                                                              |
|-------|-------------|-------------------------------------------------------------------|
| `404` | `RE_NF_001` | Source registration not found                                     |
| `404` | `U_NF_001`  | No user found with the given phone/email                          |
| `400` | `ST_BR_005` | Target user already registered; proxy registration not allowed    |

**Validation logic:**

```text
1. Load source registration by :id  →  programId + program.allowsProxyRegistration
2. Look up target user by phone + email + countryCode
   └── Not found → 404 U_NF_001
3. Check for existing active registration (excludes cancelled/archived):
   Target user has active reg for same program?
     ├── No  → { alreadyRegistered: false, proxyAllowed }
     └── Yes → proxyAllowed?
                 ├── true  → { alreadyRegistered: true, existingRegistrationId }  ← warn
                 └── false → 400 ST_BR_005  ← block
```

---

### 2. Transfer Seat

Executes the seat transfer.

```http
PATCH /registration/:id/seat-transfer
```

**Auth:** Bearer token required. Roles: `admin`, `operational_manager`, `coordinator` (Shoba).

**Path param:**

| Param | Type   | Description                  |
|-------|--------|------------------------------|
| `id`  | number | Registration ID to transfer  |

**Request body:**

```json
{
  "user": {
    "userId": 9271
  },
  "answers": [
    { "questionId": 7468, "answer": "Madhuri Karedla" },
    { "questionId": 7470, "answer": "2001-01-01" },
    { "questionId": 7471, "answer": "+919493791985" },
    { "questionId": 7472, "answer": "madhuri@example.com" }
  ],
  "transferReason": "Original registrant unable to attend",
  "adminNotes": "Approved by program coordinator"
}
```

**Request fields:**

| Field                  | Type   | Required | Description                                                              |
|------------------------|--------|----------|--------------------------------------------------------------------------|
| `user.userId`          | number | Yes      | ID of the user to transfer the seat to (obtained from validate step)     |
| `answers`              | array  | No       | Registration field overrides — a payment/invoice/locked-proforma answer rejects the whole request with `400 ST_BR_002` |
| `answers[].questionId` | number | Yes      | Question ID                                                              |
| `answers[].answer`     | any    | Yes      | String, number, boolean, or array depending on question type             |
| `transferReason`       | string | No       | Reason for the transfer                                                  |
| `adminNotes`           | string | No       | Internal admin notes                                                     |

**Success `200`:**

```json
{
  "success": true,
  "data": {
    "registration": {
      "id": 6839,
      "userId": 9271,
      "ownerUserId": 9271,
      "registrationMode": "self",
      "fullName": "Madhuri Karedla",
      "emailAddress": "madhuri@example.com",
      "registrationStatus": "pending"
    },
    "targetUser": { "id": 9271, "fullName": "Madhuri Karedla" }
  }
}
```

**Error responses:**

| HTTP  | Code        | When                                                          |
|-------|-------------|---------------------------------------------------------------|
| `400` | `ST_BR_006` | Actor's role is not permitted to transfer seats                |
| `404` | `RE_NF_001` | Registration not found                                        |
| `400` | `ST_BR_001` | Registration not eligible for transfer (see preconditions)    |
| `404` | `U_NF_001`  | Target user not found                                         |
| `400` | `ST_BR_002` | A submitted answer targets a payment/invoice/locked-proforma field |
| `400` | `ST_BR_003` | Failed to clear existing room/pair/group allocations           |
| `500` | `ST_IE_001` | Unexpected server error during transfer                       |

---

## Preconditions (ST_BR_001)

Transfer is rejected if **any** of the following is true:

| Condition                                                                             | Reason                                      |
|---------------------------------------------------------------------------------------|---------------------------------------------|
| Registration status is `waitlisted`                                                   | Waitlisted seats cannot be transferred      |
| `seatAllocated !== true`                                                              | Seat must be physically allocated           |
| No payment with status `online_completed` or `offline_completed`                     | Only fully paid registrations can be transferred (matches legacy `infini-hdb-be` behavior — `offline_pending` is intentionally excluded since the offline payment isn't confirmed yet) |

---

## What Happens on Transfer

Executed atomically in a single database transaction:

1. Registration preconditions validated
2. Target user looked up by `userId`
3. Answers validated — any payment/invoice/locked-proforma-mapped questions reject the whole request (`ST_BR_002`)
4. All seat allocations cleared via `allocationClearingService.clearRegistrationAllocations()`
5. `userId` and `ownerUserId` updated on the registration row (see below)
6. Provided `answers` applied — only registration-table fields updated
7. Transfer logged in `seat_transfer_track` with full audit trail (chain-transfer aware)
8. Final registration state returned

> No notifications are sent automatically. The admin is responsible for any follow-up communication.

---

## Registration Field Updates on Transfer

Two fields on `program_registration` are updated atomically:

| Field              | Before transfer      | After transfer              |
|--------------------|----------------------|-----------------------------|
| `userId`           | original participant | new participant             |
| `ownerUserId`      | original owner       | new participant (= `userId`)|

**Why `ownerUserId` changes:** `GET /user/:id/registrations` filters by `ownerUserId`. Without updating it, the transferred-to user would never see the registration in their list.

> **`registrationMode` is intentionally left unchanged for now** — see [Known Gaps](#known-gaps). The original design intent (below) was to always reset it to `self`, since after transfer the new user is both the beneficiary (`userId`) and the owner (`ownerUserId`). That reset is not yet implemented.

---

## Answers — What Gets Updated

Answers are matched to registration fields via each question's `answerLocation`.

| Answer type                                                           | Behaviour                                         |
|-----------------------------------------------------------------------|---------------------------------------------------|
| Maps to a registration-table field (fullName, dob, email, etc.)       | Updated via targeted SQL UPDATE                   |
| Maps to a payment or invoice table                                    | **Rejects the whole request** — `400 ST_BR_002`   |
| Maps to `registration.proForma*` and payment is completed             | **Rejects the whole request** — `400 ST_BR_002`   |
| Maps to `registration.proForma*` and payment is NOT completed         | Allowed                                           |
| Empty string (`""`)                                                   | Saved as `null`                                   |
| Non-string value (number, boolean, array)                             | Saved as-is                                       |

The check runs against **every** answer in the request before any write happens (`validateTransferAnswers()` in `registration-action.service.ts`, using the same `isAnswerFieldEditable()` rule that produces the `answerFields[].isEditable` flag from the validate-user endpoint). One locked field in the payload aborts the entire transfer — nothing is partially applied.

---

## Audit Trail — `seat_transfer_track`

Every transfer writes one row to `seat_transfer_track`. Chain transfers (A→B→C) are tracked via `sourceUserId`, which always points back to the original registrant.

| Column                  | Description                                                            |
|-------------------------|------------------------------------------------------------------------|
| `programRegistrationId` | The registration that was transferred                                  |
| `originalUserId`        | The user who held the seat before this transfer                        |
| `newUserId`             | The user who received the seat                                         |
| `sourceUserId`          | First-ever holder of the seat (preserved across chain transfers)       |
| `originalUserName`      | Snapshot of the outgoing user's name at transfer time                  |
| `newUserName`           | Snapshot of the incoming user's name at transfer time                  |
| `transferReason`        | Reason provided by admin                                               |
| `adminNotes`            | Internal admin notes                                                   |
| `transferStatus`        | Always `COMPLETED`                                                     |

---

## Typical UI Flow

```text
Admin opens seat transfer modal for registration #6839
  │
  ├─ [Step 1] Enter target user phone + email + country code
  │   └─ GET /registration/6839/seat-transfer/validate-user
  │          ?phone=9493791985&email=user@example.com&countryCode=%2B91
  │       ├─ 200 { alreadyRegistered: false }
  │       │     → Show user name + "Looks good, proceed"
  │       │
  │       ├─ 200 { alreadyRegistered: true, proxyAllowed: true, existingRegistrationId: 88 }
  │       │     → Warn: "This person already has registration #88 for this program.
  │       │              Proxy is allowed — confirm to proceed anyway."
  │       │
  │       ├─ 400 ST_BR_005 → "This person is already registered and duplicates are not allowed."
  │       └─ 404 U_NF_001  → "User not found"
  │
  └─ [Step 2] Confirm transfer (optionally edit registration field answers)
      └─ PATCH /registration/6839/seat-transfer
          { "user": { "userId": 9271 }, "answers": [...] }
          ├─ 200 → Success — close modal, refresh registration detail
          └─ 400 ST_BR_001 → "This registration is not eligible for transfer"
```

---

## Error Codes

| Code        | Description                                                          |
|-------------|----------------------------------------------------------------------|
| `ST_BR_001` | Registration not eligible — waitlisted, no seat, or invalid payment  |
| `ST_BR_002` | Answer targets a payment/invoice/locked-proforma field                |
| `ST_BR_003` | Failed to clear existing room/pair/group allocations                 |
| `ST_BR_005` | Target user already registered; proxy not allowed                    |
| `ST_BR_006` | Actor's role is not permitted to transfer seats                      |
| `ST_IE_001` | Unexpected server error during transfer                              |

Reuses existing codes: `U_BR_004` (email mismatch), `U_BR_005` (phone mismatch), `U_NF_001` (user not found), `RE_NF_001` (registration not found).

---

## Known Gaps

Tracked but not yet fixed:

| Gap | Detail |
|-----|--------|
| `registrationMode` not reset | `updateUserId()` only updates `userId` and `ownerUserId`. The original design intent (a `registrationMode = self` reset) described earlier in this doc is not implemented. |
| `PATCH :id/seat-transfer` doesn't re-check "already registered" | The "target already has an active registration on this program, and proxy isn't allowed" rule is enforced **only** in `GET :id/seat-transfer/validate-user` (`ST_BR_005`). `transferSeat()` does not repeat it. The two calls are unlinked — nothing stops a caller from skipping the GET step and hitting `PATCH` directly, which would create a duplicate active registration for a user on a program that disallows proxies. This was a deliberate call to not duplicate the check server-side; the admin UI (`infinipath-web-admin`) is the only enforcement point for this rule (its "Next" button stays disabled until the GET call succeeds). |

---

## Key Files

| File                                                    | Role                                                                        |
|---------------------------------------------------------|-----------------------------------------------------------------------------|
| `src/registration/registration.controller.ts`           | `GET :id/seat-transfer/validate-user`, `PATCH :id/seat-transfer` endpoints  |
| `src/registration/registration-action.service.ts`       | `transferSeat()`, `validateSeatTransferUser()`                              |
| `src/registration/registration.repository.ts`           | `updateUserId()` — updates `userId`, `ownerUserId`                         |
| `src/registration/dto/seat-transfer.dto.ts`             | `SeatTransferDto`, `SeatTransferUserDto`, `SeatTransferAnswerDto`           |
| `src/common/entities/seat-transfer-track.entity.ts`     | Audit trail entity                                                          |
| `src/common/enum/registration-mode.enum.ts`             | `RegistrationModeEnum.SELF / OTHER`                                         |
