Commerce Domain
Handles the full ticketing purchase lifecycle — from order creation through payment gateway integration (JohorPay) to email confirmation and PDF receipt generation. Supports both member and non-member (guest) purchases, paid and free orders.
Handles:
- Order creation — member and guest checkout
- Free order creation — zero-amount tickets
- Payment gateway — JohorPay integration (FPX / bank transfer)
- Payment redirect flow — process → gateway → return callback
- Order management — admin view with pagination and filters
- PDF receipt generation — downloadable ticket PDF
- Refund flow (planned)
- QR code ticket validation (planned — via Zoo API in Tenancy)
Architecture
graph LR
Customer["Customer Browser"]
CoreAPI["CoreAPI"]
JohorPay["JohorPay Gateway"]
DB["Ticketing DB\n(order_ticket_group, order_ticket_info)"]
EmailService["Email Service"]
PDFService["PDF Service"]
Customer -->|POST /api/orderTicketGroup| CoreAPI
CoreAPI --> DB
CoreAPI -->|POST /payment/process| JohorPay
JohorPay -->|GET /payment/return| CoreAPI
CoreAPI -->|send confirmation| EmailService
CoreAPI --> PDFService
Data Model
erDiagram
OrderTicketGroup {
uint orderTicketGroupId
string langChosen
uint ticketGroupId
string custId
string transactionId
string orderNo
string transactionStatus
string bankCurrentStatus
float totalAmount
string buyerName
string buyerEmail
string admitDate
bool isEmailSent
}
OrderTicketInfo {
uint orderTicketInfoId
uint orderTicketGroupId
string itemId
float unitPrice
string itemDesc1
string itemDesc2
string variant
int quantityBought
string admitDate
string encryptedId
}
OrderTicketLog {
uint orderTicketLogId
uint orderTicketGroupId
string performedBy
string type
string title
string message
string date
}
OrderTicketGroup ||--o{ OrderTicketInfo : "contains"
OrderTicketGroup ||--o{ OrderTicketLog : "has logs"
Order Status Values
transactionStatus |
Meaning |
|---|---|
INITIATED |
Order created, payment not yet attempted |
PENDING |
Redirected to gateway, awaiting callback |
SUCCESS |
Payment confirmed |
FAILED |
Payment rejected or expired |
FREE |
Zero-amount order, no payment needed |
Purchase Flow
sequenceDiagram
participant Customer
participant CoreAPI
participant JohorPay
Customer->>CoreAPI: POST /api/orderTicketGroup\n{ ticketGroupId, variants, buyerInfo }
CoreAPI->>CoreAPI: Validate availability + create order (INITIATED)
CoreAPI-->>Customer: 200 { orderNo, billId, totalAmount }
Customer->>CoreAPI: POST /payment/generateToken
CoreAPI->>JohorPay: Fetch rand_key token
JohorPay-->>CoreAPI: { token }
CoreAPI-->>Customer: { token }
Customer->>CoreAPI: POST /payment/process (form POST)
CoreAPI->>JohorPay: Forward form + checksum (SHA-512)
JohorPay-->>Customer: Bank selection page (redirect)
Customer->>JohorPay: Complete bank transfer
JohorPay->>CoreAPI: GET /payment/return?...
CoreAPI->>CoreAPI: Decrypt payload, update order to SUCCESS
CoreAPI->>CoreAPI: Send confirmation email + PDF
CoreAPI-->>Customer: Redirect to /payment/success
Endpoints
Orders (/api)
GET /api/orderTicketGroups
List all orders for the authenticated user. Requires auth.
Query params: page, limit, status
Response 200
[
{
"orderTicketGroupId": 1,
"orderNo": "ORD-20260601-001",
"transactionStatus": "SUCCESS",
"totalAmount": 50.00,
"buyerName": "Ahmad Bin Ali",
"buyerEmail": "ahmad@example.com",
"admitDate": "2026-07-15",
"createdAt": "2026-06-01T10:00:00Z"
}
]
GET /api/orderTicketGroup
Get a single order by ?orderNo=<orderNo> or ?transactionId=<transactionId>.
GET /api/orderNonMemberInquiry
Guest order lookup by ?email=<email>&orderNo=<orderNo>. No auth required.
POST /api/orderTicketGroup
Create a paid order. Requires a valid customer account.
Request
{
"langChosen": "en",
"ticketGroupId": 3,
"custId": "CUST-001",
"buyerName": "Ahmad Bin Ali",
"buyerEmail": "ahmad@example.com",
"admitDate": "2026-07-15",
"productDesc": "Zoo Negara General Admission",
"items": [
{
"itemId": "VAR-001",
"variant": "Adult",
"unitPrice": 25.00,
"quantityBought": 2,
"itemDesc1": "Adult Ticket",
"itemDesc2": "General Admission",
"itemDesc3": ""
}
]
}
Response 200
{
"orderNo": "ORD-20260601-001",
"billId": "BILL-123",
"productId": "PROD-003",
"totalAmount": 50.00,
"msgToken": "<token>",
"transactionId": "<uuid>"
}
POST /api/orderTicketGroup/free
Create a zero-amount order. Skips payment flow and immediately marks as FREE.
Request — same shape as paid order.
Response 200 — order confirmation with PDF ticket attached.
GET /api/orderTicketGroups/management
Paginated order list for admin. Requires ADMIN, SYSADMIN, or MEMBER role.
Query params: page, limit, query, status, startDate, endDate, ticketGroupId
Payment (/payment)
POST /payment/generateToken
Get a JohorPay rand_key token before initiating a payment.
Response 200
{ "success": true, "token": "<rand_key>" }
POST /payment/bankList
Fetch available FPX banks from JohorPay.
Request
{ "mode": "individual" }
mode values: individual (personal FPX) or corporate (business FPX).
Response 200
{ "success": true, "banks": [{ "bankCode": "MBBEMYKL", "bankName": "Maybank" }] }
POST /payment/process
Form POST that forwards to the JohorPay gateway with a SHA-512 checksum. The response is an HTML auto-submit form that redirects the browser to the bank selection page.
Browser redirect only
This endpoint returns HTML, not JSON. It is designed to be submitted by the browser as a form, not called by JavaScript fetch/XHR.
GET /payment/return
JohorPay callback URL. Decrypts the transaction payload, updates the order status, triggers email + PDF, and redirects the customer to /payment/success or /payment/failure.
GET /payment/decrypt
Developer utility — decrypts a raw JohorPay payload without updating any order. Returns the parsed transaction data.
GET /pdf/receipt
Generate and download the ticket PDF for a completed order.
Query params: orderNo=<orderNo>
Response 200 — application/pdf