---
module: claims.settings
title: Settings
tab: Architect
order: 30
audience: dev
---

How the Claims **Settings** (vendor email templates) feature is built. Single-service, synchronous, DB-backed — **no Kafka, no executor/record-store pattern** in this feature. See [platform.service-ownership].

## Services involved

- **`hiveops-claims`** owns this feature end to end (backend image `hiveiq-claims:8092`, frontend `hiveiq-claims-frontend:5179`). No other service participates.
- No cross-service calls, no Kafka producer/consumer for templates. (The wider claims service *does* consume `hiveops.device.events` into `device_summary`, but that is unrelated to Settings — see [platform.kafka].)

## Data flow — reading the effective template

```
SettingsPage.svelte
  └─ EmailTemplatesSection.svelte  (onMount → emailTemplateAPI.effective())
       └─ GET api.bcos.cloud/claims/settings/email-templates/effective   [JWT]
            └─ EmailTemplateController.effective(ClaimsPrincipal)
                 └─ EmailTemplateRepository.findByInstitutionKeyAndIsDefaultTrue(institutionKey)
                      .or( findByInstitutionKeyAndIsDefaultTrue("__GLOBAL__") )
                      └─ 200 {name, subject, global}   or   204 No Content → UI fallback
```

The `global` boolean in the response is computed as `"__GLOBAL__".equals(template.institutionKey)` so the UI can label an inherited default.

## Data flow — using the template (send path)

The template is consumed at message-send time, not just displayed:

```
POST /api/claims/{claimUuid}/messages
  └─ ClaimActivityService.addMessage(...)  (persists claim_messages row)
       └─ EmailService.sendMessageNotification(claim, vendor, text, sender, additionalEmail)
            └─ EmailTemplateRepository.findByInstitutionKeyAndIsDefaultTrue(claim.institutionKey)
                 ├─ present → resolvePlaceholders(subject/bodyHtml)
                 └─ empty   → __GLOBAL__ default → else hardcoded fallback body
            └─ JavaMailSender.send(MimeMessage)   (skipped if mail bean not configured)
```

Placeholders substituted in subject + body (HTML-escaped): `{{claimNumber}}`, `{{terminalId}}`, `{{location}}`, `{{shortageAmount}}`, `{{claimStatus}}`, `{{vendorName}}`, `{{messageText}}`, `{{senderEmail}}`. Attachment-notification emails (`sendAttachmentNotification`) use a **hardcoded** body, not the template.

## Write path & the "single default" invariant

- `EmailTemplateService.create/update`: when the request sets `isDefault=true`, it first runs the `@Modifying` query `clearDefaultForInstitution(institutionKey)` (UPDATE ... SET is_default=false WHERE institution_key=:key), then saves the new/updated row as default. This keeps at most one default per institution, which `findByInstitutionKeyAndIsDefaultTrue` (returns `Optional`) relies on.
- `update`/`delete` enforce ownership: `institutionKey.equals(template.institutionKey)` or throw 403; missing `uuid` → 404.

## Tables read/written

| Table | R/W | By |
|-------|-----|----|
| `email_templates` (DB `hiveiq_claims`) | R | `effective`, `list`, send path |
| `email_templates` | W | `create`, `update`, `delete`, `clearDefaultForInstitution` |

Schema from `V2__email_templates.sql`: `id BIGSERIAL`, `uuid UUID UNIQUE`, `institution_key VARCHAR(255)` (indexed), `name`, `subject VARCHAR(500)`, `body_html TEXT`, `is_default BOOLEAN`, `created_at`, `updated_at`. The special `institution_key = '__GLOBAL__'` row is the shared inherited default. See [platform.data-architecture].

## API endpoints (all served by hiveops-claims)

| METHOD | Path | Auth | Notes |
|--------|------|------|-------|
| GET | `/api/claims/settings/email-templates/effective` | JWT | 200 `{name,subject,global}` or 204 |
| GET | `/api/claims/settings/email-templates` | JWT | list for caller's institution |
| POST | `/api/claims/settings/email-templates` | JWT | 201; body `{name,subject,bodyHtml,isDefault}` |
| PUT | `/api/claims/settings/email-templates/{uuid}` | JWT | ownership-checked |
| DELETE | `/api/claims/settings/email-templates/{uuid}` | JWT | 204; ownership-checked |
| GET/POST/PUT/DELETE | `/api/internal/email-templates` | `X-Internal-Secret` + `?institutionKey=` | `permitAll` at security layer, secret-gated in controller; used for `__GLOBAL__` / cross-institution |

## Security model

`SecurityConfig` permits `/actuator/health|info`, swagger, `/h2-console/**`, `/error`, `/api/internal/**`, then `anyRequest().authenticated()`. **No role-based guards** — isolation is by `institutionKey` from the JWT (`JwtAuthenticationFilter` → `ClaimsPrincipal`). The `/api/internal/**` path is publicly routable but every method rejects unless `X-Internal-Secret` matches the non-blank `internal.service-secret` (`INTERNAL_SERVICE_SECRET`).

Cross-links: [platform.data-architecture] · [platform.kafka] · [platform.service-ownership]
