# REST ERP — Multi-Tenant SaaS Architecture

## Table of Contents

1. [Overview](#overview)
2. [Architecture Diagram](#architecture-diagram)
3. [Database Schema Layout](#database-schema-layout)
4. [Package Structure](#package-structure)
5. [Request Lifecycle](#request-lifecycle)
6. [Core Multi-Tenant Components](#core-multi-tenant-components)
7. [Authentication Flows](#authentication-flows)
8. [How to Add a New Company (Tenant)](#how-to-add-a-new-company-tenant)
9. [Refactoring Guide — Removing Company Parameters](#refactoring-guide--removing-company-parameters)
10. [Testing the Multi-Tenant Architecture](#testing-the-multi-tenant-architecture)
11. [Health Check & Debug Endpoints](#health-check--debug-endpoints)
12. [SecurityConfig — Public vs Protected Endpoints](#securityconfig--public-vs-protected-endpoints)
13. [Scheduled Tasks & Background Jobs](#scheduled-tasks--background-jobs)
14. [WebSocket / WebRTC Considerations](#websocket--webrtc-considerations)
15. [Common Mistakes to Avoid](#common-mistakes-to-avoid)
16. [Environment Configuration](#environment-configuration)

---

## Overview

REST ERP is a SaaS enterprise management application built with **Spring Boot** (Java 17) and **Angular**. It includes modules for HR, Finance (invoicing, accounting, POS), Project Management, Communication (chat, WebRTC), and more.

The system uses a **schema-per-tenant** multi-tenancy strategy with **PostgreSQL** and **Hibernate**. Each company gets its own PostgreSQL schema (e.g., `company_14`). Data isolation is automatic — once a user authenticates, every database query is scoped to their company's schema. **Controllers and services do NOT need to pass `companyId` as a parameter.**

**Tech Stack:**
- Java 17 + Spring Boot 2.x
- PostgreSQL (schema-per-tenant via Hibernate `MultiTenancyStrategy.SCHEMA`)
- JWT authentication (Bearer tokens with `tenantSchema` and `companyId` claims)
- Angular frontend
- Firebase for push notifications
- ZATCA e-invoicing SDK

---

## Architecture Diagram

```
┌──────────────────────────────────────────────────────────────────────┐
│                         Angular Frontend                             │
│  (dashboard.rest.net.sa / admin.rest.net.sa)                        │
└──────────────────────┬───────────────────────────────────────────────┘
                       │  HTTPS + JWT Bearer Token
                       ▼
┌──────────────────────────────────────────────────────────────────────┐
│                         Spring Boot Backend                          │
│                                                                      │
│  ┌─────────────┐   ┌─────────────┐   ┌──────────────────────────┐   │
│  │ TenantFilter │──▶│ AuthFilter  │──▶│ Controller / Service     │   │
│  │ (Order: 1)   │   │ (Security)  │   │ (auto-scoped to tenant)  │   │
│  │              │   │             │   │                          │   │
│  │ JWT ──▶      │   │ Validates   │   │ Uses TenantContextService│   │
│  │  schema +    │   │ JWT & sets  │   │ for cross-schema access  │   │
│  │  companyId   │   │ SecurityCtx │   │                          │   │
│  └─────────────┘   └─────────────┘   └──────────────────────────┘   │
│                                                                      │
│  ┌──────────────────────────────────────────────────────────────┐    │
│  │               Hibernate Multi-Tenant Layer                    │    │
│  │  TenantContext (ThreadLocal) → ConnectionProvider → Schema    │    │
│  └──────────────────────────────────────────────────────────────┘    │
└──────────────────────┬───────────────────────────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────────────────────────┐
│                         PostgreSQL Database                           │
│                                                                      │
│  ┌─────────────┐  ┌─────────────┐  ┌────────────┐  ┌────────────┐  │
│  │   public     │  │ superadmin  │  │ company_14 │  │ company_15 │  │
│  │             │  │             │  │            │  │            │  │
│  │ - roles     │  │ - company   │  │ - users    │  │ - users    │  │
│  │ - countries │  │ - superadmin│  │ - invoices │  │ - invoices │  │
│  │ - shared    │  │   _users    │  │ - products │  │ - products │  │
│  │   ref data  │  │ - trail     │  │ - employees│  │ - employees│  │
│  │             │  │   _model    │  │ - customers│  │ - customers│  │
│  └─────────────┘  └─────────────┘  └────────────┘  └────────────┘  │
└──────────────────────────────────────────────────────────────────────┘
```

---

## Database Schema Layout

| Schema | Purpose | Key Tables |
|--------|---------|------------|
| `public` | Shared reference data, Hibernate default | `roles`, `country`, shared lookups |
| `superadmin` | SaaS platform management | `company`, `superadmin_users`, `trail_model` |
| `company_{id}` | Per-tenant isolated data | `users_table`, `invoice`, `product`, `employee`, `department`, `chart`, `journal`, etc. |

**Where does each entity live?**
- `company` entity → **superadmin** schema (managed by `CompanyService` in `SuperAdmin/Services/`)
- `Role` entity → **public** schema (shared across all tenants, in `Public/Models/`)
- `users` entity → **tenant** schema (each company has its own users)
- All business data → **tenant** schema

---

## Package Structure

```
src/main/java/com/camelsoft/restforyou/
│
├── Client/                              # ★ TENANT-SCOPED CODE
│   ├── Controller/                      # REST endpoints (HR, Finance, Auth, etc.)
│   │   ├── AuthController.java          # Login, logout, refresh token
│   │   ├── HR/Admin/                    # HR admin endpoints
│   │   ├── HR/Employee/                 # Employee self-service
│   │   ├── Finances/                    # Invoices, bills, POS, products
│   │   └── Tools/                       # Appointments, ratings, FAQ
│   ├── Models/                          # JPA entities (tenant data)
│   ├── Services/                        # Business logic
│   │   ├── Auth/                        # UserService, RefreshTokenService
│   │   ├── Finance/                     # Invoice, billing, products
│   │   ├── HRTools/                     # Salary, loans, attendance
│   │   ├── MultiTenant/                 # TenantSchemaService
│   │   └── Schedule/                    # Cron jobs (⚠️ needs tenant context)
│   ├── Repository/                      # JPA repositories
│   └── DTO/                             # Data transfer objects
│
├── Public/                              # ★ SHARED DATA (public schema)
│   ├── Models/Role.java                 # Shared Role entity
│   ├── Enum/RoleEnum.java               # Role enumeration
│   ├── Services/RoleService.java        # Role CRUD
│   └── Controllers/CountryController.java
│
├── SuperAdmin/                          # ★ PLATFORM MANAGEMENT (superadmin schema)
│   ├── controllers/
│   │   ├── SuperAdminAuthController.java   # SuperAdmin login (no companyName)
│   │   ├── SuperAdminControllerV2.java     # Create/manage companies
│   │   ├── MigrationController.java        # Data migration endpoints
│   │   └── TrialController.java            # Trial management
│   ├── models/
│   │   ├── company.java                    # Company entity (superadmin schema)
│   │   ├── SuperAdminUser.java             # Platform admin users
│   │   └── TrailModel.java                 # Trial period tracking
│   ├── Services/
│   │   ├── CompanyService.java             # Company CRUD
│   │   └── DataMigrationService.java       # Old DB → multi-tenant migration
│   └── repository/
│
├── tools/                               # ★ INFRASTRUCTURE
│   ├── configuration/
│   │   ├── MultiTenant/                 # ★★★ CORE MULTI-TENANT FILES ★★★
│   │   │   ├── TenantContext.java                      # ThreadLocal storage
│   │   │   ├── TenantContextService.java               # Cross-schema operations
│   │   │   ├── TenantFilter.java                       # JWT → schema extraction
│   │   │   ├── MultiTenantConfiguration.java           # Hibernate config
│   │   │   ├── MultiTenantConnectionProviderImpl.java  # Schema switching
│   │   │   └── CurrentTenantIdentifierResolverImpl.java
│   │   └── HealthCheckController.java   # Health & debug endpoints
│   ├── scurity/
│   │   ├── AuthFilter.java              # JWT validation + Spring Security
│   │   └── SecurityConfig.java          # Endpoint security rules
│   └── util/
│       └── TokenUtil.java               # JWT generation & parsing
│
└── webRTCvideoChat/                     # Video conferencing module
```

---

## Request Lifecycle

```
1. HTTP Request arrives → Authorization: Bearer <JWT>
       │
2. TenantFilter (Order 1) — FIRST
       │  • Extracts tenantSchema from JWT → e.g., "company_14"
       │  • Extracts companyId from JWT → e.g., 14
       │  • Stores in TenantContext (ThreadLocal)
       │  • SuperAdmin tokens → schema = "public"
       │  • No token (public endpoints) → schema = "public"
       │
3. AuthFilter (Spring Security) — SECOND
       │  • Validates JWT signature & expiration
       │  • Loads UserDetails from DB (queries correct schema!)
       │  • Sets Spring SecurityContext
       │
4. Controller executes
       │  • ALL JPA queries automatically go to tenant schema
       │  • No companyId parameter needed
       │
5. TenantFilter finally block
       │  • TenantContext.clear() — prevents memory leaks
       │
6. Response sent
```

---

## Core Multi-Tenant Components

### TenantContext.java — ThreadLocal Storage
```java
// Auto-set by TenantFilter on every request
TenantContext.getCurrentTenant();     // "company_14"
TenantContext.getCurrentCompanyId();  // 14L

// Manual use (login, background jobs):
TenantContext.setCurrentTenant("company_14");
TenantContext.setCurrentCompanyId(14L);
TenantContext.clear(); // ALWAYS in finally block
```

### TenantContextService.java — Cross-Schema Helper
```java
@Autowired
private TenantContextService tenantContextService;

// Get company (auto-switches to superadmin schema and back)
company comp = tenantContextService.getCurrentCompany();

// Get company ID (no DB call, reads ThreadLocal)
Long id = tenantContextService.getCurrentCompanyId();

// For public endpoints (no JWT context)
company comp = tenantContextService.getCompanyById(id);
```

### TenantFilter.java — Request Filter
- `@Order(1)` — runs before AuthFilter
- Reads JWT → extracts `tenantSchema` + `companyId`
- Sets TenantContext
- Clears context in `finally`

### MultiTenantConnectionProviderImpl.java — Hibernate Connection Provider
- Sets `SET search_path TO <schema>` on each connection
- Resets to `public` on release

### CurrentTenantIdentifierResolverImpl.java — Hibernate Resolver
- Reads `TenantContext.getCurrentTenant()` to tell Hibernate which schema

---

## Authentication Flows

### Regular User Login

```
POST /api/v1/auth/signin
{
  "companyName": "ABC Corp",
  "username": "admin@abc.com",
  "password": "pass123",
  "deviceType": "WEB",
  "deviceId": "device-001",
  "ip": "192.168.1.1"
}
```

**Flow:**
1. Validate company name → query `CompanyService` (superadmin schema)
2. Get `tenantSchema` from company entity → `"company_14"`
3. `TenantContext.setCurrentTenant("company_14")` — no JWT yet!
4. Query user from tenant schema
5. Check trial expiration
6. Authenticate via Spring Security `AuthenticationManager`
7. Generate JWT: `{ sub: "admin@abc.com", tenantSchema: "company_14", companyId: 14 }`
8. Create device tracking record
9. Return `JwtResponse` with token + refreshToken

**Response:**
```json
{
  "token": "eyJ...",
  "refreshToken": "abc-def-123",
  "roles": "ROLE_ADMIN",
  "deviceId": "device-001",
  "userId": 1,
  "tokenExpiryDate": "2025-02-20T10:00:00",
  "tenantSchema": "company_14"
}
```

### SuperAdmin Login

```
POST /api/v1/superadmin/auth/signin
{
  "username": "admin",
  "password": "superpass"
}
```

- No `companyName` needed
- Queries `superadmin_users` from public schema
- JWT: `{ tenantSchema: "public", isSuperAdmin: true }`

### Token Refresh

```
POST /api/v1/auth/refreshtoken
{
  "refreshToken": "<refresh_token>",
  "token": "<expired_jwt>",
  "deviceType": "WEB",
  "deviceId": "device-001"
}
```

- Extracts `tenantSchema` and `companyId` from the **expired** JWT (claims are still readable)
- Verifies refresh token in that tenant's schema
- Issues new JWT with same tenant info

---

## How to Add a New Company (Tenant)

```
POST /api/v1/super_admin_v2/add_company
Headers: Authorization: Bearer <superadmin_token>
Params: companyname, email, password, name, phone, address, city, country, trial_period_days
```

**What happens:**
1. Company record created in superadmin schema
2. PostgreSQL schema `company_{id}` created via `TenantSchemaService`
3. `company.tenantSchema` updated
4. `TenantContext` manually set to new schema
5. Admin user created in tenant schema with `ROLE_ADMIN`
6. TenantContext cleared
7. If any step fails → rollback (delete schema + company)

---

## Refactoring Guide — Removing Company Parameters

### Before (Old Pattern)
```java
// Everything filtered by companyId
List<Invoice> findByCompanyId(Long companyId);

public List<Invoice> getInvoices(Long companyId) {
    return invoiceRepo.findByCompanyId(companyId);
}

@GetMapping("/invoices")
public ResponseEntity<?> getInvoices(@RequestParam Long companyId) { ... }
```

### After (Multi-Tenant)
```java
// Schema isolation handles filtering — just query directly
List<Invoice> findAll();

public List<Invoice> getInvoices() {
    return invoiceRepo.findAll();
}

@GetMapping("/invoices")
public ResponseEntity<?> getInvoices() { ... }
```

### When You Need Company Info
```java
// Company ID (no DB call)
Long id = tenantContextService.getCurrentCompanyId();

// Full company entity (auto-switches to superadmin schema)
company comp = tenantContextService.getCurrentCompany();
```

### Cross-Schema Access Pattern
```java
String previousSchema = TenantContext.getCurrentTenant();
try {
    TenantContext.setCurrentTenant("superadmin");
    // ... query superadmin data ...
} finally {
    TenantContext.setCurrentTenant(previousSchema);
}
```

---

## Testing the Multi-Tenant Architecture

### 1. Health Check (no auth)

```bash
curl https://server.rest.net.sa/api/v1/public/health
```
Expected:
```json
{ "status": "UP", "database": "UP", "tenantSchemas": 5 }
```

### 2. List Schemas (no auth)

```bash
curl https://server.rest.net.sa/api/v1/public/health/schemas
```
Expected: list of all schemas including `company_*`

### 3. SuperAdmin Login

```bash
curl -X POST https://server.rest.net.sa/api/v1/superadmin/auth/signin \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "yourpass"}'
```

### 4. Create a Company

```bash
curl -X POST https://server.rest.net.sa/api/v1/super_admin_v2/add_company \
  -H "Authorization: Bearer <superadmin_token>" \
  -F "companyname=TestCompany" \
  -F "email=admin@test.com" \
  -F "password=Admin123!" \
  -F "name=Admin User" \
  -F "trial_period_days=30"
```

### 5. Regular User Login

```bash
curl -X POST https://server.rest.net.sa/api/v1/auth/signin \
  -H "Content-Type: application/json" \
  -d '{
    "companyName": "TestCompany",
    "username": "admin@test.com",
    "password": "Admin123!",
    "deviceType": "WEB",
    "deviceId": "test-001",
    "ip": "127.0.0.1"
  }'
```

### 6. Verify Tenant Context (authenticated)

```bash
curl -H "Authorization: Bearer <user_token>" \
  https://server.rest.net.sa/api/v1/debug/tenant-info
```
Expected:
```json
{
  "currentSchema": "company_14",
  "companyId": 14,
  "username": "admin@test.com",
  "authenticated": true,
  "roles": ["ROLE_ADMIN"],
  "hasTenantContext": true,
  "companyName": "TestCompany",
  "dbSearchPath": "company_14"
}
```

### 7. Verify Schema Tables (authenticated)

```bash
curl -H "Authorization: Bearer <user_token>" \
  https://server.rest.net.sa/api/v1/debug/verify-schema
```

### 8. Data Isolation Test

```bash
# Login as Company A → get token_A
# Login as Company B → get token_B

# Same endpoint, different data:
curl -H "Authorization: Bearer $TOKEN_A" .../api/v1/hr/employees  # Company A only
curl -H "Authorization: Bearer $TOKEN_B" .../api/v1/hr/employees  # Company B only
```

### 9. Database Verification

```sql
-- List all tenant schemas
SELECT schema_name FROM information_schema.schemata
WHERE schema_name LIKE 'company_%' ORDER BY schema_name;

-- Tables in a specific tenant
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'company_14' ORDER BY table_name;

-- Data isolation proof
SELECT count(*) FROM company_14.users_table;
SELECT count(*) FROM company_15.users_table;

-- Company table location
SELECT schemaname, tablename FROM pg_tables WHERE tablename = 'company';
```

---

## Health Check & Debug Endpoints

| Endpoint | Auth | Purpose |
|----------|------|---------|
| `GET /api/v1/public/health` | No | App status, DB connectivity, schema count |
| `GET /api/v1/public/health/schemas` | No | List all PostgreSQL schemas |
| `GET /api/v1/debug/tenant-info` | JWT | Current tenant context, user, roles |
| `GET /api/v1/debug/verify-schema` | JWT | Tables in current schema + health check |
| `GET /api/v1/debug/compare-schemas?schema1=X&schema2=Y` | JWT | Compare tables between schemas |

**File:** `tools/configuration/HealthCheckController.java`

---

## SecurityConfig — Public vs Protected Endpoints

The following endpoints are accessible **without authentication** (defined in `SecurityConfig.PUBLIC_ENDPOINTS`):

```
/api/v1/auth/**              ← User login, register, refresh token
/api/v1/superadmin/auth/**   ← SuperAdmin login (⚠️ ADD THIS!)
/api/v1/public/**            ← Health checks, public data
/api/v1/country/**           ← Country data
/api/v1/payments/**          ← Payment callbacks
/api/v1/zatca/**             ← ZATCA e-invoicing
/signaling/**, /ws/**, /socket/** ← WebSocket endpoints
```

**⚠️ Required: Add to `PUBLIC_ENDPOINTS` in `SecurityConfig.java`:**
```java
"/api/v1/superadmin/auth/**",
```

**⚠️ Required: Add to `MigrationController.java`:**
```java
@PreAuthorize("hasRole('SUPER_ADMIN')")
```

---

## Scheduled Tasks & Background Jobs

**⚠️ CRITICAL:** Cron jobs run on background threads with NO HTTP request — TenantContext is empty.

**Files affected:** `Client/Services/Schedule/`
- `BillNotificationCron.java`
- `ExpenseSchedule.java`
- `MyScheduler.java`
- `SubscriptionsSchedule.java`

**Required pattern:**
```java
@Scheduled(cron = "0 0 9 * * ?")
public void scheduledTask() {
    String prevSchema = TenantContext.getCurrentTenant();
    try {
        TenantContext.setCurrentTenant("superadmin");
        List<company> companies = companyService.findAll();
        TenantContext.clear();

        for (company comp : companies) {
            if (comp.getTenantSchema() == null) continue;
            try {
                TenantContext.setCurrentTenant(comp.getTenantSchema());
                TenantContext.setCurrentCompanyId(comp.getId());
                // ... tenant-scoped work ...
            } catch (Exception e) {
                logger.error("Failed for company {}: {}", comp.getId(), e.getMessage());
            } finally {
                TenantContext.clear();
            }
        }
    } finally {
        TenantContext.clear();
    }
}
```

---

## WebSocket / WebRTC Considerations

**⚠️ WebSocket connections bypass servlet filters after handshake.** The `webRTCvideoChat/` package needs tenant awareness.

**Solution:** Create a `TenantChannelInterceptor` that extracts tenant info from the JWT during CONNECT and restores TenantContext for each message. See the code example in the WebSocket section of the codebase documentation.

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Passing `companyId` as request param | Use `tenantContextService.getCurrentCompanyId()` |
| Forgetting `TenantContext.clear()` | Always use try-finally |
| Setting `hibernate.default_schema` | Remove from yml and config |
| `@Async` without tenant context | Pass tenant info explicitly |
| Querying company without schema switch | Use `TenantContextService` |
| Wrong `Role`/`RoleEnum` imports | Use `Public.Models.Role` and `Public.Enum.RoleEnum` |
| Cron jobs without tenant loop | Loop companies, set context per iteration |
| WebSocket DB queries | Use `TenantChannelInterceptor` |

---

## Environment Configuration

| Profile | Database | Usage |
|---------|----------|-------|
| `test` | `jdbc:postgresql://46.224.48.190:5432/restfu` | Dev/Testing |
| `production` | See `application-production.yml` | Live |

**Key Properties:**

| Property | Value | Notes |
|----------|-------|-------|
| `server.port` | 8000 | HTTPS with JKS keystore |
| `auth.header` | `Authorization` | JWT header name |
| `auth.jwtExpirationMs` | 86400000 (24h) | Token lifetime |
| `spring.jpa.hibernate.hbm2ddl.auto` | `update` | Auto-creates tables |

**⚠️ Remove `hibernate.default_schema: public` from application.yml — conflicts with multi-tenancy.**

---

## Quick Reference

| I need to... | Use this |
|--------------|----------|
| Get current company ID | `tenantContextService.getCurrentCompanyId()` |
| Get current company entity | `tenantContextService.getCurrentCompany()` |
| Get company in public endpoint | `tenantContextService.getCompanyById(id)` |
| Check if tenant context exists | `tenantContextService.hasTenantContext()` |
| Manually switch schema | `TenantContext.setCurrentTenant("schema")` + try-finally |
| Create a new tenant | `POST /api/v1/super_admin_v2/add_company` |
| Run migration | `POST /api/v1/migration/migrate/{id}` |
| Verify tenant works | `GET /api/v1/debug/tenant-info` |
| Check app health | `GET /api/v1/public/health` |