# API: Send Travel Plan Email

## Endpoint

```
PUT /registration/:id/send-travel-plan-email
```

---

## Purpose

Allows an admin to manually trigger a travel plan notification email to a seeker. The email template and all merge/variable data are resolved dynamically from the database; no request body is needed.

This is used when:
- The seeker's travel plan has been updated by the operations team and they need to be re-notified.
- The automated trigger did not fire (e.g., template was not configured at the time of update).
- Admin wants to resend the travel plan link to the seeker on demand.

---

## Request

### Path Parameter

| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | `number` | Yes | Registration ID of the seeker |

### Headers

| Header | Value |
|---|---|
| `Authorization` | `Bearer <firebase-token>` |
| `Content-Type` | `application/json` |

### Body

None.

---

## Response

### 200 OK — Email sent successfully

```json
{
  "success": true,
  "data": null,
  "message": "Travel plan email sent successfully"
}
```

### 400 Bad Request — Registration not found

```json
{
  "success": false,
  "error": {
    "code": "RE_NF_001",
    "message": "Registration not found"
  }
}
```

### 400 Bad Request — Program not linked

```json
{
  "success": false,
  "error": {
    "code": "PROGRAM_NOT_FOUND",
    "message": "Program not found for this registration"
  }
}
```

### 400 Bad Request — Email address missing

```json
{
  "success": false,
  "error": {
    "code": "P_BR_011",
    "message": "Registration has no email address"
  }
}
```

### 400 Bad Request — Communication send failed

```json
{
  "success": false,
  "error": {
    "code": "COMMUNICATION_SEND_FAILED",
    "message": "..."
  }
}
```

---

## Auth & Access Control

| Guard | Behaviour |
|---|---|
| `CombinedAuthGuard` | Firebase token required |
| `RolesGuard` | Controller-level: all registered roles allowed (`admin`, `mahatria`, `relational_manager`, `operational_manger`, `rm_support`, `finance_manager`, `shoba`, `rm`, `viewer`) |

No additional `@Roles` decorator is applied at the method level — controller-level roles apply.

---

## Implementation

### Controller

**File:** [registration.controller.ts:1612](../src/registration/registration.controller.ts#L1612)

```typescript
@Put(':id/send-travel-plan-email')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Send travel plan email to seeker (admin-triggered, link auto-generated)' })
@ApiParam({ name: 'id', description: 'Registration ID', type: Number })
@ApiResponse({ status: HttpStatus.OK, description: 'Travel plan email sent successfully' })
async sendTravelPlanEmail(
  @Param('id', ParseIntPipe) id: number,
  @Res() res: Response,
) {
  try {
    await this.service.sendTravelPlanEmail(id);
    await this.responseService.success(res, 'Travel plan email sent successfully');
  } catch (error) {
    handleControllerError(res, error);
  }
}
```

---

### Service

**File:** [registration.service.ts:13043](../src/registration/registration.service.ts#L13043)

**Method:** `sendTravelPlanEmail(registrationId: number): Promise<void>`

**Flow:**

```
1. Fetch registration (with program, user, travelInfo, etc.) via repository.findRegistrationById()
   └─ Throws RE_NF_001 if not found
   └─ Throws PROGRAM_NOT_FOUND if registration.program is null
   └─ Throws P_BR_011 if registration.emailAddress is null/empty

2. Resolve email template via communicationMergeDataService.getTemplateWithMergeInfo()
   └─ programId            = registration.programId
   └─ templateAccessKey    = CommunicationTemplateAccessKeyEnum.TRAVEL_PLAN_CHANGE_EMAIL_SEEKER
   └─ communicationType    = CommunicationTypeEnum.EMAIL
   └─ registrationId       = the registration ID
   └─ Returns { templateKey, mergeInfo } or null
   └─ If null/no templateKey → logs warning and returns early (no error thrown)

3. Send email via communicationService.sendSingleEmail()
   └─ from:        program.emailSenderAddress (fallback: ZEPTO_EMAIL env)
   └─ from name:   program.emailSenderName (fallback: ZEPTO_EMAIL_NAME env)
   └─ to:          registration.emailAddress
   └─ to name:     registration.fullName
   └─ mergeInfo:   resolved from template merge field mappings
   └─ trackinfo:   { registrationId }

4. Logs success
   └─ On any error: logs error, calls handleKnownErrors(COMMUNICATION_SEND_FAILED, error)
```

---

### Template Resolution

**Service:** `CommunicationMergeDataService.getTemplateWithMergeInfo()`  
**File:** [communication-merge-data.service.ts:397](../src/communication/service/communication-merge-data.service.ts#L397)

Steps:
1. Looks up a `CommunicationTemplate` record by `(programId, templateAccessKey, communicationType)`.
2. Fetches all active `MergeFieldMap` records for that template.
3. Evaluates each merge field against the registration data.
4. Returns `{ templateKey, mergeInfo }` where `templateKey` is the external template identifier (Zepto Mail template key) and `mergeInfo` is the populated variable map.

**Template access key used:**

```
CommunicationTemplateAccessKeyEnum.TRAVEL_PLAN_CHANGE_EMAIL_SEEKER = 'TRAVEL_PLAN_CHANGE_EMAIL_SEEKER'
```

This key is in the **Travel related** group of `CommunicationTemplateAccessKeyEnum` alongside:
- `TRAVEL_PLAN_CHANGE`
- `TRAVEL_PLAN_NEW`
- `TRAVEL_PLAN_RETURN_CHANGE`
- `TRAVEL_PLAN_ONWARD_CHANGE`

**Template not found behaviour:** If no template is configured for this `(programId, accessKey, EMAIL)` combination, the method returns `null`. The service logs a warning and exits gracefully — no error is returned to the caller, the HTTP response is still `200 OK`.

---

### Repository — Registration Fetch

**File:** [registration.repository.ts:3654](../src/registration/registration.repository.ts#L3654)

`findRegistrationById(id)` loads the registration with all relations needed for merge data resolution:

- `program`, `program.type`
- `programSession`
- `invoiceDetails`
- `paymentDetails`, `paymentDetails.editRequests`
- `travelInfo`, `travelPlans`
- `user`, `user.programExperiences`, `user.profileExtension`
- `approvals`, `preferences`, `ratings`
- `allocatedProgram`, `allocatedSession`
- `swapsRequests` (with nested program relations)
- `rmContactUser`
- `goodies`
- `recommendation`
- `user.seekerDefaulter`

---

## Error Codes Reference

| Code | Constant | Condition |
|---|---|---|
| `RE_NF_001` | `REGISTRATION_NOT_FOUND` | Registration ID does not exist |
| `PROGRAM_NOT_FOUND` | `PROGRAM_NOT_FOUND` | `registration.program` is null |
| `P_BR_011` | `REQUIRED_FIELD_MISSING` | `registration.emailAddress` is null/empty |
| `COMMUNICATION_SEND_FAILED` | `COMMUNICATION_SEND_FAILED` | Zepto Mail send threw an error |

Error code definitions: [error-string-constants.ts](../src/common/constants/error-string-constants.ts)

---

## Behaviour Notes

- **Idempotent:** Calling the endpoint multiple times will send multiple emails. There is no deduplication or cooldown guard.
- **Silent template miss:** If no `TRAVEL_PLAN_CHANGE_EMAIL_SEEKER` template is configured for the program, the call succeeds silently (`200 OK`) but no email is sent. Check server logs for the warning: `Travel plan email template not found for registration {id}`.
- **No state mutation:** This endpoint only sends an email. It does not update registration status, travel plan status, or any audit fields.
- **Sender identity:** Falls back to `ZEPTO_EMAIL` / `ZEPTO_EMAIL_NAME` env vars if the program has no custom sender configured.
- **Sandbox mode:** If `EMAIL_USE_SANDBOX=true` and a sandbox template ID is set on the template, the sandbox template is used instead of the production one.
