# KADiCon Deep Product Analysis Report

**Date:** 2026-05-24  
**Repo:** `https://github.com/KADiCon-UG/kadicon.git`  
**Analyst:** Aria sub-agent (claude-sonnet-4-6)  
**Prior report:** `/root/aria/state/sub-agent-context/kadi-discovery/report.md` (BFF API mapping, reservation-focused)  
**This report:** Full product-level analysis — architecture, security, vulnerabilities, roadmap, KAR-612 feasibility

---

## 1. Architecture

### 1.1 Monorepo Layout

Nx monorepo with pnpm workspaces.

```
/
├── apps/
│   ├── backend/           NestJS API (primary backend)
│   ├── admin/web/         Angular 21 restaurant dashboard
│   ├── admin/mobile/      Ionic 8 + Capacitor (iOS + Android)
│   ├── admin-cli/         Rust TUI (ratatui) — internal ops CLI
│   ├── customer/          Angular customer kiosk app
│   ├── iot-display/       IoT menu/order display
│   ├── kds/               Kitchen Display System (Android + web)
│   ├── reservation-widget/ Angular custom element (embeddable widget)
│   └── reservation-widget-demo/
├── libs/
│   ├── backend/auth/      JWT strategy, guards, interceptors, SessionStore
│   ├── backend/domain/    TypeORM entities + enums
│   ├── backend/repository/ Tenant-scoped BaseEntityRepository
│   ├── backend/config/    env var types + validation
│   ├── backend/logging/   Pino + OpenTelemetry
│   ├── core/types/        Shared enums (ReservationState, etc.)
│   ├── core/utils/        Date utils, schedule utils, table sorting
│   ├── core/google-reservations/ Proto types + API types
│   ├── api-client/angular/ Generated HTTP client
│   └── plugins/           Nx generators (openapi, protobuf, typeorm)
└── infra/                 Terraform (AWS: ECS, RDS, S3, ECR, ElastiCache/Valkey)
```

### 1.2 Backend Tech Stack

| Layer | Technology | Version |
|-------|-----------|---------|
| Framework | NestJS on Fastify | 11.1.19 |
| ORM | TypeORM + PostgreSQL | 0.3.x |
| Queue | BullMQ + Redis/Valkey | 11.0.4 |
| Auth | JWT HS256 + argon2 passwords | — |
| WebSocket | Socket.io | 11.1.19 |
| Storage | AWS S3 (SDK v3) | — |
| Email | @nestjs-modules/mailer (MJML templates) | 2.3.4 |
| Push | Firebase Cloud Messaging | — |
| Logging | Pino + nestjs-pino | — |
| Tracing | OpenTelemetry | — |
| Billing | Stripe Connected Accounts | API 2026-03-25.dahlia |
| Cache | Keyv + Valkey | — |
| IaC | Terraform → AWS ECS Fargate | — |
| Monitoring | Datadog (ECS sidecar) | — |
| Dynamic config | AWS AppConfig (cron schedule overrides) | — |

### 1.3 Frontends

| App | Framework | Purpose |
|-----|-----------|---------|
| admin/web | Angular 21 + Angular Material + ngrx/component-store | Restaurant management dashboard |
| admin/mobile | Ionic 8 + Angular 21 + Capacitor | Mobile dashboard |
| customer | Angular 21 | Tablet kiosk / self-serve |
| reservation-widget | Angular Elements | Embeddable booking widget |
| kds | Angular 21 + Android native | Kitchen Display System |
| iot-display | Angular | Menu/order display boards |
| admin-cli | Rust (ratatui TUI) | Internal ops terminal tool |

### 1.4 Deployment

- AWS ECS Fargate — containerized backend
- AWS RDS PostgreSQL — primary DB (staging + production separate)
- AWS ElastiCache Valkey — queues and caching
- AWS S3 — public asset storage
- AWS ECR — Docker image registry
- AWS AppConfig — runtime cron overrides
- AWS ALB — HTTP + WebSocket targets

**Confirmed production endpoints from `apps/admin-cli/env_config.json`:**
- API: `https://api.kadicon.de/api`
- DB: `main-db-production.cqepwx0suhv3.eu-central-1.rds.amazonaws.com`

---

## 2. Reservation Logic (Full Domain)

### 2.1 State Machine

```
[Widget POST / Admin POST / Google CreateBooking]
        ↓
  canAutoApproveReservation()?
    YES → state: approved → customer confirmation email + .ics
    NO  → state: unconfirmed → restaurant email with Accept/Decline links

[Admin dashboard]:
  unconfirmed ──CONFIRM──> approved ──> seated ──> done
  unconfirmed/approved ──REJECT──> rejected
  approved ──UNCONFIRM──> unconfirmed

[Cron: reservation_reminder] every 10 min
  → approved reservations starting in T+24h to T+24h+10min window
  → sends reminder email, sets reminderSent=true

[Cron: reservation_seeder] daily at 03:00 UTC, PRODUCTION only
  → seeds fake reservations for Google-enabled tenants
  → EXCLUDES 4e22f973-36d5-4e8c-b8a3-c85a7b87cf10 (Durrani's tenant UUID)
```

**States (ReservationState enum):**

| State | Description |
|-------|-------------|
| `unconfirmed` | Default on widget create without auto-approve |
| `approved` | Confirmed (auto or manual); Google bookings land here |
| `rejected` | Declined by restaurant or Google CANCELED |
| `seated` | Customer physically seated |
| `done` | Completed visit |

**OpenReservationFilter:** `[unconfirmed, approved, seated]`  
**ApprovedOpenReservationFilter (capacity):** `[approved, seated]`

### 2.2 Auto-Approval Logic

Location: `apps/backend/src/app/features/reservation/shared/utils.service.ts`

```
canAutoApproveReservation():
  1. reservation.tables must exist
  2. settings.autoApproveReservations.enabled === true
  3. noOfPersons <= guestLimitPerReservation
  4. noOfPersons + currentGuestsInInterval <= guestLimitOverall
```

**BUG:** `reservation.model.ts` documents `0 means no limit` for both limit fields. But the check is `noOfPersons > limit` — when limit is 0, any `noOfPersons > 0` evaluates true, blocking all auto-approvals. Setting to 0 silently breaks auto-approve instead of enabling unlimited.

### 2.3 Table Assignment Algorithm

Location: `apps/backend/src/app/features/reservation/shared/table-assignment.service.ts`

1. Prefer hint `tableId` if capacity fits and no overlap
2. Prefer hint `tableCombinationId` if capacity fits
3. Auto-assign: smallest single table fitting party, not conflicting with `approved` reservations
4. Auto-assign via combinations: smallest combination fitting party
5. **Rebooking fallback**: if all combinations are taken, reassign their single-table reservations to other free tables and use the freed slots — this actively mutates other reservations in-place
6. Returns `{ tables: [] }` → `UnprocessableEntityException` "Selected Time Slot is not available"

**Notable:** Only `approved` state checked for overlaps. Commented-out TODO block (lines 250-282 in `reservation.service.ts`) addresses unconfirmed re-assignment after confirm — **not implemented**.

### 2.4 Timeslot Engine

Location: `apps/backend/src/app/features/reservation/services/timeslot.service.ts`

- Reads `Settings.schedule` (JSON) → `IScheduleSettings` → per-weekday rules
- If day is `closed` → returns `[]`
- Generates 30-min slots from `openingTime` to `closingTime` per rule
- Handles overnight shifts (closingTime < openingTime → add 1 day)
- Availability = `occupiedTables.length !== totalTableCount` (binary, ignores party size)
- Past slots (timezone-aware) → `available: false`

---

## 3. Full API Surface Inventory

All routes prefixed `/api`.

### 3.1 Customer Routes (X-TENANT-ID only, no JWT)

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/customer/settings/widget` | Widget config, schedule, defaultReservationTime |
| GET | `/api/customer/settings/app` | CustomerApp display settings |
| GET | `/api/customer/reservation/slots/:date` | Available 30-min slots |
| POST | `/api/customer/reservation` | Create reservation (widget source) |
| GET | `/api/customer/reservation/verify/:tableId/:query` | QR check-in name verification |
| GET | `/api/customer/menu/category` | Visible menu categories |
| GET | `/api/customer/menu/item` | Visible menu items |
| POST | `/api/customer/message` | Table message (waiter call, payment request) |
| POST | `/api/customer/order` | Create order |
| PUT | `/api/customer/order/:orderId/items` | Add items to order |
| PUT | `/api/customer/order/:orderId/pay` | Pay items (cash) |
| PATCH | `/api/customer/order/:orderId/checkout` | Checkout order |
| GET | `/api/customer/token/verify/:token` | Verify QR table token (no X-TENANT-ID needed) |
| POST | `/api/customer/payment/intent` | Create Stripe payment intent |
| GET | `/api/customer/reviews` | Aggregate reviews + Google review URL |

### 3.2 Admin Routes (JWT + X-TENANT-ID)

Auth: `POST /api/auth/login|verify|verify/token|password/reset|password/reset/new`, `PUT /api/auth/password`

Reservations: full CRUD + confirm/unconfirm/reject/search/slots/tables-free/review/customer-search

Orders: checkin, items, pay, checkout, cancel, reopen, items-state, item-course

Settings, Menu, Room Plan, Users, Dashboard, Messages, Billing, Reporting, Storage: standard CRUD under respective paths

**Tenant onboarding (public, no auth):**
- `POST /api/tenant` — creates new tenant (requires `verificationToken` in body)
- `GET /api/tenant/:id` — get tenant info (JWT)

### 3.3 Google Booking Server (HTTP Basic)

`GET /v3/HealthCheck`, `POST /v3/BatchAvailabilityLookup`, `POST /v3/CreateBooking`, `POST /v3/UpdateBooking`, `POST /v3/GetBookingStatus`

### 3.4 Misc Public

- `GET /api/health` — health check
- `GET /api` — Swagger UI
- `POST /api/billing/stripe/webhook` — Stripe webhook (no auth, signature verified)
- `GET /api/static/*` — static file serving

---

## 4. Auth Model

### 4.1 Customer / Widget

No JWT. `X-TENANT-ID` header → `TenantHeaderInterceptor` → `SessionStore.set({ tenantId })` via `AsyncLocalStorage`.

### 4.2 Admin JWT

1. `POST /api/auth/login` → argon2 verify → `jwtService.signAsync(SessionInfo)`
2. All admin requests: `Authorization: Bearer <JWT>` + `X-TENANT-ID`
3. `MultiTenancyInterceptor` validates JWT `tenantId === X-TENANT-ID` header
4. JWT expiry: **24h** (hardcoded, TODO: lower after refresh tokens)
5. No refresh token mechanism

**JWT payload (SessionInfo):**
```typescript
{
  id, firstName, lastName, email, emailVerified, tenantId, preferedLanguage?
}
```

**JWT secret:** `process.env.JWT_SECRET ?? '8a9b36731918dd0766ba2f7a68b4a7ecde90e89ee31f006934956c49e4169264'`

The hardcoded fallback IS the production secret (confirmed from `admin-cli/env_config.json`).

### 4.3 Google Booking Server

HTTP Basic auth. Credentials from env vars.

### 4.4 Tenant Registration

`POST /api/tenant` is public but requires a `verificationToken` JWT (1h expiry, email-verified) from `POST /api/auth/verify`.

---

## 5. Multi-Tenant Model

### 5.1 Mechanism

All entities extend `BaseEntity` which has `tenantId: string (UUID, nullable)`.

`BaseEntityRepository<T>` overrides every TypeORM method:
- **find/findOne/findOneBy**: appends `WHERE tenantId = SessionStore.tenantId`
- **save**: injects `entity.tenantId = SessionStore.tenantId`
- **update/delete/softDelete**: appends `tenantId` to criteria
- **createQueryBuilder**: throws `NotImplementedException` — intentionally blocked to prevent filter bypass

This is **ORM-level row-based isolation** in shared tables, not DB-level RLS.

### 5.2 Weak Points

- `BaseEntityRepository.query()` (raw SQL) does NOT inject tenantId — any raw query is a cross-tenant leak risk
- Google Booking Server manually sets `SessionStore.tenantId = request.merchant_id` from body — no interceptor validation; if credentials are compromised, any tenantId can be impersonated
- Cron jobs run without SessionStore context (intentional for multi-tenant iteration, but any future raw query in a cron risks all-tenant exposure)

### 5.3 Tenant Onboarding

1. `POST /api/auth/verify` → email with 1h JWT
2. `POST /api/tenant` → creates Company + User + credentials + seed data in one `@Transactional()` call
3. `TenantSeedService.seed()` creates default settings, tables, etc.

---

## 6. Booking Lifecycle

### 6.1 Creation Flow

1. `POST /api/customer/reservation` → validate startDate > now, endDate > startDate
2. `TableAssignmentService.assignTable()` → assigns tables
3. `canAutoApproveReservation()` → decides state
4. `ReservationRepository.save()` with tenantId
5. `notifyNewReservation()`: approved → customer email + .ics; unconfirmed → restaurant action email

### 6.2 Confirmation Email

Sent on first approval. Subject is "Reservation Updated - KADiCon" (cosmetic bug — should be "Confirmed").

Contains `.ics` with 30-min VALARM reminder. Also sends "Reservation Updated" email to restaurant owner.

### 6.3 Accept/Decline Links in Restaurant Email

Link pattern: `${DASHBOARD_URL}/reservations?id=${reservation.id}&action=accept`

These are NOT magic links — they require dashboard login (JWT). The `action` param is handled client-side in Angular.

### 6.4 Cancellation

**No customer-facing cancel endpoint exists.** Only `PATCH /api/reservation/:id/reject` (JWT, admin only). Customers must call the restaurant.

---

## 7. Security Posture

| Area | Status |
|------|--------|
| HTTPS | Enforced at ALB |
| Security headers | `@fastify/helmet` installed |
| CORS | `origin: '*', methods: '*'` — wide open |
| Rate limiting | None anywhere |
| Input validation | NestJS ValidationPipe globally |
| Auth on admin routes | JWT + tenant header verification |
| Password hashing | argon2 |
| SQL injection | Protected by TypeORM ORM layer |
| Stripe webhooks | Signature verification (pattern correct) |
| Swagger | Always enabled including production |

---

## 8. Vulnerabilities and Weaknesses

### 8.1 CRITICAL: Secrets in Git History

**Files:**
- `apps/backend/.env` — committed to git, contains:
  - AWS Access Key `AKIA5EVTXMXHHOZ5BCFJ` + secret
  - Stripe test API key + webhook secret
  - Firebase service account full private key
- `apps/admin-cli/env_config.json` — committed to git, contains:
  - **Production PostgreSQL password** `KYYtgLT20JR9tRWZmnpjqryfaUJjBwzH`
  - **Staging PostgreSQL password** `XzKuW73nFc4tjBC2`
  - Production RDS hostname
  - JWT secret for ALL environments: `8a9b36731918dd0766ba2f7a68b4a7ecde90e89ee31f006934956c49e4169264`

`.env` is NOT in `.gitignore`. `env_config.json` is NOT in `.gitignore`.

**Actions required immediately:**
1. Rotate ALL credentials (AWS, DB passwords, Stripe, Firebase, JWT secret)
2. Add both files to `.gitignore`
3. Rewrite git history (BFG Repo Cleaner or `git filter-repo`)

### 8.2 HIGH: No Rate Limiting on Customer Routes

- `POST /api/customer/reservation` — unlimited booking spam
- `POST /api/customer/message` — unlimited message flooding
- `POST /api/customer/payment/intent` — unlimited Stripe PI creation (cost risk)
- `POST /api/auth/login` — unlimited credential stuffing
- `POST /api/auth/password/reset` — unlimited reset email sending (cost + abuse)

**Fix:** `@nestjs/throttler` at application level.

### 8.3 HIGH: Hardcoded JWT Secret Fallback

`libs/backend/auth/src/lib/constants/jwt.constants.ts`:
```typescript
SECRET: process.env['JWT_SECRET'] ?? '8a9b36731918dd0766ba2f7a68b4a7ecde90e89ee31f006934956c49e4169264'
```

This fallback IS the production secret. Anyone who reads the code can forge admin JWTs for any tenantId.

**Fix:** Remove fallback, throw on startup if `JWT_SECRET` is unset.

### 8.4 HIGH: Unprotected Tenant Creation Endpoint

`POST /api/tenant` is fully public with no rate limiting. Requires a verification token but that token can be obtained freely via `POST /api/auth/verify`. Spam registration is possible.

### 8.5 MEDIUM: Dynamic Field Injection in Admin Search

`apps/backend/src/app/features/reservation/services/reservation.service.ts`, `searchCustomers()`:
```typescript
where: { customerData: { [field]: ILike(`%${query.toLowerCase()}%`) } }
```
`field` comes from the URL path `GET /api/reservation/customer/:field/:query` with no allowlist. TypeORM prevents SQL injection but an attacker can query arbitrary CustomerData columns.

**Fix:** Validate `field` against `['name', 'email', 'phoneNumber']`.

### 8.6 MEDIUM: Auto-Approve 0-Limit Bug

When `guestLimitPerReservation = 0` or `guestLimitOverall = 0` (documented as "unlimited"), the check `noOfPersons > 0` is always true → auto-approve silently disabled for all reservations.

**Fix:** `if (limit !== 0 && noOfPersons > limit) return false`

### 8.7 MEDIUM: No CSRF Protection

Wide-open CORS + no CSRF middleware. Not currently exploitable (JWT in Authorization header, no cookies), but a risk if cookies are added.

### 8.8 MEDIUM: QR Check-in Brute-forceable

`GET /api/customer/reservation/verify/:tableId/:query` — no rate limiting. Guest names are guessable. Comment in code: `// TODO: Verify proper working`.

### 8.9 LOW: Google Booking Server Merchant ID Trust

`BookingServerService` sets `SessionStore.tenantId = request.merchant_id` from body. If HTTP Basic credentials are compromised, any tenantId can be targeted.

### 8.10 LOW: JWT 24h Expiry, No Revocation

No refresh token, no token revocation. Compromised JWT valid for up to 24h.

### 8.11 LOW: Swagger Exposed in Production

`/api` in production exposes full API schema including admin routes.

### 8.12 LOW: Email Subject Bug

Confirmation emails use subject "Reservation Updated - KADiCon" even for first approval.

---

## 9. Extension Potential

### Easy to Extend

- New customer route: add controller with `@UseInterceptors(TenantHeaderInterceptor)` — tenancy automatic
- New admin route: `@UseAuth(JWT)` + `@UseInterceptors(MultiTenancyInterceptor)` — tenancy automatic
- New entity: extend `BaseEntity` + `BaseEntityRepository<T>` — tenancy automatic
- New email template: add MJML file + constant + service method
- New cron: `@Cron({ name, schedule, appconfigScheduleKey })` — AWS AppConfig integration built in
- New BullMQ queue: infrastructure is modular

### Hard to Extend

- Fine-grained roles/permissions: no RBAC layer exists, only admin/customer distinction
- WebSocket granularity: only one event type (`reservations_updated`), adding per-tenant or per-reservation push requires rework
- Outbound webhooks to tenants: no delivery infrastructure exists
- Reservation audit log / history: no event sourcing; every state transition would need new recording
- Multi-factor auth: no MFA infrastructure
- Customer self-serve cancellation: see section 11

---

## 10. Inferred Roadmap

| Item | Evidence | Location |
|------|----------|----------|
| Refresh tokens | TODO: "Lower this value after implementing refresh tokens" | `core.module.ts` |
| Unconfirmed reservation table re-assignment | Large commented-out block | `reservation.service.ts:250-282` |
| Queue-based confirm flow | TODO: "Move to a queue" | `reservation.service.ts:252` |
| Idempotency token for Google bookings | TODO comment | `booking-server.service.ts` |
| Unconfirmed reservations in table assignment | TODO: "Also take unconfirmed reservations" | `table-assignment.service.ts:125` |
| Free table re-assignment after reject/unconfirm | TODO x2 | `reservation.service.ts:307,328` |
| QR check-in verification fix | TODO: "Verify proper working" | `reservation.service.ts:399` |
| Dashboard order timeout from settings | TODO: "Get from company preferences" | `order.service.ts:156` |
| ReservationSource `admin` → `dashboard` rename | TODO comment | `domain/reservation/enums.ts` |
| Customer cancellation endpoint (KAR-612) | No implementation | — |
| Stripe payment for customers | Controller exists | Billing feature |
| Multi-language | `preferedLanguage` JWT field + `@jsverse/transloco` | Frontends |
| Google feed automation | Cron + SFTP processor maintained | Google integration |

---

## 11. KAR-612 Storno-Lifecycle Feasibility

**Requirement:** `POST /api/customer/reservation/:id/cancel` with magic-link token, time-window, audit log, rate-limit, state transition, email confirmation, optional reason.

### 11.1 Controller Placement

Add to: `apps/backend/src/app/features/reservation/controllers/customer-reservation.controller.ts`

New endpoint:
```typescript
@Post(':id/cancel')
@ApiQuery({ name: 'token', type: String })
async cancel(
  @Param('id', ParseUUIDPipe) id: string,
  @Query('token') token: string,
  @Body() body: CancelReservationRequest  // { reason?: string }
): Promise<void>
```

### 11.2 Service Method

Add `cancelReservation(id, token, reason?)` to `apps/backend/src/app/features/reservation/services/reservation.service.ts`:

1. Verify JWT token: `jwtService.verifyAsync(token, { secret: JWT_CONSTANTS.SECRET })`
2. Validate `{ reservationId, tenantId }` from token matches the route `id`
3. Load reservation, validate state is `unconfirmed` or `approved` (not seated/done/rejected)
4. Check time window: `new Date() < addHours(reservation.startDate, -(settings.cancellationWindowHours ?? 24))`
5. Transition state to `rejected` (or new `cancelled` state)
6. Optional: save `cancellationReason` (new column, new migration)
7. Call `notifyReservationUpdate()` → triggers existing email + Google realtime update

### 11.3 Magic-Link Token

Pattern already exists in `apps/backend/src/app/features/auth/services/auth.service.ts`:
```typescript
// Generate at reservation creation:
const token = await jwtService.signAsync(
  { reservationId: reservation.id, tenantId: reservation.tenantId },
  { secret: JWT_CONSTANTS.SECRET, expiresIn: '7d' }
)
// Include in confirmation email as: 
// ${CUSTOMER_APP_URL}/cancel?id=${reservation.id}&token=${token}&tenantId=${tenantId}
```

The cancel URL must include the `tenantId` so the BFF can inject `X-TENANT-ID` header. The magic link goes to the restaurant's website (e.g., Durrani), which proxies to KADi.

### 11.4 Time-Window Rules

Extend `IReservationSettings` in `libs/backend/domain/src/lib/settings/reservation.model.ts`:
```typescript
cancellationWindowHours: number  // hours before reservation; 0 = always cancellable
```

No migration needed (JSON column).

### 11.5 Audit Log

Two options:
1. **Minimal (migration):** Add `cancelledAt: Date`, `cancellationReason: string` columns to `Reservation` entity
2. **Proper:** New `ReservationAuditLog` entity with `reservationId, action, actor, payload, createdAt`

Minimal fits the existing ORM pattern fastest.

### 11.6 State Transition

Option A: Reuse `rejected` state → simplest, but loses distinction between restaurant-reject and customer-cancel  
Option B: New `cancelled` state → requires:
- New enum value in `libs/core/types/src/`
- DB migration (enum column change)
- Update `OpenReservationFilter` / `ApprovedOpenReservationFilter`
- Update Google integration: already sends `UpdateBooking(CANCELED)` in `RealtimeUpdateProcessor`

Recommendation: add `cancelled` state for clean audit trail.

### 11.7 Email Confirmation

`apps/backend/src/app/features/reservation/shared/email.service.ts`: add `sendCancellationEmail(reservation)`.

New MJML template in `apps/backend/src/assets/templates/mjml/`. Include `.ics` with `STATUS:CANCELLED` (ICSService already handles this via `STATUS` field).

### 11.8 Rate Limiting

No existing throttling infrastructure. Requires adding `@nestjs/throttler` — this should be done globally (fixes 8.2 simultaneously). For the cancel endpoint specifically: 5 attempts per IP per hour.

### 11.9 Implementation Complexity

**Medium overall.** All infrastructure (JWT tokens, email, state transitions, repository) exists. New work:
- New controller method + DTO (~50 lines)
- New service method with validation (~80 lines)
- Token generation + inclusion in confirmation email (~30 lines)
- Optional `cancelled` state + migration (medium)
- Audit columns or entity (small-medium)
- New email template (medium)
- `@nestjs/throttler` global setup (required, medium but high impact)

**Key risk:** If the confirmation email fails on booking creation, the customer has no cancel link. Consider a separate endpoint `GET /api/customer/reservation/:id/cancel-token` that resends the cancel link to the email on file (rate-limited).

### 11.10 Files to Modify/Create

| Component | File |
|-----------|------|
| Controller (new endpoint) | `apps/backend/src/app/features/reservation/controllers/customer-reservation.controller.ts` |
| Service (new method) | `apps/backend/src/app/features/reservation/services/reservation.service.ts` |
| Email (cancel method) | `apps/backend/src/app/features/reservation/shared/email.service.ts` |
| Email template | `apps/backend/src/assets/templates/mjml/` (new file) |
| Settings model (window hours) | `libs/backend/domain/src/lib/settings/reservation.model.ts` |
| State enum (cancelled) | `libs/core/types/src/` |
| Migration | `apps/backend/src/migrations/` (new file) |
| Rate limiting | `apps/backend/src/app/core/core.module.ts` |
| DTO (CancelReservationRequest) | `apps/backend/src/app/features/reservation/types/reservation.dto.ts` |

---

## 12. Prioritized Product Improvements

### P0 — Immediate Security (do today)

1. Rotate all leaked credentials (AWS, DB passwords prod+staging, Stripe, Firebase, JWT secret)
2. Remove `.env` and `env_config.json` from git tracking + rewrite history
3. Remove hardcoded JWT secret fallback in `libs/backend/auth/src/lib/constants/jwt.constants.ts`

### P1 — Critical Security / Correctness

4. Add `@nestjs/throttler` globally — rate limiting on all customer + auth routes
5. Fix auto-approve 0-limit bug in `utils.service.ts`
6. Allowlist `field` parameter in `searchCustomers`
7. Implement refresh tokens (current 24h JWT is irrevocable)

### P2 — Product Completeness

8. KAR-612: Customer cancel endpoint (section 11 above)
9. Implement table re-assignment on confirm/reject (commented-out TODO in `reservation.service.ts`)
10. Idempotency token for widget bookings (duplicate POSTs create duplicate reservations)
11. Fix email subject: "Reservation Updated" → "Reservation Confirmed" for first approval
12. Add `cancelled` state distinct from `rejected`

### P3 — DX / Observability

13. Disable Swagger in production or protect behind IP/Basic auth
14. One-click Accept/Decline magic links for restaurant staff (current links require dashboard login)
15. Outbound webhooks for tenant integrations (no push on state changes)
16. Fix `tenantId nullable: true` in `BaseEntity` — should be non-nullable with DB constraint
17. Customer order cancellation endpoint (same gap as reservation)
18. Improve test coverage — email and state transition logic have minimal tests

---

## Appendix: Key Facts

| Fact | Value |
|------|-------|
| JWT expiry | 24h (hardcoded) |
| Slot duration | 30 minutes (hardcoded) |
| Slot availability | Binary — any free table, party size ignored |
| Reminder cron window | T+24h to T+24h+10min, every 10 min |
| Seeder cron | 03:00 UTC daily, production only |
| Durrani tenant UUID | `4e22f973-36d5-4e8c-b8a3-c85a7b87cf10` |
| Production API | `https://api.kadicon.de/api` |
| CORS | `origin: '*', methods: '*'` |
| Rate limiting | None |
| Swagger | Always on at `/api` |
| Tenant isolation | ORM-level (not DB RLS) |
| Raw SQL bypass | `BaseEntityRepository.query()` bypasses tenant filter |
