# Seat Transfer — Detailed Flow

## Overview

Seat transfer allows an admin to reassign a completed program registration from one person to another. The original registration record (and its payment history) is preserved; only the `userId` on `program_registration` is swapped to a newly-created user.

**No email or WhatsApp notification is sent** during seat transfer. History is recorded in `registration_status_history`, but communications are the caller's responsibility if needed.

---

## Entry Point

```
PUT /admin/seeker/{existingUserId}
Content-Type: application/json

{
  "is_seat_transfer": true,
  "user": {
    "fullName": "string (required)",
    "email":    "string (required)",
    "mobile":   "string (required)"
  },
  "address": {
    "state":   "",
    "country": "",
    "city":    ""
  },
  "userDetails":       { ... },
  "userTransportation": { ... },
  "transferReason": "optional – defaults to 'Admin initiated seat transfer'",
  "adminNotes":     "optional"
}
```

Handler: `src/lambda-functions/seeker/seekerService.ts` → `handleSeatTransfer(existingUserId, body)`

---

## Preconditions

| Check | Failure response |
|---|---|
| `body.user.fullName`, `email`, `mobile` all present | `400 Required user fields are missing` |
| `program_registration` row exists for `existingUserId` | `400 Program registration not found` |
| `registerStatus` is `Online Completed` **or** `Offline Completed` | `400 Seat transfer can only be done for completed registrations` |

---

## Step-by-step Flow

### Step 1 — Validate inputs
`fullName`, `email`, and `mobile` are required. Missing any returns `400`.

### Step 2 — Load existing registration
`getProgramRegistrationByUserId(existingUserId)` — fetches `program_registration` + joined `user`.  
`getSourceRegistrationByRegistrationId(programRegistrationId)` — fetches `source_registration` (needed later to preserve `sourceUserId`).

### Step 3 — Check status guard
`registerStatus` must be `Online Completed` or `Offline Completed`. Any other value aborts here.

### Step 4 — Clear room / pair / group

`checkPairAndRoom(programRegistrationId)` returns:

```ts
{
  roomAllotted: boolean,
  paired: boolean,
  pairedSeekerIds: number[],
  groupDetails?: { userGroupId, userGroupMapId },
  userPairId: number
}
```

Cleanup order (if applicable):

```
4a. roomAllotted === true
    → unAllocateRooms(userPairId)
    → failure → 400 "Failed to unallocate room before seat transfer"

4b. paired === true AND pairCount !== 1
    → unpairUsers(pairedSeekerIds)
    → null result → 400 "Failed to unpair user before seat transfer"

4c. groupDetails present
    → removeSeekersFromGroup(userGroupId, userGroupMapId)
```

> A solo pair (`pairCount === 1`) is left in place — no unpair needed.

### Step 5 — Create new User

```ts
new Users(
  '',          // password
  null, null, null,
  body.user.email,
  body.user.mobile,
  null, null,
  currentDate, currentDate,
  'ADMIN', 'ADMIN'   // createdBy, updatedBy
)
// fullName and termsAndConditions=true set after construction
```

Saved via `saveUser()` → returns `savedUser` with assigned `userId`.

### Step 6 — Create UserRelation

```ts
saveUserRelation({ userId: savedUser.userId, roleId: 1, ... })
```

### Step 7 — Create Address

```ts
createAddress({ state, country, city, userId: savedUser.userId })
```

Fields come from `body.address`; all default to `""` if absent.

### Step 8 — Create UserDetails + QR code

A UUID v4 (`userDetailsUUID`) is generated.  
A QR code is generated from `savedUser.toString()` (JPEG, error correction H, quality 0.3).  
The base64 data-URI prefix is stripped before storing.  
`saveSeekerDetails({ ...body.userDetails, userId, uuid: userDetailsUUID, qrCode })`.

### Step 9 — Generate JWT tokens

```ts
const program = await getProgramById(programId);
const isWaitingList = await areSeatsFilled(program.programId);   // reflects current seat availability

// expiry = seconds until program.registrationEndDate (omitted if date has passed)
const token = jwt.sign(
  { uuid: userDetailsUUID, isWaitingList },
  JWT_SECRET,
  expiresIn ? { expiresIn } : undefined
);

// Same token used for both fields
await updateSourceRegistrationTokens(programRegistrationId, {
  paymentToken: token,
  travelToken:  token,
});
```

### Step 10 — Transfer the registration

```ts
updateProgramRegistrationDetails(programRegistrationId, {
  userId:    savedUser.userId,
  updatedBy: 'ADMIN',
  updatedAt: currentDate,
});
```

This is the core swap — the existing `program_registration` row now points to the new user.

### Step 11 — Update transportation (conditional)

If `body.userTransportation` is non-empty, `updateUserWithDetails(savedUser.userId, body)` is called after normalizing:

| Field | Coercion |
|---|---|
| `transportationTypeId === 0` | → `null` |
| `returnTransportationTypeId === 0` | → `null` |
| `idType === 0` | → `null` |
| `travelSchedule === ""` | → `null` |

`body.invoice` and `body.payment` are deleted from the update payload before calling.

### Step 12 — Record in `seat_transfer_track`

```ts
saveSeatTransferTrack({
  programRegistrationId,
  originalUserId:   existingRegistration.user.userId,
  newUserId:        savedUser.userId,
  originalUserName: existingRegistration.user.fullName,
  newUserName:      savedUser.fullName,
  transferReason:   body.transferReason || 'Admin initiated seat transfer',
  adminNotes:       body.adminNotes || null,
  sourceUserId:     sourceRegistration?.sourceUserId || null,  // original registrant preserved
});
// transferStatus hardcoded to 'COMPLETED', createdBy/updatedBy = 'ADMIN'
```

`sourceUserId` preserves a chain across multiple transfers — it always points to whoever originally registered, even if the seat has been transferred more than once.

### Step 13 — Insert history record

```ts
createUserHistory({
  userId:               savedUser.userId,
  programRegistrationId,
  previousStatus:       existingRegistration.status,   // same value intentionally
  newStatus:            existingRegistration.status,   // no status change occurs
  createdBy:            'ADMIN',
  updatedBy:            'ADMIN',
});
```

> `previousStatus` and `newStatus` are set to the same value. The history row records **who now holds the seat** (via the new `userId`) rather than a status transition.

### Step 14 — Return response

```json
{
  "status": true,
  "message": "Seat transferred successfully. New user <name> (ID: <id>) assigned to registration <id>",
  "userData": { ... },
  "detailsData": { ... }
}
```

---

## Database Writes

| Table | Operation | Key columns set |
|---|---|---|
| `users` | INSERT | `email`, `mobile`, `fullName`, `createdBy = 'ADMIN'` |
| `user_relation` | INSERT | `userId`, `roleId = 1` |
| `address` | INSERT | `state`, `country`, `city`, `userId` |
| `user_details` | INSERT | `uuid`, `qrCode`, all body.userDetails fields |
| `source_registration` | UPDATE | `paymentToken`, `travelToken` (same JWT) |
| `program_registration` | UPDATE | `userId → newUser.userId` |
| `user_transportation` | UPDATE (if provided) | transportation fields for new user |
| `seat_transfer_track` | INSERT | full transfer record |
| `registration_status_history` | INSERT | `userId = newUser`, `previousStatus = newStatus` |

---

## `seat_transfer_track` Schema Reference

| Column | Type | Notes |
|---|---|---|
| `seat_transfer_track_id` | PK int | auto-increment |
| `program_registration_id` | int FK | the registration being transferred |
| `original_user_id` | int FK | previous seat holder |
| `new_user_id` | int FK | new seat holder |
| `source_user_id` | int nullable | original registrant (preserved across chains) |
| `original_user_name` | varchar 255 | snapshot at time of transfer |
| `new_user_name` | varchar 255 | snapshot at time of transfer |
| `transfer_reason` | text nullable | from request or default string |
| `admin_notes` | text nullable | from request |
| `transfer_status` | varchar 50 | always `"COMPLETED"` |
| `created_by` / `updated_by` | varchar 255 | always `"ADMIN"` |

---

## Communications

Seat transfer sends **no email or WhatsApp message**. If communications are required (e.g. confirmation to the new seat holder), they must be triggered separately by the caller or a downstream process.

This differs from Deny Seat and Release Seat, which both send ZeptoMail + WhatsApp automatically.

---

## Error Handling Summary

| Condition | HTTP status | Message |
|---|---|---|
| Missing `fullName`/`email`/`mobile` | 400 | `Required user fields are missing for seat transfer` |
| No registration found for userId | 400 | `Program registration not found` |
| Status not Completed | 400 | `Seat transfer can only be done for completed registrations` |
| Room unallocation fails | 400 | `Failed to unallocate room before seat transfer: <detail>` |
| Unpair fails | 400 | `Failed to unpair user before seat transfer` |
| Room/pair cleanup throws | 500 | `Failed to remove existing room allocation or pair before seat transfer` |
| `updateProgramRegistrationDetails` returns falsy | 500 | `Failed to update program registration` |
| Any uncaught exception | 500 | `An error occurred during seat transfer` |

---

## Related Files

| Purpose | File |
|---|---|
| Handler | [src/lambda-functions/seeker/seekerService.ts](../src/lambda-functions/seeker/seekerService.ts) |
| `seat_transfer_track` entity | [src/entity/seatTransferTrack.ts](../src/entity/seatTransferTrack.ts) |
| Program registration entity | [src/entity/programRegistration.ts](../src/entity/programRegistration.ts) |
| Room unallocation | [src/lambda-functions/room-allocation/roomAlloctionService.ts](../src/lambda-functions/room-allocation/roomAlloctionService.ts) |
| User pair service | [src/lambda-functions/user-pair/userPairService.ts](../src/lambda-functions/user-pair/userPairService.ts) |
| Status enums | [src/types/enum.ts](../src/types/enum.ts) |
| All seat actions overview | [docs/seat-actions.md](seat-actions.md) |
