# 15 — App Settings

## Overview

Editable platform-wide key/value settings. Two surfaces: **contact details / TeliCMI number** (the main settings page), and **app-store / play-store links** (a sub-page).

**Status:** Live

These settings flow into the public apps via OotboAPI endpoints that read the same `app_settings` table — so editing them here affects what end users see in "Contact Us", "Rate Us", "Update App" CTAs.

---

## User Stories

| ID | As a | I want to | So that |
|----|------|-----------|---------|
| SET-01 | Ops (perm 1) | Edit vendor/client contact email + mobile + TeliCMI number | Update support routing |
| SET-02 | Ops | Edit Host & Partner app-store + play-store links | Push users to new app versions |

---

## Screens & Flows

```
┌─────────────────────────────────────┐
│ /setting          settings/index    │
│  - vendor-contact-us-email          │
│  - vendor-contact-us-mobile         │
│  - client-contact-us-email          │
│  - client-contact-us-mobile         │
│  - telicmi-number                   │
└────────────┬────────────────────────┘
             │
             ├─ on render: POST /setting  ──▶ getSetting(keys[])  returns values
             └─ on save:   POST /updateSetting → 5 DB::update rows

┌─────────────────────────────────────┐
│ /setting/app-store  appstore.blade  │
│  - host-appstore-link               │
│  - host-playstore-link              │
│  - partner-appstore-link            │
│  - partner-playstore-link           │
└────────────┬────────────────────────┘
             │
             └─ on save: POST /updateAppLinkSetting → 4 DB::update rows
```

### Routes & Actions

| Route | Method | Handler | Description |
|-------|--------|---------|-------------|
| `/setting` | GET | `SettingController::index()` | Settings home view |
| `/setting` | POST | `SettingController::getSetting()` | Returns current values for given keys (JSON) |
| `/updateSetting` | POST | `SettingController::updateSetting()` | Update 5 keys |
| `/setting/app-store` | GET | `SettingController::appStore()` | App-store sub-page |
| `/updateAppLinkSetting` | POST | `SettingController::updateAppLinkSetting()` | Update 4 keys |
| `/setting/social-link` | GET | `SettingController::socialLink()` | Route exists but handler is commented out — dead |

---

## Data Model

### `app_settings`

```php
// App\Models\Settings
protected $table = 'app_settings';

{
  id:         int,
  app_key:    string,        // e.g. 'vendor-contact-us-email'
  app_value:  string,        // free-text value
}
```

### Known Keys (read/written by VisoAdmin)

| Key | Surface | Used by |
|-----|---------|---------|
| `vendor-contact-us-email` | `/setting` | Vendor app "Contact Us" |
| `vendor-contact-us-mobile` | `/setting` | Vendor app "Contact Us" |
| `client-contact-us-email` | `/setting` | Client app "Contact Us" |
| `client-contact-us-mobile` | `/setting` | Client app "Contact Us" |
| `telicmi-number` | `/setting` | Telephony integration (TeliCMI) for masked-call routing |
| `host-appstore-link` | `/setting/app-store` | Client app store URL |
| `host-playstore-link` | `/setting/app-store` | Client play store URL |
| `partner-appstore-link` | `/setting/app-store` | Vendor app store URL |
| `partner-playstore-link` | `/setting/app-store` | Vendor play store URL |
| `report-list` | (read-only fallback in `getSetting`) | Default report content? |

---

## Validations & Business Rules

| Rule | Detail |
|------|--------|
| Update writes via raw `DB::update` with key interpolated | `"UPDATE app_settings SET app_value='$val' WHERE app_key='$key'"` |
| No validation of email / phone format | Server takes whatever the form posts |
| Keys are server-controlled (controller-defined) | So `$key` cannot be injected from request — only `$val` can contain anything |
| `$val` is interpolated directly into SQL | **SQL injection vector** if value contains single-quote |
| Multiple settings updated in a loop | Each is its own DB call (no transaction) |
| Form `getSetting` accepts comma-separated keys via `?keys=` | Returns subset on demand |

---

## API Endpoints

| Method | Path | Auth | Request | Response | Consumer |
|--------|------|------|---------|----------|----------|
| GET | `/setting` | session + permission 1 | — | HTML | Browser |
| POST | `/setting` | session (AJAX whitelisted) | `keys=key1,key2,...` (optional) | `{ data: { key_with_underscores: value, ... } }` | Settings JS on load |
| POST | `/updateSetting` | session + permission 1 | `hostMail, partnerContact, hostContact, partnerMail, telecmiNumber` | redirect | Form |
| GET | `/setting/app-store` | session + permission 1 | — | HTML | Browser |
| POST | `/updateAppLinkSetting` | session + permission 1 | `ios_link, android_link, partner_ios_link, partner_android_link` | redirect | Form |

---

## Upstream Impact

- **None — `app_settings` is root config.**

---

## Downstream Impact

- **Public Client app** — `host-appstore-link`, `host-playstore-link`, `client-contact-us-*`.
- **Public Vendor app** — `partner-appstore-link`, `partner-playstore-link`, `vendor-contact-us-*`.
- **TeliCMI integration** — `telicmi-number` is the masking-call routing target.
- **Settings cached on app start** — apps may not reflect changes until next launch (depends on OotboAPI caching).

---

## Impact of Changes

| If you change... | Risk to... | Level | Type |
|-----------------|------------|-------|------|
| Adding a new key without exposing it in `updateSetting` keys map | New key is not writable from UI; only via SQL | Medium | UI |
| Renaming a key | Public app reads stop returning a value → display blanks | High | Data |
| Allowing `app_value` to contain `'` | Raw SQL UPDATE breaks | High | Data |
| Removing `getSetting` defaults | Settings JS on load silently fails | Medium | UI |
| Changing column types | All updates fail | High | Data |

---

## Known Issues

- **SQL injection via `$val`** — `"UPDATE app_settings SET app_value='$val' WHERE app_key='$key'"`. The `$val` comes from POST inputs (`$request->hostMail` etc.). A submitted single-quote will break the UPDATE. Replace with bound params. **Fix owed**.
- **`socialLink()` route is registered** (`/setting/social-link`) but the handler is commented out in `SettingController`. Hitting it returns a 500.
- **No transaction around the loop** — partial updates possible if MySQL errors midway.
- **The keys `vendor-contact-us-email` map to form field `hostMail`** — the naming inversion (vendor vs host) is a legacy code-name from when "vendor" was called "host". Easy to get the wrong field updated.
- **Public app caching** — settings changes here may not propagate immediately. Verify cache strategy with OotboAPI.
