# WS Demo — Entity Model

Tenant **global unique** entity model: every record has a platform-wide unique `id` (UUID). Isolation uses scope columns, not composite primary keys.

---

## Scope levels

| Scope | `tenantId` | `companyId` | Examples |
|-------|------------|-------------|----------|
| **platform** | — | — | `tenants`, `platform_applications`, `signup_leads` |
| **global** | — | — | `currencies` (reference) |
| **tenant** | required | — | `access_categories`, `access_groups`, `approval_workflows` |
| **company** | required | required | `chart_of_accounts`, `journal_entries`, `contacts`, `cases` |

---

## Core entities

### Platform

```
ws_tenant
  id UUID PK
  code VARCHAR UNIQUE
  name VARCHAR
  plan ENUM
  status ENUM
  base_currency CHAR(3)
  database_mode ENUM(SHARED, DEDICATED)
  database_ref VARCHAR NULL
  kyc_status ENUM
  + audit columns
```

### Company (multi-company isolation)

```
ws_company
  id UUID PK
  tenant_id UUID FK → ws_tenant
  code VARCHAR
  legal_name VARCHAR
  trading_name VARCHAR
  status ENUM
  UNIQUE(tenant_id, code)
```

```
ws_company_profile
  id UUID PK
  tenant_id UUID
  company_id UUID FK → ws_company
  registration_number, tax_id, head_office, sector, …
```

### Organisation structure

Hierarchy: **Division → Group → Department → Unit**

```
ws_org_division     (tenant_id, company_id, code, name, head, status)
ws_org_group        (tenant_id, company_id, division_id, code, name, status)
ws_org_department   (tenant_id, company_id, division_id, group_id?, code, name, head, status)
ws_org_unit         (tenant_id, company_id, department_id, code, name, status)
```

### Finance (multi-currency GL)

```
gl_chart_of_account
  id, tenant_id, company_id, code, name, type, currency, parent_id, status
  UNIQUE(tenant_id, company_id, code)

gl_journal_entry
  id, tenant_id, company_id, ref, date, event_date, description, currency,
  // ref format: JE-YYYY-MM-###### (6-digit monthly sequence from batch creation date)
  batch_id, batch_date, journal_group, process_module, process_type,
  // batch_id format: BID-YYYY-MM-##### (5-digit monthly sequence from batch creation date)
  processor, processor_department, processor_unit, company,
  transaction_currency, exchange_rate, recurring, split, split_type, split_count,
  income_code_id, expense_code_id, rev_batch_id, rev_transaction_id,
  validation_status, status, created_by, source_module_id, source_type, source_id
  UNIQUE(tenant_id, company_id, ref)

gl_journal_line
  id, journal_entry_id FK, account_id FK, gl_group_id, description,
  journal_type (Debit|Credit), debit, credit, jnl_amount, jnl_currency,
  txn_amount, txn_currency, converted, exchange_rate,
  sub_ledger, sl_group, sl_account_id, client_name, invoice_number, invoice_date,
  branch_id, cost_centre_id, has_attachment, doc_count, line_order,
  line_role (Income|Contra|null), income_code_id, default_income_gl_id, splits[]

fin_income_transaction
  id, tenant_id, company_id, journal_entry_id FK, transaction_ref, batch_id,
  line_index, income_code_id, default_income_gl_id, account_id,
  total_amount, currency, split, split_type, splits[{cost_centre_id, amount, percentage}],
  status, value_date, description
  — detailed income split / apportionment linked to master journal_entries amount
```

**Posting rule:** sum(debit) = sum(credit) in journal currency before status → `Posted`. Wisdom-parity batches also require ≥2 lines and store Debit/Credit as `journal_type` with `jnl_amount` mapped onto debit/credit columns. **Income Journal:** Income Entries are always Credit (via income code → default income GL); Offset/Contra Entries are always Debit (GL/SL excluding Revenue/Expense accounts). Income split applies only to Income Entries and is stored in `fin_income_transactions` while the full credit remains on `journal_entries`.

### CRM & contacts

```
crm_contact   (tenant_id, company_id, name, type, email, phone, status)
crm_case      (tenant_id, company_id, ref, title, type, status, priority, customer, assignee)
```

### Access & approval

```
acs_category  (tenant_id, code, name, status)
acs_group     (tenant_id, category_id, code, name, privilege, status)

awf_workflow  (tenant_id, code, name, entity_type, levels, status)
awf_request   (tenant_id, company_id, workflow_id, ref, entity_ref, amount, currency, status)
```

### Operations

```
ops_product, ops_subscription, ops_payment, ops_self_service_case
  — all company-scoped
```

---

## Relationships (key)

```mermaid
erDiagram
  TENANT ||--o{ COMPANY : owns
  COMPANY ||--|| COMPANY_PROFILE : has
  COMPANY ||--o{ ORG_DIVISION : contains
  ORG_DIVISION ||--o{ ORG_GROUP : contains
  ORG_GROUP ||--o{ ORG_DEPARTMENT : contains
  ORG_DEPARTMENT ||--o{ ORG_UNIT : contains
  COMPANY ||--o{ CHART_OF_ACCOUNT : maintains
  COMPANY ||--o{ JOURNAL_ENTRY : posts
  JOURNAL_ENTRY ||--|{ JOURNAL_LINE : lines
  JOURNAL_LINE }o--|| CHART_OF_ACCOUNT : account
  TENANT ||--o{ APPROVAL_WORKFLOW : configures
  APPROVAL_WORKFLOW ||--o{ APPROVAL_REQUEST : processes
```

---

## Prototype collection mapping

| Prototype (`store.js`) | DB table | Scope |
|------------------------|----------|-------|
| `tenants` | `ws_tenant` | platform |
| `companies` | `ws_company` | tenant |
| `company_profiles` | `ws_company_profile` | company |
| `org_divisions` | `ws_org_division` | company |
| `org_groups` | `ws_org_group` | company |
| `org_departments` | `ws_org_department` | company |
| `org_units` | `ws_org_unit` | company |
| `chart_of_accounts` | `gl_chart_of_account` | company |
| `journal_entries` | `gl_journal_entry` + lines | company |
| `fin_income_transactions` | `fin_income_transaction` + splits | company |
| `contacts` | `crm_contact` | company |
| `cases` | `crm_case` | company |

Full registry: `v3/js/platform/entity-schema.js`

---

## ID generation (production)

- Use UUID v7 or ULID for sortable, globally unique IDs.
- Prototype uses prefixed strings (`div_ops`, `je_001`) for readability — **do not** use in production.
- Business references (`ref`, `code`) are unique within `(tenant_id, company_id)`.
