# Settings Module

**Date:** 2026-05-30  
**Status:** Draft  
**Depends on:** `foundation-auth-rbac`, `system-i18n`, `invoices-adapters`, `billing-module`, `system-communications-notifications`, `calendar-module`  
**Referenced by:** all modules (settings hub)

---

## Overview

Tenant settings hub and user profile. Three top-level areas: (1) Business profile & organization; (2) Integrations hub (app-store visual layout); (3) User profile. Settings are the single place to configure cross-module behavior without diving into each module.

### OS route title

OS and mobile frames MUST title `/profile` and `/settings/profile` "Profile".

Rationale: preserve user-profile deep-link wayfinding inside the settings app frame.

---

## Business Profile (`/settings/business`)

### Business Info

Displayed on invoices, portal, and outbound emails.

Fields:
- Business name (shown on invoices)
- Business type: עוסק מורשה / חברה בע"מ / עוסק פטור
- Business tax ID (ח.פ. / ע.מ.) — shown on tax invoices
- Address (multi-line)
- Logo (R2 upload — shown on invoices + portal)
- Primary email, phone, website
- Support email (defaults to `support@{tenantSlug}.zync.is` — CF Email Routing)

Stored in `tenants` table (`settings JSONB` column). Updated via `PATCH /api/settings/account`.

### Organization Preferences

| Setting | Options | Default |
|---------|---------|---------|
| Idle timer threshold | 5 / 10 / 15 / 30 min | 10 min |
| Auto-pause idle timers | on / off | on |
| Default billing type | fixed / hourly / retainer | hourly |
| Overtime billing | enabled / disabled | disabled |
| Invoice VAT default | 18% (from vat_rates) | current rate |
| Invoice currency | ILS / USD / EUR | ILS |
| Fiscal year start | January / April | January |

### User Management (`/settings/users`)

Table: Name, Email, Role, Status, Last active.

Actions: Invite user, edit role, freeze/unfreeze, remove.

> This is a tenant-admin view of the users in that tenant — distinct from `admin-dashboard.md` which is the system ADMIN view of all tenants.

---

## Locale & Internationalization (`/settings/locale`)

| Setting | Options |
|---------|---------|
| App language | Hebrew / English |
| Country | Israel (only option V1) |
| Date format | DD/MM/YYYY / YYYY-MM-DD |
| Currency display | ₪ symbol / ILS code |
| Week start | Sunday / Monday |

Locale settings stored in `tenants.settings JSONB`; users may override app language in their profile. Country adapter selection affects VAT rates, invoice numbering rules, and legal compliance logic.

---

## Integrations Hub (`/settings/integrations`)

App-store visual layout: grid of integration cards, grouped by category. Each card shows: provider logo, connection status (Connected / Disconnected / Error), last sync timestamp, primary action button.

**Groups:**

### Invoicing

- Morning (Green Invoice) — connect via API key; shows balance, last sync
- iCount — API key
- Rivhit — API key
- Invoice4u — API key
- Easycount — API key

Each card expands to show: connection status, invoice count synced, automation settings, sync logs (last 20 entries).

**Invoice automation settings** (Business+ tier):
- Auto-generate invoice when task reaches status [selector]
- When retainer bank depleted: auto-generate invoice [on/off]
- IL invoice flow: require proforma approval before tax invoice [on/off]
- Default payment terms (days)

### Billing / Payments

- Morning Pay
- Isracard Direct Debit
- Upay
- iCount Pay

Connect flow: enter API credentials → test connection → save encrypted to `adapter_credentials`.

### Communications

- Email: SMTP/POP3 (host, port, username, password, TLS toggle) or Gmail/Outlook OAuth
- Telegram: per-tenant bot setup (bot token → encrypted to `adapter_credentials`, webhook auto-registered at `POST /api/webhooks/telegram/{tenantId}`)
- WhatsApp: Enterprise only (stubbed in V1 — "Coming soon" badge)
- Slack: OAuth app connection (notifications + ticket creation)

### Calendar

- Google Calendar (OAuth) — status, last synced, calendar selection, sync toggle
- Microsoft Outlook (OAuth) — same
- Scheduling: Calendly, Acuity, moCal — API key + booking actions config

### Support (CRM)

- Ticket email routing: shows `support@{tenantSlug}.zync.is` address (read-only — CF Email Routing configured by system admin)
- Auto-create ticket from Telegram: toggle (routes to CRM)
- Auto-create ticket from email: toggle

### Tasks (Deferred — V2)

Task management adapter connections (Trello, Asana, Jira, Monday.com, ClickUp) — stubbed with "Coming soon" in V1.

### Integration Card Detail

Expandable panel per integration:
- Provider name + logo
- Connection status badge: Connected (green) / Error (red) / Disconnected (grey)
- Last sync: timestamp
- Items synced: count
- "Sync now" button → triggers manual sync
- Sync logs: table of last 20 `integration_sync_logs` entries (status, items, error, timestamp)
- "Disconnect" button (clears credentials from `adapter_credentials`, sets status = inactive)

---

## User Profile (`/profile`)

Three tabs:

### Personal Details

- Display name, avatar (authed binding upload — see Avatar Upload below)
- Email — shown with a **[Change email]** link (see Change Email Flow below)
- Phone
- Timezone (affects time entry display, notification delivery timing)

### Security

- Change password (current password verification + new password); `PATCH /api/profile/password`
- Active sessions list (user agent, IP, last seen, revoke session) — backed by `user_sessions` table (spec 122 `session-security`); endpoints: `GET /api/user/sessions`, `DELETE /api/user/sessions/:id` <!-- sessions served by canonical /api/user/sessions (spec 122); profile Security tab consumes it -->
- Two-factor authentication — managed here; full flow in spec 47 (`auth-2fa`): enroll SMS-based 2FA, disable, view/regenerate backup codes

### Notifications

Notification delivery preferences per event type:
<!-- canonical notification event set owned by spec 97 system-communications-notifications; this table mirrors the live contract -->

| Event | In-app | Email |
|-------|--------|-------|
| Invoice paid | ✓ | ✓ |
| Invoice overdue | ✓ | ✓ |
| New lead | ✓ | ✓ |
| Project milestone | ✓ | ✓ |
| Ticket reply | ✓ | ✓ |

Digest frequency: none / daily / weekly (summary instead of individual notifications).

Email toggle: defaults from tenant settings; user can opt out per event type. Stored in `user_preferences` JSONB.

### Avatar Upload

Profile avatars use the private `STORAGE` (R2) bucket via the Worker binding — no presigned S3 URLs and no `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` secrets. Upload and serve are symmetric authed Worker routes.

1. Client `POST /api/profile/avatar` with raw image bytes and `Content-Type: image/png` or `image/jpeg` (PNG/JPG, max 2 MB; client pre-validates).
2. Worker auth-gates (`session.type === 'user'` + tenant), rejects oversize bodies (`Content-Length` > 2 MB → `413`; streamed read also aborts if actual bytes exceed 2 MB), validates declared type and magic bytes (PNG / JPEG), `STORAGE.put` under `avatars/{tenantId}/{userId}/{uuid}.{ext}`, deletes the previous avatar object when replacing, atomically updates `users.avatar_url` to `/api/profile/avatar/{key}`, returns `{ avatar_url }`.
3. Browser and `<Avatar>` load the image via `GET /api/profile/avatar/:key` — same-origin authed proxy with tenant-prefix isolation (`avatars/{tenantId}/…`).

Clear avatar: `PATCH /api/profile` with `avatarUrl: null` (no upload call).

> **Rationale:** bucket is private and prod lacks R2 S3 API credentials; the binding is the robust credential-less pattern, symmetric with serving.

### Change Email Flow

From `/profile` → Personal Details tab, clicking **[Change email]** opens an inline form:

1. User enters new email address + current password (to prevent account takeover via unattended session).
2. `POST /api/profile/email/request` — validates password, checks new email not already in use, sends a verification email to the **new** address with an opaque random token (24h TTL). Also sends a "your email change was requested" notification to the **old** address.
3. User clicks verification link in email → `GET /api/profile/email/verify?token=<token>` → Worker:
   - Hashes received token; compares against `SHA256` stored in `pending_email_token`
   - Validates token not expired (`pending_email_expires_at > now()`), not already used
   - Re-checks new email (`pending_email`) not already registered (TOCTOU: could be taken during 24h window)
   - Updates `users.email` in DB
   <!-- self-hosted auth: email lives only in users.email; no external IdP -->
   - Invalidates all existing sessions except current (email change = security event)
   - Redirects to `/profile?email_changed=1`
4. Profile page shows success banner: "Email updated to new@example.com".

**Failure states:**
- Token expired: "This link expired. Request a new email change from your profile."
- New email already registered: 409 at step 2 — "That email is already in use."
- Password wrong: 401 at step 2 — "Incorrect password."

**Schema delta:**
```sql
ALTER TABLE users ADD COLUMN IF NOT EXISTS pending_email TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS pending_email_token TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS pending_email_expires_at TIMESTAMPTZ;
```

Token stored hashed (`SHA256(token)`) in `pending_email_token`. On verify: compare hash, then clear all three columns.

---

## Data Model

Settings live in two complementary tenant-level stores (the split is by data shape, not by module):

- **`tenants.settings JSONB`** — free-form business profile, locale, and org preferences. Schemaless values that never need referential integrity or DB-level type/CHECK enforcement (business_name, address, logo_url, idle_threshold_min, default_billing_type, …). Owned by THIS module; written via `PATCH /api/settings/*`.
- **`tenant_settings`** (typed table, base owned by `foundation-auth-rbac`) — per-tenant **typed module config**: columns that carry an FK (e.g. `kb_default_space_id`, `contract_default_template_id`) or a CHECK/strong type JSONB cannot enforce. Each module `ALTER TABLE tenant_settings ADD COLUMN IF NOT EXISTS …` for its own settings; no module re-creates the table.

```sql
-- tenant-level free-form prefs (tenants table):
tenants.settings JSONB  -- business profile, locale, org preferences

-- tenant-level typed module config (foundation-owned base; modules add columns):
tenant_settings (id, tenant_id UNIQUE -> tenants(id), created_at, updated_at, <per-module typed cols>)

-- per-user preferences (already in users/user_preferences):
user_preferences.notification_channels JSONB  -- per-event email/telegram toggles
user_preferences.locale, timezone, default_currency, ...

-- integration credentials (already in adapter_credentials):
adapter_credentials (adapter_id, tenant_id, credentials_enc BYTEA, status, last_synced_at, ...)

-- sync logs (already in integration_sync_logs):
integration_sync_logs (adapter_id, tenant_id, status, items_count, error_msg, created_at)
```

Discriminator: a setting needing an FK or DB-enforced CHECK/type → `tenant_settings` column; otherwise → `tenants.settings` JSONB key. Tenant *identity* (tax_id, vat_number, country_code, default_currency) stays as scalar columns on `tenants`.

---

## Permissions

| Action | Required permission |
|--------|-------------------|
| View settings | `settings:read` |
| Edit business profile | `settings:write` |
| Manage users (invite/freeze) | `users:manage` |
| Configure integrations | `settings:write` |
| View own profile | (self) |
| Edit own profile / notifications | (self) |

---

## API Endpoints

> **Rationale:** Tenant business settings are served at `/api/settings/account` (not `/api/settings/business`); all consumers and `accountSettingsRoute` use that path.

```
GET    /api/settings/account               → tenant business info + org prefs
PATCH  /api/settings/account               → update

GET    /api/settings/locale                → tenant locale settings
PATCH  /api/settings/locale                → update

GET    /api/settings/integrations          → list all integration statuses
GET    /api/settings/integrations/:adapterId → detail + sync logs
POST   /api/settings/integrations/:adapterId/connect    → save credentials + test
POST   /api/settings/integrations/:adapterId/sync       → manual sync trigger
DELETE /api/settings/integrations/:adapterId            → disconnect + clear creds

GET    /api/profile                        → current user profile
PATCH  /api/profile                        → update name, avatar, phone, timezone
POST   /api/profile/avatar                 → upload avatar (raw bytes → STORAGE.put → atomic avatar_url update)
GET    /api/profile/avatar/:key            → authed proxy for private-bucket avatar image
PATCH  /api/profile/password               → change password
POST   /api/profile/email/request          → request email change (validates password, sends verification email)
GET    /api/profile/email/verify           → verify email change token (query: ?token=)
GET    /api/user/sessions                  → active sessions (canonical — spec 122 session-security) <!-- sessions served by canonical /api/user/sessions (spec 122); profile Security tab consumes it -->
DELETE /api/user/sessions/:sessionId       → revoke session (canonical — spec 122 session-security)
GET    /api/profile/notifications          → notification preferences
PATCH  /api/profile/notifications          → update preferences
```

---

## Foundation Deltas

**Sessions:** Active session list uses `user_sessions` table (spec 122 `session-security`) — no additional schema changes needed here. The `user_sessions` table already stores `user_agent`, `ip_address`, `last_active_at`.

**Change email:** Add `pending_email TEXT`, `pending_email_token TEXT`, `pending_email_expires_at TIMESTAMPTZ` to `users` table (see Change Email Flow above).

---

## Settings Navigation Manifest

All `/settings/*` routes across all specs. Canonical list used by sidebar navigation builder.

| Route | Spec | Tier | Description |
|-------|------|------|-------------|
| `/settings/business` | spec 25 (this) | All | Business profile, org prefs, user management (API: `/api/settings/account`) |
| `/settings/locale` | spec 25 (this) | All | Language, country, timezone |
| `/settings/plan` | spec 33 | All | Subscription plan, billing, upgrade modal, trial banner |
| `/settings/users` | spec 138 | All | Team members, invitations, roles, pending approvals |
| `/settings/roles` | spec 163 | All | Custom role editor (OWNER/ADMIN only) |
| `/settings/modules` | spec 32 | All | Enable/disable modules, dependency matrix |
| `/settings/invoicing` | spec 125 | All | Invoice number prefix, VAT defaults, payment terms, late fees |
| `/settings/expenses` | spec 61 | All | Default expense category, auto-approve threshold, OCR settings |
| `/settings/ita` | spec 165 | Business+ | ITA e-invoice (Shaba) registration: API credentials, allocation-number threshold |
| `/settings/time-tracking` | spec 169 | All | Time rounding, idle detection threshold, overtime rules |
| `/settings/contractors` | spec 148 | All | Contractor portal toggle, approval policy, withholding defaults |
| `/settings/projects` | spec 151 | All | Project defaults: billing type, rate, currency, time rounding, budget alert channel |
| `/settings/customers` | spec 152 | All | Customer defaults: portal auto-invite, visibility, default payment terms |
| `/settings/contracts` | spec 153 | All | Contract defaults: template, expiry, signing order, e-sig/renewal reminders |
| `/settings/proposals` | spec 96 | All | Proposal defaults: expiry window, auto-reminder |
| `/settings/portal` | spec 136 | All | Customer portal branding, welcome message, module visibility, default section |
| `/settings/products` | spec 85 | All | Product/service library, pricing |
| `/settings/saved-views` | spec 143 | All | Manage saved list filters/views across modules (personal + team-shared) |
| `/settings/integrations` | spec 25 (this) | All | Integrations hub (app-store layout) |
| `/settings/integrations/payments` | spec 49 | All | Payment gateway: Payplus / Cardcom / Stripe credentials |
| `/settings/integrations/invoicing` | spec 127 | Business+ | Invoice adapters: iCount / Morning / Rivhit / Invoice4u / Easycount |
| `/settings/integrations/accounting` | spec 181 | Business+ | Accountant export (Hashavshevet movement file + Form 6111) |
| `/settings/integrations/smtp` | spec 51 | Business+ | Custom From address (Resend DKIM) or custom SMTP relay (Enterprise) |
| `/settings/integrations/telegram` | spec 43 | Business+ | Telegram bot token setup, webhook registration |
| `/settings/integrations/calendar` | spec 113 | Business+ | Calendar OAuth + sync (Google and Outlook providers on one page) |
| `/settings/integrations/webhooks` | spec 27 | White Label | Webhook endpoints, delivery logs |
| `/settings/api-keys` | spec 60 | Business+ | API key management, scopes |
| `/settings/api` | spec 94 | Business+ | API usage gauges, quota limits, overage toggle |
| `/settings/communications` | spec 25 (this) | All | Notification preferences hub |
| `/settings/security` | spec 47 | All | 2FA enrollment, backup codes, enforce-2FA toggle (OWNER/ADMIN) |
| `/settings/sla` | spec 145 | Business+ | SLA policy configuration + escalation rules for support tickets |
| `/settings/crm` | spec 22 | All | CRM settings: lead scoring, pipeline stage config |
| `/settings/email-templates` | spec 66 | Business+ | Customizable HTML email templates per document type |
| `/settings/data` | spec 54 | All | Data export, GDPR deletion |
| `/settings/import` | spec 40 | Business+ | Bulk CSV/XLSX import (customers, invoices, products, time entries); OWNER/ADMIN |
| `/settings/kb` | spec 154 | All | Knowledge Base workspace config: publication workflow, spaces (requires `users:manage`) |
| `/settings/permissions` | spec 121 | Business+ | Field-level permission rules (hide/read-only fields per role); admin only |
| `/settings/white-label` | spec 137 | Enterprise | Custom domain (CNAME) provisioning, white-label branding |
| `/settings/audit-log` | spec 50 | Business+ | Tenant-facing audit log, 90d / 1yr retention |
| `/settings/ai` | spec 44 | Business+ | AI personality, custom system prompt, credit usage bar, extra-usage toggle + spend limit |
| `/profile` | spec 25 (this) | All | Personal profile: name, avatar, phone, timezone |
| `/profile/security` | spec 25 (this) | All | Password change, active sessions, trusted devices |
| `/profile/notifications` | spec 25 (this) | All | Per-channel notification preferences |

> Routes owned by other specs render inside the settings shell from this spec. Each spec owns its API endpoints; this spec owns the sidebar tree and shell layout.

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| Settings split by data shape | `tenants.settings` JSONB for free-form prefs; typed `tenant_settings` table for FK/CHECK-typed module config | JSONB cannot enforce FKs or DB-level types; settings that need referential integrity (e.g. `kb_default_space_id`, `contract_default_template_id`) require real typed columns. Free-form profile/locale prefs stay schemaless in `tenants.settings`. `tenant_settings`' base is owned by `foundation-auth-rbac` (in every tenant's closure), so module ALTERs never hit a missing table. |
| Integration credentials in `adapter_credentials` | Not separate per-integration tables | Unified encrypted store; same pattern for all providers; `adapter_id` string distinguishes them |
| Integrations hub in settings | Not per-module settings pages | Users expect a single place to connect/disconnect all external services; scattered per-module settings = poor discovery |
| Task adapter integrations V2 | Not V1 | Trello/Jira/Asana sync is high-effort, low-priority vs core features; stub with "Coming soon" |
| WhatsApp Enterprise stub | Not V1 | WhatsApp Business API requires Meta approval + per-number provisioning; operational complexity exceeds V1 scope |
| Active sessions = `user_sessions` table | Not `refresh_tokens` | Spec 122 (`session-security`) owns `user_sessions` with `user_agent`, `ip_address`, `last_active_at`; profile page reads from that table |
