Skip to content

Tenancy Domain

Manages platform-level configuration and the multi-tenant hierarchy for the PlayTelly platform: general holds ticketing tenant settings — site branding, legal content pages (privacy policy, T&Cs, FAQ, contact us, refund policy), third-party API credentials (Zoo API for QR scanning, JohorPay for payments), and email server configuration — while Organizations and Workspaces provide the platform-wide tenant, membership, and app-access model used by CoreAPI itself. As of 2026-09-14, general is no longer a single global row — it's one row per workspace, lazily created on first access. Admin writes require the real per-request permission workspace:general-settings:manage on that specific workspace (not a flat SYSADMIN role); the content-page GETs stay public and unauthenticated for the customer app, and still resolve via the single pinned SPATIO_TICKETING_WORKSPACE_ID since ticketcms has no per-request workspace concept.

Handles:

  • General settings — contact email, timezone, environment config
  • Site logo — public image retrieval
  • Legal content pages — privacy policy, T&C, FAQ, contact us, refund policy (multilingual: BM/EN/CN)
  • Integration settings — Zoo API and JohorPay credentials, email server config
  • Organizations — tenant creation, self-signup with approval, membership, roles, app access
  • Workspaces — per-org sub-units for membership, app access, spaces, and channels
  • Teams — org-scoped user groups that can be granted a role on a workspace as a unit
  • Webhook configuration (planned)

Two tenancy models

This domain covers two independent config surfaces that happen to share a package: the legacy ticketing general record documented below (single-row, ticketing-specific), and the platform-wide organization/workspace hierarchy documented on their own pages. They don't reference each other.


Architecture

graph LR
    CustomerApp["Customer App (public reads)"]
    AdminPortal["Admin Portal (workspace:general-settings:manage writes)"]
    CoreAPI["CoreAPI (Tenancy handlers)"]
    DB["Ticketing DB\n(general — one row per workspace)"]

    CustomerApp -->|GET /api/settings/privacyPolicy\n/termsOfService\n/faq etc.| CoreAPI
    AdminPortal -->|GET /PUT /api/settings/:workspaceId/general| CoreAPI
    AdminPortal -->|PUT /api/settings/:workspaceId/integration| CoreAPI
    CoreAPI --> DB

The general table used to be a genuine single-row config store; since 2026-09-14 it's one row per workspace, created lazily with zero-value defaults the first time a workspace's settings are read or written.


Data Model

erDiagram
    General {
        uint generalId
        string organizationId
        string workspaceId
        string contactEmail
        string timeZone
        string envKey
        string envValue
        string privacyPolicyContentEn
        string privacyPolicyContentBm
        string privacyPolicyContentCn
        string termsOfPurchaseContentEn
        string termsOfServiceContentEn
        string faqContentEn
        string contactUsContentEn
        string refundPolicyContentEn
        string zooApiBaseUrl
        string jpGatewayUrl
        string emailHost
        int emailPort
    }

Endpoints

Public (no auth required)

Returns the tenant's site logo image file.

Response 200 — image file with appropriate Content-Type.


GET /api/settings/privacyPolicy

Response 200

{
  "success": true,
  "data": {
    "contentEn": "<html>...",
    "contentBm": "<html>...",
    "contentCn": "<html>...",
    "lastUpdatedDate": "01-06-2026"
  }
}


GET /api/settings/termsOfPurchase

Same shape as privacy policy — terms of purchase multilingual content.


GET /api/settings/termsOfService

Terms of service multilingual content.


GET /api/settings/faq

FAQ multilingual content.


GET /api/settings/contactUs

Contact us multilingual content.


GET /api/settings/refundPolicy

Refund policy multilingual content.


Admin — requires SYSADMIN role

GET /api/settings/general

Get the full general settings record including all API credentials and email config.

Sensitive fields

This response includes jpApiKey, jpAgToken, emailPassword, emailClientSecret, and emailRefreshToken. Never expose this endpoint to non-admin clients.

Response 200 — full General object.


PUT /api/settings/general

Update general settings (non-integration fields: contact email, timezone, logo image upload).

Request — multipart/form-data with optional logo file + JSON fields:

{
  "contactEmail": "support@example.com",
  "timeZone": "Asia/Kuala_Lumpur"
}


PUT /api/settings/privacyPolicy

Update privacy policy content in all three languages.

Request

{
  "contentEn": "<html>Updated privacy policy...</html>",
  "contentBm": "<html>Dasar privasi dikemaskini...</html>",
  "contentCn": "<html>更新的隐私政策...</html>",
  "lastUpdatedDate": "23-06-2026"
}

Same shape applies for PUT /termsOfPurchase, PUT /termsOfService, PUT /faq, PUT /contactUs, PUT /refundPolicy.


PUT /api/settings/integration

Update third-party API credentials and email server config.

Request

{
  "zooApiBaseUrl": "https://zoo-api.example.com",
  "zooQrEndpoint": "/qr/scan",
  "zooTokenEndpoint": "/auth/token",
  "zooTicketEndpoint": "/tickets/validate",
  "zooApiUsername": "zoo_user",
  "zooApiPassword": "zoo_pass",
  "jpGatewayUrl": "https://jp.gateway.com",
  "jpPaymentEndpoint": "/payment",
  "jpRedflowEndpoint": "/redflow",
  "jpBankListEndpoint": "/banks",
  "jpApiKey": "<key>",
  "jpAgToken": "<token>",
  "emailHost": "smtp.gmail.com",
  "emailPort": 587,
  "emailUsername": "noreply@example.com",
  "emailPassword": "<password>",
  "emailFrom": "noreply@example.com",
  "emailUseSsl": true,
  "emailClientId": "<oauth-client-id>",
  "emailClientSecret": "<oauth-client-secret>",
  "emailRefreshToken": "<oauth-refresh-token>"
}


Content Pages

All legal content pages follow the same pattern:

Endpoint GET PUT (SYSADMIN)
/api/settings/privacyPolicy Public read Update content
/api/settings/termsOfPurchase Public read Update content
/api/settings/termsOfService Public read Update content
/api/settings/faq Public read Update content
/api/settings/contactUs Public read Update content
/api/settings/refundPolicy Public read Update content

All content is stored as raw HTML strings and served verbatim. The customer app renders them in a WebView.