# ORDRAT - Backend Project Guide
> Powered by CAMELSOFT LLC | oordarat.com  
> This file is the single source of truth for Claude Code. Read it fully before touching any file.

---

## 1. What This Project Is

**ORDRAT** is the operating system for businesses that sell through social messaging. One dashboard. Every channel. Every order. Automated.

It unifies WhatsApp, Instagram, Facebook Messenger, and Telegram into a single inbox with an AI bot that takes orders automatically, a CRM that segments and retains customers, and analytics that make sense of it all - for any order-based business: restaurants, e-commerce, retail, cloud kitchens, boutiques, or any business that receives orders through social messaging apps.

**Channel strategy:** WhatsApp is the first channel in production. Instagram DM, Facebook Messenger, Telegram and the website chat widget (`MessageChannel.WEB`) are supported in the data model and UI and are built in parallel - no feature should be architected as WhatsApp-only. Every channel-touching piece of code must use the `MessageChannel` enum and route through the same conversation/inbox abstraction.

**Web chat is the exception that proves the rule.** It has no external API and no webhook: a tenant creates a named `WebChatAgent`, pastes a `<script>` snippet on their own site, and the widget polls `/api/v1/public/webchat/**`. `ChannelMessagingRouter` therefore treats `WEB` as a no-op send - recording the outbound message in the inbox *is* the delivery. It is also the only channel where the customer key and the conversation key differ: the thread is keyed by a per-browser `web:<token>` handle so it cannot collide with the same person's WhatsApp thread, while the customer (and any order the bot places) is keyed by the phone number they give.

**Multi-tenancy model:** Each tenant = one F&B business. Every table in the DB has a `tenant_id`. The `TenantContext` ThreadLocal carries the current tenant on every request thread. Never write a query without a tenant scope.

---

## 2. Stack

| Layer | Technology |
|---|---|
| Language | Java 17 |
| Framework | Spring Boot 3.2.x |
| Auth | Spring Security + JJWT (no Keycloak - see §8) |
| Database | PostgreSQL 16 |
| Migrations | Flyway |
| ORM | Spring Data JPA / Hibernate |
| Cache | Redis 7 |
| Messaging | Apache Kafka |
| Email templates | Thymeleaf |
| HTTP client | Spring WebFlux `WebClient` (for Meta API, reCAPTCHA) |
| Mapping | MapStruct |
| Boilerplate | Lombok |
| Testing | JUnit 5, Mockito, MockMvc, Testcontainers |
| Build | Maven multi-module |
| Local infra | Docker Compose |

---

## 3. Module Structure

```
ordrat-backend/
├── ordrat-domain          ← Pure Java. Zero Spring, zero JPA. Models, events, exceptions, repo interfaces.
├── ordrat-application     ← Use-case services, port interfaces (in/out), command DTOs.
├── ordrat-infrastructure  ← All adapters: JPA entities, Spring Data repos, JWT, OTP, email, Kafka, Redis, WhatsApp.
├── ordrat-api             ← REST controllers, filters, WebSocket, request/response models, SecurityConfig.
└── ordrat-app             ← Spring Boot entry point. Assembles all modules. application.yml lives here.
```

### Dependency direction (enforced - never break this)
```
ordrat-api → ordrat-application → ordrat-domain
ordrat-infrastructure → ordrat-application → ordrat-domain
ordrat-app → ordrat-api + ordrat-infrastructure
```

`ordrat-domain` must never import Spring, JPA, or any framework. It is pure Java.  
`ordrat-application` may import `spring-context` and `spring-tx` for `@Service` and `@Transactional`. Nothing else from Spring.

---

## 4. Architecture: Hexagonal (Ports and Adapters)

Every external concern (DB, Kafka, Redis, WhatsApp, email) is accessed through a **port interface** defined in `ordrat-application/port/out/`. The implementation (adapter) lives in `ordrat-infrastructure`.

```
AuthService (application)
  │
  ├── UserRepository (port) ←── UserRepositoryAdapter (infrastructure/JPA)
  ├── TokenService (port)   ←── JwtTokenService (infrastructure/security)
  ├── OtpService (port)     ←── OtpServiceImpl (infrastructure/security)
  ├── EmailSender (port)    ←── EmailSenderImpl (infrastructure/email)
  ├── PasswordEncoder (port)←── BcryptPasswordEncoder (infrastructure/security)
  ├── RecaptchaVerifier(port)←── RecaptchaVerifierAdapter → RecaptchaService
  └── TenantProvisioner(port)←── TenantProvisionerImpl (infrastructure/tenant)
```

**Pattern for every new feature:**
1. Define domain model in `ordrat-domain`
2. Define port interface in `ordrat-application/port/out/`
3. Define use-case service in `ordrat-application/service/`
4. Implement adapter in `ordrat-infrastructure/`
5. Write controller in `ordrat-api/controller/v1/`
6. Write Flyway migration in `ordrat-infrastructure/src/main/resources/db/migration/`

---

## 5. Base Packages

| Module | Base package |
|---|---|
| ordrat-domain | `io.ordrat.domain` |
| ordrat-application | `io.ordrat.application` |
| ordrat-infrastructure | `io.ordrat.infrastructure` |
| ordrat-api | `io.ordrat.api` |
| ordrat-app | `io.ordrat` |

---

## 6. What Is Already Built

### ordrat-domain
- **Models:** `Tenant`, `User`, `Customer`, `Order`, `OrderItem`
- **Enums:** `TenantStatus`, `SubscriptionPlan`, `CustomerSegment`, `OrderStatus`, `OrderType`, `UserRole`
- **Domain events:** `OrderPlacedEvent`, `OrderStatusChangedEvent`, `WhatsAppMessageReceivedEvent`
- **Exceptions:** `DomainException` (abstract base), `InvalidCredentialsException`, `AccountLockedException`, `EmailNotVerifiedException`, `InvalidOtpException`, `EmailAlreadyUsedException`, `InvalidTokenException`, `TenantNotFoundException`, `TenantSuspendedException`, `OrderLimitExceededException`
- **Repository interfaces:** `UserRepository`, `TenantRepository`, `CustomerRepository`, `OrderRepository`

### ordrat-application
- **Auth commands/DTOs:** `RegisterCommand`, `LoginCommand`, `VerifyEmailCommand`, `ForgotPasswordCommand`, `ResetPasswordCommand`, `AuthTokens`
- **Port interfaces:** `PasswordEncoder`, `TokenService`, `OtpService`, `EmailSender`, `RecaptchaVerifier`, `TenantProvisioner`
- **Service:** `AuthService` - register, verifyEmail, login, refresh, logout, forgotPassword, resetPassword

### ordrat-infrastructure
- **JPA entities:** `UserEntity`, `RefreshTokenEntity`, `EmailOtpEntity`
- **Spring Data repos:** `UserJpaRepository`, `RefreshTokenJpaRepository`, `EmailOtpJpaRepository`
- **Adapters:** `UserRepositoryAdapter`, `BcryptPasswordEncoder`, `JwtTokenService`, `OtpServiceImpl`, `RecaptchaService`, `RecaptchaVerifierAdapter`, `EmailSenderImpl`, `TenantProvisionerImpl`
- **Mapper:** `UserMapper` (MapStruct)
- **Multi-tenancy:** `TenantContext` (ThreadLocal)
- **Kafka topics:** `KafkaTopics` constants class
- **Flyway migrations:** V1 (tenants + users + tokens + OTPs), V2 (customers + orders), V3 (menu), V4 (login_attempts)

### ordrat-api
- **Auth controller:** `AuthController` - 7 endpoints (register, verify-email, login, refresh, logout, forgot-password, reset-password)
- **Security:** `SecurityConfig` (stateless JWT, CORS), `JwtAuthFilter`, `OrdratAuthToken`
- **Exception handling:** `GlobalExceptionHandler` - maps every domain exception to HTTP status
- **Request/Response:** `RegisterRequest`, `LoginRequest`, `VerifyEmailRequest`, `RefreshRequest`, `ForgotPasswordRequest`, `ResetPasswordRequest`, `AuthResponse`

### ordrat-app
- `OrdratApplication.java` - main class with `@EnableAsync` + `@EnableScheduling`
- `application.yml` - full config with dev/prod profiles
- `AuthProperties` - `@ConfigurationProperties` for `ordrat.security.*`

### Docker Compose
PostgreSQL 16, Redis 7, Kafka + Zookeeper, Kafka UI (`:8090`), MailHog (`:8025`)

---

## 7. What Is NOT Yet Built (Build in This Order)

### Immediate - auth is not runnable without these
- [ ] **Thymeleaf email templates** - `verify-email.html`, `reset-password.html`, `welcome.html`  
  Path: `ordrat-infrastructure/src/main/resources/templates/email/`  
  Variables injected: `fullName`, `otp`, `expiryMinutes` (verify/reset) | `fullName`, `businessName` (welcome)

- [ ] **`TenantRepositoryAdapter`** - same pattern as `UserRepositoryAdapter`  
  Needs: `TenantEntity` JPA entity + `TenantJpaRepository` + `TenantMapper` + `TenantRepositoryAdapter`

- [ ] **Real JWT secret in `.env`** - copy `.env.example` to `.env`, set `JWT_SECRET` to a 32+ char random string

### Backend modules (build in this order after auth runs)

1. **Credentials Vault** - superadmin-managed encrypted API keys (AES-256); other modules pull keys from here at runtime. All channel integrations and AI providers pull their secrets from here.

2. **Channel Abstraction Layer** - shared ports and domain model that all messaging channels implement before any channel-specific adapter is built:
   - `ChannelAdapter` port interface: `sendMessage()`, `sendTemplate()`, `resolveWebhook()`
   - `ChannelRouter` service: routes outbound messages to the correct adapter by `MessageChannel` enum
   - `ConversationEntity` + `MessageEntity` JPA entities and Flyway migration (V5)
   - `ConversationRepository` + `MessageRepository` domain interfaces

3. **WhatsApp Channel** - first channel in production (Meta Cloud API):
   - Webhook handler (`POST /api/v1/webhooks/whatsapp`), signature verification
   - `WhatsAppAdapter` implementing `ChannelAdapter`
   - Send text, send template (for campaigns), media inbound handling
   - Embedded Signup OAuth flow for tenant self-onboarding

4. **Instagram Channel** - Meta Graph API (same app as WhatsApp, different product):
   - Webhook handler (`POST /api/v1/webhooks/instagram`)
   - `InstagramAdapter` implementing `ChannelAdapter`
   - Instagram DM send/receive; story mention handling

5. **Facebook Messenger Channel** - Meta Graph API:
   - Webhook handler (`POST /api/v1/webhooks/messenger`)
   - `MessengerAdapter` implementing `ChannelAdapter`
   - Page-level send/receive; postback handling

6. **Telegram Channel** - Telegram Bot API:
   - Webhook handler (`POST /api/v1/webhooks/telegram`)
   - `TelegramAdapter` implementing `ChannelAdapter`
   - Bot token per tenant stored in Credentials Vault

7. **Menu Management** - channel-agnostic; the AI bot and human staff both read this:
   - Categories + items CRUD, image upload, active/inactive toggle, sort order
   - Bilingual (Arabic + English)

8. **AI Ordering Bot** - reads menu, receives inbound messages from `ChannelRouter`, replies via `ChannelRouter`:
   - `AiBotService` with pluggable provider (`ANTHROPIC` claude-haiku-4-5 / `GEMINI` gemini-1.5-flash)
   - Conversation state machine: `IDLE → COLLECTING → AWAITING_CONFIRMATION → CONFIRMED`
   - `aiState` field on `ConversationEntity` tracks bot state per conversation
   - On customer confirmation → places order automatically
   - AI message labelling: `ORDER`, `NOT_SURE`, `NOT_ORDER`

9. **Orders** - created by AI bot or manually by staff; channel-agnostic:
   - Status transitions: `PENDING → CONFIRMED → PREPARING → READY → COMPLETED / CANCELLED`
   - Kanban feed via WebSocket (`/topic/orders/{tenantId}`)
   - ESC/POS thermal printer payload generation

10. **Inbox** - unified across all channels:
    - Real-time conversation list + message thread via WebSocket (`/topic/inbox/{tenantId}`)
    - Assign to staff, resolve/reopen, read/unread tracking
    - Channel filter (ALL / WHATSAPP / INSTAGRAM / MESSENGER / TELEGRAM)
    - Manual reply routed through `ChannelRouter` to the correct channel adapter

11. **Campaigns** - broadcast via each channel's template/bulk API:
    - Target by customer segment, schedule, dispatch
    - WhatsApp requires pre-approved template messages; Instagram/Messenger/Telegram use free-form text
    - Per-recipient delivery tracking (sent / failed + error message)

12. **Loyalty** - stamp card per customer, stamp on order complete, reward redemption

13. **Customer Segmentation** - nightly scoring job: VIP, REGULAR, NEW, AT_RISK, LOST, SILENT, DORMANT

14. **Analytics** - orders/day, revenue/day, top items, customer growth, channel breakdown

15. **Billing** - Konnect Network integration, STARTER (300 orders/month) vs PRO (unlimited), trial/grace period state machine

16. **Settings** - tenant profile, business hours, delivery zones, connected channels, notification preferences

17. **Superadmin** - tenant list, impersonation, system health, credential vault UI

### Dashboard (frontend - `ordrat-dashboard/`)

The React dashboard shell and all page components already exist. Work proceeds in parallel with backend APIs - wire each page to its real API as the corresponding backend module is completed.

| Dashboard page | Depends on backend module |
|---|---|
| Login / Register / Verify email | Auth (already built) |
| Orders Kanban | Module 9 - Orders |
| Inbox | Module 10 - Inbox + Channel Abstraction |
| Menu | Module 7 - Menu Management |
| AI Config | Module 8 - AI Ordering Bot |
| Campaigns | Module 11 - Campaigns |
| Loyalty | Module 12 - Loyalty |
| Analytics | Module 14 - Analytics |
| Settings (connected channels) | Modules 3-6 - Channel adapters |
| Admin → Tenants | Module 17 - Superadmin |
| Admin → Config (Vault) | Module 1 - Credentials Vault |

Marketing website (Next.js) is a separate project, not in this repo.

---

## 8. Critical Decisions - Do Not Reverse

### No Keycloak
Keycloak was explicitly rejected for ORDRAT. Reason: realm-per-tenant model conflicts with ORDRAT's multi-tenant architecture, and the operational overhead isn't justified. Auth is implemented natively with Spring Security + JJWT. If Keycloak is suggested in any context, decline and use the existing auth infrastructure.

### No open-in-view
`spring.jpa.open-in-view=false` in application.yml. Never enable it. Lazy loading must be explicit.

### Flyway owns the schema
`spring.jpa.hibernate.ddl-auto=validate`. Hibernate validates, never creates or alters. All schema changes go through a new Flyway migration file.

### Token design
- Access tokens: JWT, HMAC-SHA256, 15 min TTL, carry `userId/email/role/tenantId`
- Refresh tokens: opaque 64-byte random, stored as SHA-256 hash in `refresh_tokens` table, rotate on every use
- Refresh token delivery: HttpOnly + Secure + SameSite=Strict cookie for browsers; request body fallback for mobile

### Forgot password always returns 200
Never confirm whether an email exists. `AuthService.forgotPassword()` wraps the OTP send in `ifPresent()` and always returns success.

---

## 9. Coding Conventions

### Naming
- Controllers: `{Domain}Controller` in `io.ordrat.api.controller.v1.{domain}/`
- Services: `{Domain}Service` in `io.ordrat.application.service/`
- Port interfaces: descriptive verb noun - `EmailSender`, `TokenService`, `RecaptchaVerifier`
- Adapters: `{Domain}RepositoryAdapter` or `{ServiceName}Impl`
- JPA entities: `{Domain}Entity`
- Spring Data repos: `{Domain}JpaRepository`
- MapStruct mappers: `{Domain}Mapper`
- Commands (input DTOs): `{Action}Command`
- Events: `{Fact}Event` (past tense fact)

### Error handling
All exceptions extend `DomainException`. `GlobalExceptionHandler` in `ordrat-api` maps them to HTTP responses. Never throw raw `RuntimeException` from domain or application code. Never catch and swallow `DomainException`.

The error response shape is always:
```json
{
  "code": "SNAKE_CASE_ERROR_CODE",
  "message": "Human readable message",
  "fieldErrors": {},
  "meta": {},
  "timestamp": "2024-01-01T00:00:00Z"
}
```

### Multi-tenancy
Every service method that accesses tenant-scoped data must have `tenantId` as a parameter - never read it from `TenantContext` inside the application layer. `TenantContext` is infrastructure concern; the filter sets it, and the controller reads it from `OrdratAuthToken` and passes it down.

```java
// CORRECT - tenantId is explicit
orderService.listOrders(auth.getTenantId(), status);

// WRONG - don't do this in application layer
orderService.listOrders(TenantContext.get(), status);
```

### Testing requirements
Every new service class needs a unit test in the same module under `src/test/`. Every new controller needs a `@WebMvcTest` integration test. Use Testcontainers for infrastructure tests that require a real DB or Kafka.

Test class naming: `{ClassName}Test` for unit tests, `{ClassName}IT` for integration tests.

### Lombok usage
Use `@Builder`, `@Getter`, `@Setter` on entities and DTOs. Use `@RequiredArgsConstructor` on services (for constructor injection). Do not use `@Data` on JPA entities (hashCode/equals based on all fields causes issues with Hibernate proxies). Use `@EqualsAndHashCode(of = "id")` on entities.

### MapStruct
All domain↔entity and domain↔DTO conversions go through MapStruct mappers. Never write manual mapping loops. Mapper interfaces live in `ordrat-infrastructure/persistence/mapper/` (for entity↔domain) and `ordrat-api/` (for domain↔response, if needed).

---

## 10. API Conventions

- Base path: `/api/v1/`
- Auth endpoints: `/api/v1/auth/`
- Tenant-scoped endpoints: `/api/v1/{resource}/` (tenant resolved from JWT, not URL)
- Superadmin endpoints: `/api/v1/admin/`
- Channel webhooks: `/api/v1/webhooks/whatsapp`, `/api/v1/webhooks/instagram`, `/api/v1/webhooks/messenger`, `/api/v1/webhooks/telegram`
- Web chat: `/api/v1/webchat/agents` (tenant CRUD) and `/api/v1/public/webchat/**` (unauthenticated visitor API + `widget.js`). The public path has its own any-origin CORS policy in `SecurityConfig`; the real boundary is the per-agent origin allow-list plus the visitor session token, never the site key.
- All responses use standard HTTP status codes
- Paginated list responses: `{ content: [], page: 0, size: 20, totalElements: 0, totalPages: 0 }`

### Controller method signatures
```java
@GetMapping
public ResponseEntity<PageResponse<OrderResponse>> listOrders(
        @AuthenticationPrincipal OrdratAuthToken auth,
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size) {
    // auth.getTenantId() for tenant scope
    // auth.getUserId() for user-specific data
}
```

---

## 11. Flyway Migration Rules

- Files: `ordrat-infrastructure/src/main/resources/db/migration/`
- Naming: `V{n}__{snake_case_description}.sql`
- Next migration number: **V76**
- Never modify an existing migration - always add a new one
- Always include `tenant_id` FK + index on every new business table
- Use `gen_random_uuid()` for default UUID PKs (requires `pgcrypto` extension, already loaded in V1)
- Use `TIMESTAMPTZ` (not `TIMESTAMP`) for all datetime columns

---

## 12. Environment Variables

Copy `.env.example` to `.env` before running. Minimum required to start:

```
DB_URL=jdbc:postgresql://localhost:5432/ordrat
DB_USER=ordrat
DB_PASS=ordrat
REDIS_HOST=localhost
KAFKA_BROKERS=localhost:9092
JWT_SECRET=<at-least-32-random-characters>
```

Everything else (Meta, Stripe, Google, reCAPTCHA) can be empty for local dev - the adapters degrade gracefully when keys are missing.

---

## 13. Local Development Startup

```bash
# 1. Start infrastructure
docker compose up -d

# 2. Wait for postgres health
docker compose ps   # all should show "healthy"

# 3. Copy env
cp .env.example .env
# Edit .env - set JWT_SECRET

# 4. Build
mvn clean install -DskipTests

# 5. Run
cd ordrat-app
mvn spring-boot:run -Dspring-boot.run.profiles=dev

# Useful URLs
# API:         http://localhost:8080
# Swagger UI:  http://localhost:8080/swagger-ui.html
# Kafka UI:    http://localhost:8090
# MailHog:     http://localhost:8025
```

---

## 14. Kafka Topics (All Defined in `KafkaTopics.java`)

### Inbound - one topic per channel (separate consumer groups per channel adapter)

| Constant | Topic Name |
|---|---|
| `CHANNEL_INBOUND_WHATSAPP` | `ordrat.channel.whatsapp.inbound` |
| `CHANNEL_INBOUND_INSTAGRAM` | `ordrat.channel.instagram.inbound` |
| `CHANNEL_INBOUND_MESSENGER` | `ordrat.channel.messenger.inbound` |
| `CHANNEL_INBOUND_TELEGRAM` | `ordrat.channel.telegram.inbound` |

Each inbound payload has the shape `{ tenantId, channel, conversationId, messageId, senderId, messageType, content, timestamp }`. The AI bot and inbox service consume all four topics.

### Outbound - single topic, ChannelRouter dispatches

| Constant | Topic Name |
|---|---|
| `CHANNEL_OUTBOUND` | `ordrat.channel.outbound` |

Payload: `{ tenantId, channel, conversationId, recipientId, messageType, content, templateName? }`. `ChannelRouter` reads `channel` and delegates to the correct `ChannelAdapter`.

### Business events

| Constant | Topic Name |
|---|---|
| `ORDER_PLACED` | `ordrat.orders.placed` |
| `ORDER_STATUS_CHANGED` | `ordrat.orders.status-changed` |
| `AI_BOT_PROCESS` | `ordrat.ai.process` |
| `CAMPAIGN_DISPATCH` | `ordrat.campaigns.dispatch` |
| `NOTIFICATION_PUSH` | `ordrat.notifications.push` |
| `BILLING_PAYMENT_SUCCESS` | `ordrat.billing.payment-success` |
| `BILLING_PAYMENT_FAILED` | `ordrat.billing.payment-failed` |
| `ANALYTICS_EVENT` | `ordrat.analytics.events` |

`AI_BOT_PROCESS` is published by all inbound consumers when a conversation has `aiEnabled=true`; the AI bot service is the sole consumer.

---

## 15. Security Rules

- `JwtAuthFilter` reads `Authorization: Bearer <token>`, validates with `JwtTokenService.parseAccessToken()`, populates `SecurityContextHolder` with `OrdratAuthToken`
- Use `@AuthenticationPrincipal OrdratAuthToken auth` in every protected controller
- Superadmin endpoints: `@PreAuthorize("hasRole('SUPER_ADMIN')")`
- Tenant owner only: `@PreAuthorize("hasRole('TENANT_OWNER')")`
- Any authenticated user: no annotation needed (covered by `.anyRequest().authenticated()`)
- Never put `tenantId` in URL paths for business APIs - always derive it from the JWT claim

---

## 16. Billing Context

**No trials.** Every tenant is `ACTIVE` from provisioning and claims the one-time free plan at email
verification (`BillingService.grantSignupFreePlan`). `TenantStatus.TRIAL`, `trial_ends_at` and
`trial_reminder_sent_at` were removed in V56.

**Free plan** - claimed once per tenant for life (`freePlanClaimedAt`). Its credits go straight into
the persistent top-up balance (`aiTokenBalance`), `planRenewsAt` stays null, and it is never
refilled. More credits means buying a top-up pack.

**Paid plans are real Stripe subscriptions.** Checkout runs in `SUBSCRIPTION` mode, Stripe keeps the
card, and every charge it makes arrives as an `invoice.paid` webhook → `handlePlanPurchase`, which is
**the only place plan credits are ever granted**. Purchase context lives in the *subscription's*
metadata, not the session's, because renewals have no session behind them.

- **Monthly**: one charge, one refill, `planRenewsAt` = Stripe's period end.
- **Annual**: charged once a year, but the tenant bought a *monthly* allowance, so
  `BillingService.refillAnnualAllowances()` (daily, via `BillingScheduler`) tops them back up every
  month between invoices. Monthly subscribers are deliberately excluded - their refill is the payment.

**Lifecycle** (`processPlanLifecycle`, daily): `planRenewsAt` passed → `GRACE` (3 days,
`GRACE_PERIOD_DAYS`) → `EXPIRED`. On expiry the tenant is set `SUSPENDED` and **every credit is
zeroed** - the plan allowance and the purchased top-up balance alike. `invoice.payment_failed` enters
grace early; `customer.subscription.deleted` just clears `stripeSubscriptionId`.

**Service lockout**: `SubscriptionGuardFilter` returns **402 `SUBSCRIPTION_INACTIVE`** for every
tenant endpoint except `/billing`, `/auth`, `/public`, `/webhooks`, `/system`, `/support` whenever
`!tenant.isActive()` (i.e. status is not `ACTIVE`). Free-plan tenants are never locked - they simply
run out of credits and the bot pauses itself.

**Payment**: Stripe only. Plans = subscriptions; AI credit packs = one-off `PAYMENT` checkouts.
Live-order payments use each tenant's own connected account (Stripe Connect). Wallets (Apple Pay,
Google Pay, Link) need no code - enable them on the Stripe account, and register the dashboard domain
under Stripe → Settings → Payment method domains for Apple Pay to appear in embedded checkout.

**Signup abuse** (`SignupGuard`, V57) - the free plan's credits are once per tenant for life, which
only holds if one person cannot become many tenants. Three signals, all tunable by the super admin
under the `SIGNUP` settings category:
- `EmailNormalizer` collapses gmail dots and `+tags` to the real mailbox; `users.email_normalized` is
  checked at register, so `m.o+1@gmail.com` cannot become a second account. The index is
  **deliberately not unique** - pre-existing rows may collide and a failed index build would abort
  the migration; `AuthService.register` enforces it instead.
- Throwaway mail domains (`BLOCK_DISPOSABLE_EMAILS`, `DISPOSABLE_DOMAINS`) are refused with 403
  `SIGNUP_NOT_ALLOWED`.
- `FREE_CLAIMS_PER_IP` / `FREE_CLAIM_WINDOW_DAYS` (default 2 per 30 days) cap free-credit grants per
  `tenants.signup_ip`. Past the cap the account is still created, it just starts with no free credits
  - CGNAT and shared offices mean one IP is legitimately many businesses. A missing IP always passes.
  The IP comes from `X-Forwarded-For` (spoofable, which is why it never blocks an account); nginx
  must overwrite that header, not append to it.

**Cancelling** (`POST /billing/subscription-plans/cancel`) stops the next charge only. The tenant
keeps the plan until `planRenewsAt`, then the normal grace/expiry machine takes over.

---

## 17. Owner Notification Escalation Ladder

When an event needs to alert the business owner:
1. Dashboard notification (in-app, WebSocket)
2. Browser push (FCM/VAPID)
3. WhatsApp message to `tenant.ownerPersonalPhone` (WhatsApp is used for owner alerts regardless of which channel the customer used)

Always attempt all three. Failures at steps 1 or 2 do not block step 3.

---

## 18. File Locations Quick Reference

| What you need | Where it lives |
|---|---|
| Add a new domain model | `ordrat-domain/src/main/java/io/ordrat/domain/model/` |
| Add a new exception | `ordrat-domain/src/main/java/io/ordrat/domain/exception/` |
| Add a domain event | `ordrat-domain/src/main/java/io/ordrat/domain/event/` |
| Add a repository interface | `ordrat-domain/src/main/java/io/ordrat/domain/repository/` |
| Add a use-case service | `ordrat-application/src/main/java/io/ordrat/application/service/` |
| Add a port interface | `ordrat-application/src/main/java/io/ordrat/application/port/out/` |
| Add a command DTO | `ordrat-application/src/main/java/io/ordrat/application/dto/{domain}/` |
| Add a JPA entity | `ordrat-infrastructure/src/main/java/io/ordrat/infrastructure/persistence/entity/` |
| Add a Spring Data repo | `ordrat-infrastructure/src/main/java/io/ordrat/infrastructure/persistence/repository/` |
| Add a domain↔entity mapper | `ordrat-infrastructure/src/main/java/io/ordrat/infrastructure/persistence/mapper/` |
| Add a DB migration | `ordrat-infrastructure/src/main/resources/db/migration/` |
| Add an email template | `ordrat-infrastructure/src/main/resources/templates/email/` |
| Add a REST controller | `ordrat-api/src/main/java/io/ordrat/api/controller/v1/{domain}/` |
| Add a request/response model | `ordrat-api/src/main/java/io/ordrat/api/controller/v1/{domain}/` |
| Add a filter | `ordrat-api/src/main/java/io/ordrat/api/filter/` |
| Edit app config | `ordrat-app/src/main/resources/application.yml` |
| Edit config properties class | `ordrat-app/src/main/java/io/ordrat/config/` |

---

*ORDRAT - Powered by CAMELSOFT LLC*
