Tenancy Domain
Manages platform-level configuration and the multi-tenant hierarchy for the PlayTelly platform: a single general record 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. Only SYSADMIN can write ticketing general settings; most content endpoints are public-readable for the customer app.
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, membership, roles, app access
- Workspaces — per-org sub-units for membership, app access, spaces, and channels
- 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 (SYSADMIN writes)"]
CoreAPI["CoreAPI (Tenancy handlers)"]
DB["Ticketing DB\n(general — single row)"]
CustomerApp -->|GET /api/settings/privacyPolicy\n/termsOfService\n/faq etc.| CoreAPI
AdminPortal -->|GET /PUT /api/settings/general| CoreAPI
AdminPortal -->|PUT /api/settings/integration| CoreAPI
CoreAPI --> DB
The general table is a single-row config store. All endpoints read from or write to this one record.
Data Model
erDiagram
General {
uint generalId
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)
GET /api/settings/general/siteLogo
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.