CU E-Receipt — API Gateway Integration Guide
สำหรับ: External Developer / System Integrator เวอร์ชัน: 2.2 วันที่: พฤษภาคม 2569 ติดต่อ: thannaphat.j@zenithcomp.co.th
1. ภาพรวมระบบ
ระบบ CU E-Receipt API Gateway ให้ External System สามารถ:
- ดึงรายการโครงการ ที่พร้อมชำระเงินทุกประเภทในเส้นเดียว
- สร้าง Payment Session เพื่อรับ URL สำหรับ redirect ผู้ใช้ไปชำระเงิน
- ส่งรายการย่อย สำหรับแสดงในใบเสร็จ PDF
- รับ Webhook แจ้งผลการชำระเงินแบบ Real-time
- รับเลขใบเสร็จ พร้อม receipt URL สำหรับดาวน์โหลด PDF
การลงทะเบียน: ทำผ่าน Web UI ของ CU E-Receipt เท่านั้น ผู้ดูแลระบบจะตรวจสอบและอนุมัติ จากนั้น
client_id,client_secretและwebhook_secretจะถูกส่งทาง email โดยอัตโนมัติ
Flow Overview
External System CU E-Receipt
───────────────── ────────────────────────────────
[ลงทะเบียนผ่าน Web UI ───────────── อนุมัติโดย Admin]
[รับ credentials ทาง email ◄──────── ส่ง client_id, client_secret, webhook_secret]
══════════════ เปิดใช้งานแล้ว ══════════════
[1] ขอ Access Token ─────────────► POST /oauth/token
◄─── {access_token}
[1.5] ดึงรายการโครงการ ────────────► POST /api/v1/projects {"type":"content"}
(Bearer Token + HMAC Signature) ◄─── [{project_code, project_name, type, amount, ...}]
[2] สร้าง Payment Session ────────► POST /api/v1/payment/create
(Bearer Token + HMAC Signature) ◄─── {payment_url, booking_ref}
[3] Redirect user ────────────────► payment_url (หน้าชำระเงิน)
user ชำระเงิน
[4] ระบบ ──► POST callback_url (Webhook)
{event: payment.success, ref_no, receipt_no, ...}
ref_no= เลขอ้างอิงจาก system ของคุณ — ใช้ track payment ระหว่าง 2 ระบบbooking_ref= เลข booking ของระบบ CU — ใช้อ้างอิงฝั่ง CU
2. การขอ Access Token
ใช้ OAuth 2.0 Client Credentials flow — ขอ token ใหม่ทุกครั้งก่อนเรียก API
POST /oauth/token
POST /oauth/token
Content-Type: application/json
{
"grant_type": "client_credentials",
"client_id": "550e8400-e29b-41d4-a716-446655440000",
"client_secret": "AbCd1234...64chars...",
"scope": "payment:create"
}
Response 200:
{
"token_type": "Bearer",
"expires_in": 1800,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
| Field | Value |
|---|---|
expires_in |
1,800 วินาที = 30 นาที |
| Token type | JWT (RS256) |
| Refresh token | ไม่มี |
วิธีใช้งาน: ขอ Access Token ใหม่ทุกครั้ง ก่อน เรียก
/api/v1/payment/createไม่ต้อง track expiry หรือ cache — แค่ขอแล้วใช้ทันที แล้วทิ้ง
3. ดึงรายการโครงการที่พร้อมชำระเงิน
POST /api/v1/projects
ระบบ auto-filter ตาม fac_code ของ Gateway Client — ไม่ต้องระบุคณะเอง
ใช้ project_code และ type ที่ได้รับจาก response นี้ไปส่งใน POST /api/v1/payment/create (ต้องส่งคู่กันเสมอ)
ทำไมใช้ POST? filter ถูกส่งใน JSON body ซึ่งถูก sign ด้วย HMAC — ป้องกันการแก้ไข filter ระหว่างทาง
Required Headers
| Header | Description |
|---|---|
Authorization |
Bearer {access_token} |
Content-Type |
application/json |
X-Timestamp |
Unix timestamp (วินาที) — ต้องไม่ต่างจากเวลาเซิร์ฟเวอร์เกิน ±5 นาที |
X-Nonce |
Random string ยาว 16–64 chars — ห้ามซ้ำ ต่อ request |
X-Signature |
HMAC-SHA256 signature คำนวณจาก JSON body (ดู Section 6) |
Request Body (ทั้งหมด optional)
| Field | Type | Default | Description |
|---|---|---|---|
search |
string | — | ค้นหาจากชื่อโครงการ |
type |
string | ทุกประเภท | content | health_science | room |
page |
integer | 1 | หน้าที่ต้องการ |
per_page |
integer | 20 | รายการต่อหน้า (สูงสุด 50) |
ถ้าไม่ต้องการ filter ให้ส่ง body เป็น
{}หรือ empty body ("")
ตัวอย่าง Request
POST /api/v1/projects
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
Content-Type: application/json
X-Timestamp: 1741852800
X-Nonce: k9xM2pLnQr7vYwZ3
X-Signature: base64encodedHmacSignature==
{
"type": "content",
"search": "Python",
"page": 1,
"per_page": 20
}
Response 200
{
"status": "success",
"fac_code": "3700",
"data": [
{
"type": "content",
"project_code": "37000001",
"project_name": "อบรม Python Basics รุ่นที่ 3",
"amount": 1700.00,
"faculty": "3700",
"start_date": "2026-01-01 00:00:00",
"end_date": "2026-12-31 00:00:00"
},
{
"type": "health_science",
"project_code": "HS690001",
"project_name": "บริการตรวจสุขภาพพนักงาน",
"amount": "1810.00",
"faculty": "09001",
"start_date": null,
"end_date": null
},
{
"type": "room",
"project_code": "RM690001",
"project_name": "ห้องพักนิสิต A101",
"amount": "3500.00",
"faculty": "0104",
"start_date": null,
"end_date": null
}
],
"pagination": {
"total": 12,
"per_page": 20,
"current_page": 1,
"last_page": 1
}
}
| Field | Description |
|---|---|
fac_code |
รหัสคณะของ Gateway Client (null = ระดับ System Admin) |
data[].type |
ประเภทโครงการ: content, health_science, room — ต้องส่งเป็น type ใน POST /api/v1/payment/create |
data[].project_code |
รหัสโครงการ — ต้องส่งคู่กับ type ใน POST /api/v1/payment/create |
data[].project_name |
ชื่อโครงการ |
data[].amount |
จำนวนเงิน (บาท) |
4. การสร้าง Payment Session
POST /api/v1/payment/create
Required Headers
| Header | Description |
|---|---|
Authorization |
Bearer {access_token} |
Content-Type |
application/json |
X-Timestamp |
Unix timestamp (วินาที) — ต้องไม่ต่างจากเวลาเซิร์ฟเวอร์เกิน ±5 นาที |
X-Nonce |
Random string ยาว 16–64 chars — ห้ามซ้ำ ต่อ request |
X-Signature |
HMAC-SHA256 signature (ดูวิธีคำนวณ Section 6) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
mode |
string | ❌ | โหมดการทำงาน: test (default) หรือ production — ดูหมายเหตุด้านล่าง |
project_code |
string | ✅ | รหัสโครงการ (field project_code จาก POST /api/v1/projects) |
type |
string | ✅ | ประเภทโครงการ (field type จาก POST /api/v1/projects): content | health_science | room |
amount |
number | ⚠️ | จำนวนเงินรวม (บาท) — required เมื่อ type=content หรือ health_science; optional เมื่อ type=room (คำนวณจาก room.unit_price + data อัตโนมัติ) |
payer_name |
string | ⚠️ | ชื่อผู้ชำระ — required เมื่อ type=content หรือ health_science; optional เมื่อ type=room |
payer_email |
string | ⚠️ | อีเมลผู้ชำระ — required เมื่อ type=content หรือ health_science; optional เมื่อ type=room |
payer_phone |
string | ⚠️ | เบอร์โทรผู้ชำระ — required เมื่อ type=content หรือ health_science; optional เมื่อ type=room |
payer_organization |
string | ❌ | ชื่อองค์กรผู้ชำระ |
ref_no |
string | ✅ | เลขอ้างอิงจาก system ของคุณ (จะส่งกลับใน webhook) — สูงสุด 55 อักษร |
callback_url |
string | ⚠️ | URL สำหรับรับ webhook — required เมื่อ mode=production, optional เมื่อ mode=test |
title |
string | ⚠️ | ชื่อหัวข้อรายการในใบเสร็จ — required เมื่อส่ง data (type!=room); optional เมื่อ type=room |
data |
array | ❌ | รายการค่าใช้จ่ายเพิ่มเติม เช่น ค่าน้ำ ค่าไฟ — optional ทุก type (ดูรายละเอียดด้านล่าง) |
room |
object | ⚠️ | ข้อมูลห้องพัก — required เมื่อ type=room (ดูรายละเอียดด้านล่าง) |
receipt_info |
object | ❌ | ข้อมูลผู้รับใบเสร็จ — optional สำหรับ type=room (ดูรายละเอียดด้านล่าง) |
mode— ความแตกต่าง:
mode callback_url การทำงาน test(default)optional ทดสอบ flow ได้โดยไม่ต้องมี callback server productionrequired ใช้งานจริง — ระบบ POST webhook ไปที่ callback_url เมื่อชำระเงินสำเร็จ หมายเหตุ: บน Production server (
ereceipt.ofas.chula.ac.th)callback_urlบังคับ เสมอ ไม่ว่าจะส่งmodeอะไรมา
room Object (required ทุก field เมื่อ type=room)
เมื่อ type=room ต้องส่ง object room พร้อมข้อมูลครบถ้วน
| Field | Type | Required | Description |
|---|---|---|---|
room.building_id |
integer | ✅ | รหัสอาคาร (≥ 1) |
room.floor |
integer | ✅ | ชั้น (≥ 1) |
room.room_id |
integer | ✅ | รหัสห้อง (≥ 1) |
room.booking_type |
string | ✅ | ประเภทการจอง: daily (รายวัน) | monthly (รายเดือน) | term (รายเทอม) |
room.start_date |
string | ✅ | วันที่เริ่มต้น รูปแบบ YYYY-MM-DD เช่น 2026-06-01 |
room.end_date |
string | ✅ | วันที่สิ้นสุด รูปแบบ YYYY-MM-DD — ต้องไม่น้อยกว่า start_date |
room.unit_price |
number | ✅ | ราคาค่าเช่าหลัก (บาท) — ใช้เป็น main item ในใบเสร็จ |
การคำนวณ amount:
total = room.unit_price + sum(data[].quantity × data[].unit_price)หากส่งamountมาด้วย ต้องตรงกับค่าที่คำนวณได้ (tolerance ±0.01 บาท)
receipt_info Object (optional — สำหรับ type=room)
ข้อมูลผู้รับใบเสร็จ สำหรับพิมพ์ในใบเสร็จห้องพัก
| Field | Type | Required | Description |
|---|---|---|---|
receipt_info.is_personal |
boolean | ❌ | true = บุคคลธรรมดา (default), false = นิติบุคคล |
receipt_info.id_card |
string | ❌ | เลขบัตรประชาชน (กรณี is_personal=true) |
receipt_info.tax_number |
string | ❌ | เลขประจำตัวผู้เสียภาษี (กรณี is_personal=false) |
receipt_info.name |
string | ❌ | ชื่อที่ออกใบเสร็จ |
receipt_info.phone |
string | ❌ | เบอร์โทรในใบเสร็จ |
data Array (optional ทุก type)
สำหรับ type=room — ใช้สำหรับ ค่าใช้จ่ายเพิ่มเติม เท่านั้น เช่น ค่าน้ำ ค่าไฟ (ค่าเช่าหลักอยู่ใน room.unit_price)
สำหรับ type=content / type=health_science — ถ้าไม่ส่ง data ระบบจะใช้ชื่อโครงการเป็นรายการเดียวในใบเสร็จ
title ใช้แสดงเป็นหัวข้อหลักเหนือรายการย่อย (required เมื่อส่ง data สำหรับ type!=room)
| Field | Type | Required | Description |
|---|---|---|---|
data[].name |
string | ✅ | ชื่อรายการ เช่น "ค่าน้ำ", "ค่าไฟ" |
data[].quantity |
integer | ✅ | จำนวน (≥ 1) |
data[].unit_price |
number | ✅ | ราคาต่อหน่วย (บาท) |
data[].description |
string | ❌ | รายละเอียดเพิ่มเติม |
type=content/health_science:
sum(quantity × unit_price)ของ data ต้องตรงกับamount(tolerance ±0.01 บาท)
ตัวอย่าง Request — type=content
POST /api/v1/payment/create
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
Content-Type: application/json
X-Timestamp: 1741852800
X-Nonce: k9xM2pLnQr7vYwZ3
X-Signature: base64encodedHmacSignature==
{
"project_code": "37000001",
"type": "content",
"amount": 1700.00,
"payer_name": "สมชาย ใจดี",
"payer_email": "somchai@example.com",
"payer_phone": "0891234567",
"ref_no": "YOUR-SYS-REF-001",
"title": "อบรม Python Basics รุ่นที่ 3",
"data": [
{
"name": "ค่าลงทะเบียน",
"quantity": 1,
"unit_price": 1500.00,
"description": "หลักสูตรสำหรับผู้เริ่มต้น"
},
{
"name": "ค่าเอกสารประกอบ",
"quantity": 2,
"unit_price": 100.00
}
]
}
ตัวอย่าง Request — type=room
POST /api/v1/payment/create
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
Content-Type: application/json
X-Timestamp: 1741852800
X-Nonce: k9xM2pLnQr7vYwZ3
X-Signature: base64encodedHmacSignature==
{
"mode": "production",
"project_code": "0104001",
"type": "room",
"ref_no": "YOUR-SYS-REF-002",
"callback_url": "https://your-system.example.com/callback",
"room": {
"building_id": 1,
"floor": 3,
"room_id": 42,
"booking_type": "monthly",
"start_date": "2026-06-01",
"end_date": "2026-06-30",
"unit_price": 3000.00
},
"receipt_info": {
"is_personal": true,
"id_card": "1100100100001",
"name": "นายสมชาย ใจดี",
"phone": "0891234567"
},
"title": "ค่าเช่าห้องพัก เดือนมิถุนายน 2569",
"data": [
{
"name": "ค่าน้ำ",
"quantity": 1,
"unit_price": 200.00
},
{
"name": "ค่าไฟ",
"quantity": 1,
"unit_price": 350.00
}
]
}
หมายเหตุ: ตัวอย่างนี้ไม่ส่ง
amount— ระบบคำนวณให้อัตโนมัติ:3000 + 200 + 350 = 3550 บาทpayer_name,payer_email,payer_phoneเป็น optional เมื่อ type=room
Response 201:
{
"status": "success",
"payment_url": "https://ereceipt.ofas.chula.ac.th/project/payment/aB3xK9mP...",
"booking_ref": "BK-690001",
"expires_at": "2026-03-13T16:30:00+07:00"
}
| Field | Description |
|---|---|
mode |
โหมดที่ใช้งาน (test หรือ production) |
payment_url |
URL สำหรับ redirect user ไปชำระเงิน |
booking_ref |
เลขอ้างอิง booking ของระบบ CU |
expires_at |
Payment URL หมดอายุ (30 นาที) |
การใช้งาน: Redirect user ไปที่
payment_urlทันที User จะเห็นหน้าชำระเงินพร้อมข้อมูล pre-filled
5. การรับ Webhook Callback
หลังจาก user ชำระเงินสำเร็จ ระบบจะ POST ไปที่ callback_url ของคุณ
Webhook Payload
POST https://your-system.example.com/payment/callback
Content-Type: application/json
X-Timestamp: 1741852900
X-Signature: a3f8c2d1e4b7... (hex string — ดู Section 7)
{
"event": "payment.success",
"booking_ref": "BK-690001",
"ref_no": "YOUR-SYS-REF-001",
"receipt_no": "CU37690001",
"amount": 1700.00,
"paid_at": "2026-03-13T15:45:00+07:00",
"receipt_url": "https://ereceipt.ofas.chula.ac.th/project/payment/success/aB3xK9..."
}
| Field | Description |
|---|---|
event |
"payment.success" เสมอ |
booking_ref |
เลข booking ของระบบ CU |
ref_no |
เลขอ้างอิงที่คุณส่งมาตอน create session — ใช้ track payment ใน system ของคุณ |
receipt_no |
เลขใบเสร็จ เช่น CU37690001 |
amount |
จำนวนเงิน (บาท) |
paid_at |
เวลาชำระ (ISO 8601, Asia/Bangkok) |
receipt_url |
URL สำหรับดูใบเสร็จ (HTML + print) |
การ track ระหว่าง 2 ระบบ: ใช้
ref_noที่คุณส่งมาตอน create session เพื่อ map กับ order ใน system ของคุณ
Response ที่ Server คุณต้องตอบกลับ
HTTP 200 OK
ถ้า server ตอบ non-2xx ระบบจะ retry อัตโนมัติ: 1 นาที → 5 นาที → 30 นาที (รวม 4 ครั้ง)
6. HMAC Signature — Inbound (คำนวณและส่งมาให้เรา)
ทุก request ไปที่ /api/v1/* ต้องมี X-Signature
วิธีคำนวณ
message = client_id + "\n" + timestamp + "\n" + nonce + "\n" + sha256(body)
signature = base64( HMAC-SHA256(message, client_secret) )
หมายเหตุ:
client_id— UUID ที่ได้รับทาง email (lowercase)client_secret— plain text secret ที่ได้รับทาง email (ไม่ใช่ค่า bcrypt)timestamp— ค่าเดียวกับX-Timestampheadernonce— ค่าเดียวกับX-Nonceheadersha256(body)— SHA-256 hash ของ raw request body (hex string, lowercase)- ส่ง body
{}(empty JSON object) หรือ""(empty string) →sha256ค่าต่างกัน ให้ใช้ค่าจริงที่ส่งไป \n— newline character (LF,0x0A)
ตัวอย่าง (Python)
import hashlib
import hmac
import base64
import json
import time
import uuid
import requests
client_id = "550e8400-e29b-41d4-a716-446655440000"
client_secret = "AbCd1234EfGh5678..." # plain text จาก email
def create_signature(body_str: str, timestamp: int, nonce: str) -> str:
"""body_str = json.dumps(payload) สำหรับ POST หรือ '' สำหรับ GET"""
body_hash = hashlib.sha256(body_str.encode('utf-8')).hexdigest()
message = "\n".join([client_id, str(timestamp), nonce, body_hash])
sig = hmac.new(
client_secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).digest()
return base64.b64encode(sig).decode('utf-8')
# ─── Create Payment Session ───────────────────────────────────────────────────
payload = {
"project_code": "37000001",
"type": "content",
"amount": 1700.00,
"payer_name": "สมชาย ใจดี",
"payer_email": "somchai@example.com",
"ref_no": "YOUR-SYS-REF-001",
"title": "อบรม Python Basics รุ่นที่ 3",
"data": [
{"name": "ค่าลงทะเบียน", "quantity": 1, "unit_price": 1500.00},
{"name": "ค่าเอกสารประกอบ", "quantity": 2, "unit_price": 100.00}
]
}
timestamp = int(time.time())
nonce = uuid.uuid4().hex[:16]
body_str = json.dumps(payload, separators=(',', ':'), ensure_ascii=False)
signature = create_signature(body_str, timestamp, nonce)
response = requests.post(
"https://ereceipt.ofas.chula.ac.th/api/v1/payment/create",
data=body_str.encode('utf-8'),
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
"X-Timestamp": str(timestamp),
"X-Nonce": nonce,
"X-Signature": signature,
}
)
print(response.json())
ตัวอย่าง (PHP)
<?php
$clientId = '550e8400-e29b-41d4-a716-446655440000';
$clientSecret = 'AbCd1234EfGh5678...'; // plain text จาก email
/**
* $bodyJson = json_encode($payload) สำหรับ POST หรือ '' สำหรับ GET
*/
function createSignature(string $bodyJson, int $timestamp, string $nonce, string $clientId, string $clientSecret): string
{
$bodyHash = hash('sha256', $bodyJson);
$message = implode("\n", [$clientId, $timestamp, $nonce, $bodyHash]);
$rawHmac = hash_hmac('sha256', $message, $clientSecret, true);
return base64_encode($rawHmac);
}
$payload = [
'project_code' => '37000001',
'type' => 'content',
'amount' => 1700.00,
'payer_name' => 'สมชาย ใจดี',
'payer_email' => 'somchai@example.com',
'ref_no' => 'YOUR-SYS-REF-001',
'title' => 'อบรม Python Basics รุ่นที่ 3',
'data' => [
['name' => 'ค่าลงทะเบียน', 'quantity' => 1, 'unit_price' => 1500.00],
['name' => 'ค่าเอกสารประกอบ', 'quantity' => 2, 'unit_price' => 100.00],
],
];
$timestamp = time();
$nonce = bin2hex(random_bytes(8)); // 16 hex chars
$bodyJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$signature = createSignature($bodyJson, $timestamp, $nonce, $clientId, $clientSecret);
$ch = curl_init('https://ereceipt.ofas.chula.ac.th/api/v1/payment/create');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $bodyJson,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $accessToken,
'Content-Type: application/json',
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . $signature,
],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($response);
ตัวอย่าง (Node.js)
const crypto = require('crypto');
const axios = require('axios');
const clientId = '550e8400-e29b-41d4-a716-446655440000';
const clientSecret = 'AbCd1234EfGh5678...'; // plain text จาก email
/**
* bodyStr = JSON.stringify(payload) สำหรับ POST หรือ '' สำหรับ GET
*/
function createSignature(bodyStr, timestamp, nonce) {
const bodyHash = crypto.createHash('sha256').update(bodyStr, 'utf8').digest('hex');
const message = [clientId, timestamp, nonce, bodyHash].join('\n');
const hmac = crypto.createHmac('sha256', clientSecret).update(message, 'utf8').digest();
return Buffer.from(hmac).toString('base64');
}
const payload = {
project_code: '37000001',
type: 'content',
amount: 1700.00,
payer_name: 'สมชาย ใจดี',
payer_email: 'somchai@example.com',
ref_no: 'YOUR-SYS-REF-001',
title: 'อบรม Python Basics รุ่นที่ 3',
data: [
{ name: 'ค่าลงทะเบียน', quantity: 1, unit_price: 1500.00 },
{ name: 'ค่าเอกสารประกอบ', quantity: 2, unit_price: 100.00 },
],
};
const timestamp = Math.floor(Date.now() / 1000);
const nonce = crypto.randomBytes(8).toString('hex');
const bodyStr = JSON.stringify(payload);
const signature = createSignature(bodyStr, timestamp, nonce);
const response = await axios.post(
'https://ereceipt.ofas.chula.ac.th/api/v1/payment/create',
bodyStr,
{
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-Timestamp': String(timestamp),
'X-Nonce': nonce,
'X-Signature': signature,
},
}
);
console.log(response.data);
7. HMAC Signature — Outbound (verify callback ที่รับจากเรา)
เมื่อระบบส่ง Webhook มาที่ callback_url ของคุณ คุณต้อง verify signature เพื่อยืนยันว่า request มาจากระบบจริง
วิธี Verify
signature = hex( HMAC-SHA256(timestamp + "." + body, webhook_secret) )
หมายเหตุ:
webhook_secret— รับทาง email ตอนได้รับ credentialstimestamp— มาจากX-Timestampheader ของ webhook requestbody— raw JSON string ของ webhook payload- ผลลัพธ์เป็น hex string (ไม่ใช่ base64)
ตัวอย่าง (Python — Flask)
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "xK9mP2..." # จาก email
@app.route('/payment/callback', methods=['POST'])
def payment_callback():
timestamp = request.headers.get('X-Timestamp', '')
received_sig = request.headers.get('X-Signature', '')
body = request.get_data() # raw bytes
message = f"{timestamp}.{body.decode('utf-8')}"
expected_sig = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_sig, received_sig):
return jsonify({'error': 'Invalid signature'}), 401
data = request.get_json()
if data.get('event') == 'payment.success':
ref_no = data['ref_no'] # เลขอ้างอิงของคุณ
receipt_no = data['receipt_no']
amount = data['amount']
# อัปเดต order ใน system ของคุณโดยใช้ ref_no...
return jsonify({'status': 'ok'}), 200
ตัวอย่าง (PHP)
<?php
$webhookSecret = 'xK9mP2...'; // จาก email
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$receivedSig = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
$message = $timestamp . '.' . $body;
$expectedSig = hash_hmac('sha256', $message, $webhookSecret);
if (!hash_equals($expectedSig, $receivedSig)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$data = json_decode($body, true);
if ($data['event'] === 'payment.success') {
$refNo = $data['ref_no']; // เลขอ้างอิงของคุณ
$receiptNo = $data['receipt_no'];
$amount = $data['amount'];
// อัปเดต order ใน system ของคุณโดยใช้ $refNo...
}
http_response_code(200);
echo json_encode(['status' => 'ok']);
ตัวอย่าง (Node.js — Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = 'xK9mP2...'; // จาก email
app.post('/payment/callback', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.headers['x-timestamp'] || '';
const receivedSig = req.headers['x-signature'] || '';
const body = req.body.toString('utf-8');
const message = `${timestamp}.${body}`;
const expectedSig = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(message, 'utf-8')
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(expectedSig, 'utf-8'),
Buffer.from(receivedSig, 'utf-8')
)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const data = JSON.parse(body);
if (data.event === 'payment.success') {
const { ref_no, receipt_no, amount } = data;
// อัปเดต order ใน system ของคุณโดยใช้ ref_no...
}
res.status(200).json({ status: 'ok' });
});
8. Error Codes
HTTP Status Codes
| Status | ความหมาย |
|---|---|
200 |
สำเร็จ |
201 |
สร้างสำเร็จ |
401 |
Unauthorized — token ไม่ถูกต้อง / signature ไม่ตรง / timestamp เกิน ±5 นาที / nonce ซ้ำ |
403 |
Forbidden — Gateway ถูกระงับหรือยังไม่ผ่านการอนุมัติ |
404 |
ไม่พบ resource ที่ขอ |
422 |
ข้อมูลไม่ถูกต้อง — ดูรายละเอียดใน errors field |
429 |
Too Many Requests — Rate limit เกิน (30 req/นาที) |
500 |
เกิดข้อผิดพลาดระบบ |
Error Response Format
{
"status": "error",
"message": "ข้อผิดพลาดหลัก",
"errors": {
"field_name": ["รายละเอียดข้อผิดพลาด"]
}
}
ข้อผิดพลาดที่พบบ่อย
| Message | สาเหตุ | วิธีแก้ |
|---|---|---|
Missing required security headers |
ขาด X-Timestamp, X-Nonce, หรือ X-Signature |
ตรวจสอบ headers ครบหรือไม่ |
Request timestamp is too old |
เวลาเซิร์ฟเวอร์ต่างกันเกิน 5 นาที | sync NTP ให้เวลาตรง |
Nonce has already been used |
ส่ง nonce ซ้ำ | ใช้ nonce ใหม่ทุก request |
Invalid signature |
คำนวณ HMAC ผิด | ตรวจสอบลำดับ message, client_secret, UUID lowercase |
X-Nonce must be 16-64 characters |
nonce สั้น/ยาวเกิน | ใช้ 16–64 chars |
กรุณาระบุรหัสโครงการ |
ไม่ส่ง project_code |
ส่ง project_code ที่ได้จาก POST /api/v1/projects |
กรุณาระบุประเภทโครงการ |
ไม่ส่ง type |
ส่ง type ที่ได้จาก POST /api/v1/projects (content, health_science, หรือ room) |
กรุณาระบุ callback_url เมื่อใช้ mode=production |
ส่ง mode=production แต่ไม่มี callback_url |
ใส่ callback_url ที่เป็น HTTPS endpoint ของคุณ |
กรุณาระบุ title เมื่อส่ง data |
ส่ง data แต่ไม่มี title |
ใส่ title เป็นชื่อหัวข้อรายการ |
ผลรวมรายการ data ไม่ตรงกับ amount |
sum ของ data ≠ amount | ตรวจสอบ quantity × unit_price ทุก item |
9. ตัวอย่างโค้ด Full Flow
หลักการ: ขอ Access Token ใหม่ทุกครั้งก่อนเรียก API — ไม่ต้อง cache Flow: ขอ Token → ดึงรายการโครงการ → สร้าง Payment Session → Redirect user
Full Flow — Python
import hashlib
import hmac
import base64
import json
import time
import uuid
import requests
BASE_URL = "https://ereceipt.ofas.chula.ac.th"
CLIENT_ID = "550e8400-e29b-41d4-a716-446655440000"
CLIENT_SECRET = "AbCd1234..." # plain text จาก email
def _get_access_token() -> str:
resp = requests.post(
f"{BASE_URL}/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": "payment:create",
},
timeout=10,
)
resp.raise_for_status()
return resp.json()["access_token"]
def _sign(body_str: str, timestamp: int, nonce: str) -> str:
"""body_str = json.dumps(payload) สำหรับ POST หรือ '' สำหรับ GET"""
body_hash = hashlib.sha256(body_str.encode("utf-8")).hexdigest()
message = "\n".join([CLIENT_ID, str(timestamp), nonce, body_hash])
raw_hmac = hmac.new(
CLIENT_SECRET.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).digest()
return base64.b64encode(raw_hmac).decode("utf-8")
def list_projects(access_token: str, search: str = "", type_filter: str = "") -> list:
"""ดึงรายการโครงการ — POST body (filter ถูก sign ด้วย HMAC)"""
payload = {}
if search: payload["search"] = search
if type_filter: payload["type"] = type_filter
timestamp = int(time.time())
nonce = uuid.uuid4().hex[:16]
body_str = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
signature = _sign(body_str, timestamp, nonce)
resp = requests.post(
f"{BASE_URL}/api/v1/projects",
data=body_str.encode("utf-8"),
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
"X-Timestamp": str(timestamp),
"X-Nonce": nonce,
"X-Signature": signature,
},
timeout=10,
)
resp.raise_for_status()
return resp.json()["data"]
def create_payment_session(
access_token: str,
project_code: str,
project_type: str,
amount: float,
ref_no: str,
payer_name: str = "",
payer_email: str = "",
payer_phone: str = "",
data: dict = None,
) -> dict:
payload = {"project_code": project_code, "type": project_type, "amount": amount, "ref_no": ref_no}
if payer_name: payload["payer_name"] = payer_name
if payer_email: payload["payer_email"] = payer_email
if payer_phone: payload["payer_phone"] = payer_phone
if data: payload["data"] = data
timestamp = int(time.time())
nonce = uuid.uuid4().hex[:16]
body_str = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
signature = _sign(body_str, timestamp, nonce)
resp = requests.post(
f"{BASE_URL}/api/v1/payment/create",
data=body_str.encode("utf-8"),
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
"X-Timestamp": str(timestamp),
"X-Nonce": nonce,
"X-Signature": signature,
},
timeout=15,
)
resp.raise_for_status()
return resp.json()
# ─── ตัวอย่างการใช้งาน ─────────────────────────────────────────────────────
access_token = _get_access_token()
# Step 1.5: ดึงรายการโครงการ
projects = list_projects(access_token, type_filter="content")
project = next(p for p in projects if p["project_name"] == "อบรม Python Basics รุ่นที่ 3")
project_code = project["project_code"] # เช่น "37000001"
project_type = project["type"] # เช่น "content"
# Step 2: สร้าง Payment Session
result = create_payment_session(
access_token = access_token,
project_code = project_code,
project_type = project_type,
amount = 1700.00,
ref_no = "ORDER-001",
payer_name = "สมชาย ใจดี",
payer_email = "somchai@example.com",
payer_phone = "0891234567",
title = "อบรม Python Basics รุ่นที่ 3",
data = [
{"name": "ค่าลงทะเบียน", "quantity": 1, "unit_price": 1500.00},
{"name": "ค่าเอกสารประกอบ", "quantity": 2, "unit_price": 100.00},
],
)
print("Payment URL:", result["payment_url"])
print("Booking Ref:", result["booking_ref"])
# → Redirect user ไปที่ result["payment_url"]
Full Flow — PHP
<?php
const BASE_URL = 'https://ereceipt.ofas.chula.ac.th';
const CLIENT_ID = '550e8400-e29b-41d4-a716-446655440000';
const CLIENT_SECRET = 'AbCd1234...'; // plain text จาก email
function getAccessToken(): string
{
$ch = curl_init(BASE_URL . '/oauth/token');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'grant_type' => 'client_credentials',
'client_id' => CLIENT_ID,
'client_secret' => CLIENT_SECRET,
'scope' => 'payment:create',
]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
curl_close($ch);
$data = json_decode($body, true);
if (empty($data['access_token'])) {
throw new \RuntimeException('Failed to get access token');
}
return $data['access_token'];
}
/**
* $bodyJson = json_encode($payload) สำหรับ POST หรือ '' สำหรับ GET
*/
function createSignature(string $bodyJson, int $timestamp, string $nonce): string
{
$bodyHash = hash('sha256', $bodyJson);
$message = implode("\n", [CLIENT_ID, $timestamp, $nonce, $bodyHash]);
$rawHmac = hash_hmac('sha256', $message, CLIENT_SECRET, true);
return base64_encode($rawHmac);
}
function listProjects(string $accessToken, string $typeFilter = ''): array
{
$payload = $typeFilter ? ['type' => $typeFilter] : [];
$bodyJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$timestamp = time();
$nonce = bin2hex(random_bytes(8));
$signature = createSignature($bodyJson, $timestamp, $nonce);
$ch = curl_init(BASE_URL . '/api/v1/projects');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $bodyJson,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $accessToken,
'Content-Type: application/json',
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . $signature,
],
CURLOPT_TIMEOUT => 10,
]);
$body = curl_exec($ch);
curl_close($ch);
return json_decode($body, true)['data'] ?? [];
}
function createPaymentSession(
string $accessToken,
string $projectCode,
string $projectType,
float $amount,
string $refNo,
string $payerName = '',
string $payerEmail = '',
string $payerPhone = '',
?array $data = null
): array {
$payload = ['project_code' => $projectCode, 'type' => $projectType, 'amount' => $amount, 'ref_no' => $refNo];
if ($payerName) $payload['payer_name'] = $payerName;
if ($payerEmail) $payload['payer_email'] = $payerEmail;
if ($payerPhone) $payload['payer_phone'] = $payerPhone;
if ($data) $payload['data'] = $data;
$timestamp = time();
$nonce = bin2hex(random_bytes(8));
$bodyJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$signature = createSignature($bodyJson, $timestamp, $nonce);
$ch = curl_init(BASE_URL . '/api/v1/payment/create');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $bodyJson,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $accessToken,
'Content-Type: application/json',
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . $signature,
],
CURLOPT_TIMEOUT => 15,
]);
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($body, true);
if ($httpCode !== 201 || ($result['status'] ?? '') !== 'success') {
throw new \RuntimeException('Payment session failed: ' . $body);
}
return $result;
}
// ─── ตัวอย่างการใช้งาน ─────────────────────────────────────────────────────
$accessToken = getAccessToken();
// Step 1.5: ดึงรายการโครงการ
$projects = listProjects($accessToken, 'content');
$projectCode = $projects[0]['project_code'] ?? '37000001'; // เลือกโครงการที่ต้องการ
$projectType = $projects[0]['type'] ?? 'content';
// Step 2: สร้าง Payment Session
$result = createPaymentSession(
accessToken: $accessToken,
projectCode: $projectCode,
projectType: $projectType,
amount: 1700.00,
refNo: 'ORDER-001',
payerName: 'สมชาย ใจดี',
payerEmail: 'somchai@example.com',
payerPhone: '0891234567',
title: 'อบรม Python Basics รุ่นที่ 3',
data: [
['name' => 'ค่าลงทะเบียน', 'quantity' => 1, 'unit_price' => 1500.00],
['name' => 'ค่าเอกสารประกอบ', 'quantity' => 2, 'unit_price' => 100.00],
]
);
echo 'Payment URL: ' . $result['payment_url'] . PHP_EOL;
// header('Location: ' . $result['payment_url']);
Full Flow — Node.js
const crypto = require('crypto');
const https = require('https');
const BASE_URL = 'https://ereceipt.ofas.chula.ac.th';
const CLIENT_ID = '550e8400-e29b-41d4-a716-446655440000';
const CLIENT_SECRET = 'AbCd1234...'; // plain text จาก email
function postJson(url, body, headers = {}) {
return new Promise((resolve, reject) => {
const bodyStr = JSON.stringify(body);
const parsed = new URL(url);
const options = {
hostname: parsed.hostname,
port: parsed.port || 443,
path: parsed.pathname,
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr), ...headers },
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => resolve({ status: res.statusCode, data: JSON.parse(data) }));
});
req.on('error', reject);
req.setTimeout(15000, () => req.destroy(new Error('timeout')));
req.write(bodyStr);
req.end();
});
}
async function getAccessToken() {
const { data } = await postJson(`${BASE_URL}/oauth/token`, {
grant_type: 'client_credentials', client_id: CLIENT_ID,
client_secret: CLIENT_SECRET, scope: 'payment:create',
});
if (!data.access_token) throw new Error('Token failed');
return data.access_token;
}
function createSignature(bodyStr, timestamp, nonce) {
const bodyHash = crypto.createHash('sha256').update(bodyStr, 'utf8').digest('hex');
const message = [CLIENT_ID, timestamp, nonce, bodyHash].join('\n');
const rawHmac = crypto.createHmac('sha256', CLIENT_SECRET).update(message, 'utf8').digest();
return Buffer.from(rawHmac).toString('base64');
}
async function listProjects(accessToken, typeFilter = '') {
const payload = typeFilter ? { type: typeFilter } : {};
const bodyStr = JSON.stringify(payload);
const timestamp = Math.floor(Date.now() / 1000);
const nonce = crypto.randomBytes(8).toString('hex');
const signature = createSignature(bodyStr, timestamp, nonce);
const { data } = await postJson(`${BASE_URL}/api/v1/projects`, payload, {
'Authorization': `Bearer ${accessToken}`,
'X-Timestamp': String(timestamp),
'X-Nonce': nonce,
'X-Signature': signature,
});
return data.data || [];
}
async function createPaymentSession({ accessToken, projectCode, projectType, amount, refNo, payerName, payerEmail, payerPhone, data }) {
const payload = { project_code: projectCode, type: projectType, amount, ref_no: refNo };
if (payerName) payload.payer_name = payerName;
if (payerEmail) payload.payer_email = payerEmail;
if (payerPhone) payload.payer_phone = payerPhone;
if (data) payload.data = data;
const timestamp = Math.floor(Date.now() / 1000);
const nonce = crypto.randomBytes(8).toString('hex');
const bodyStr = JSON.stringify(payload);
const signature = createSignature(bodyStr, timestamp, nonce);
const { status, data: result } = await postJson(
`${BASE_URL}/api/v1/payment/create`, payload,
{ Authorization: `Bearer ${accessToken}`, 'X-Timestamp': String(timestamp), 'X-Nonce': nonce, 'X-Signature': signature }
);
if (status !== 201 || result.status !== 'success') {
throw new Error(`Failed (${status}): ${JSON.stringify(result)}`);
}
return result;
}
// ─── ตัวอย่างการใช้งาน ───────────────────────────────────────────────────────
(async () => {
const accessToken = await getAccessToken();
// Step 1.5: ดึงรายการโครงการ
const projects = await listProjects(accessToken, 'content');
const projectCode = projects[0]?.project_code ?? '37000001'; // เลือกโครงการที่ต้องการ
const projectType = projects[0]?.type ?? 'content';
// Step 2: สร้าง Payment Session
const result = await createPaymentSession({
accessToken,
projectCode,
projectType,
amount: 1700.00,
refNo: 'ORDER-001',
payerName: 'สมชาย ใจดี',
payerEmail: 'somchai@example.com',
payerPhone: '0891234567',
title: 'อบรม Python Basics รุ่นที่ 3',
data: [
{ name: 'ค่าลงทะเบียน', quantity: 1, unit_price: 1500.00 },
{ name: 'ค่าเอกสารประกอบ', quantity: 2, unit_price: 100.00 },
],
});
console.log('Payment URL:', result.payment_url);
// res.redirect(result.payment_url);
})();
10. Environment & Base URL
| Environment | Base URL |
|---|---|
| UAT / Staging | https://ereceipt.pay.ofas.chula.ac.th |
| Production | https://ereceipt.ofas.chula.ac.th |
Endpoints สรุป
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /oauth/token |
Client Credentials | ขอ Access Token |
| POST | /api/v1/projects |
Bearer + HMAC | ดึงรายการโครงการทุกประเภท |
| POST | /api/v1/payment/create |
Bearer + HMAC | สร้าง Payment Session |
Rate Limits
| Endpoint group | Limit |
|---|---|
/api/v1/* |
30 req/นาที |
11. FAQ
Q: mode=test กับ mode=production ต่างกันอย่างไร?
A: mode=test (default) — ทดสอบ flow ได้ callback_url เป็น optional ไม่ต้องมี callback server; mode=production — ใช้งานจริง callback_url required ระบบจะ POST webhook ไปที่ callback_url เมื่อชำระเงินสำเร็จ
Q: ได้รับ credentials อย่างไร?
A: ลงทะเบียนผ่าน Web UI ของ CU E-Receipt → ผู้ดูแลระบบตรวจสอบและอนุมัติ → ระบบส่ง client_id, client_secret, webhook_secret ทาง email โดยอัตโนมัติ
Q: Access Token หมดอายุต้องทำอย่างไร?
A: ขอใหม่ด้วย POST /oauth/token ด้วย credentials เดิมได้เลย ไม่มี refresh token
Q: POST /api/v1/projects คืนโครงการของคณะไหน?
A: ระบบ filter โครงการ content ตาม fac_code ของ Gateway Client อัตโนมัติ โครงการ health_science และ room คืนทั้งหมด (ไม่ filter ตามคณะ)
Q: project_code ของโครงการแต่ละประเภทมีรูปแบบอย่างไร?
A: content = รหัสที่คณะกำหนด (เช่น 37000001); health_science = integer ID ของโครงการ (เช่น 1, 2); room = รหัสห้องที่กำหนด (เช่น RM690001) — ใช้ค่าที่ได้จาก POST /api/v1/projects เสมอ อย่า hardcode
Q: ต้องส่ง type ใน POST /api/v1/payment/create ด้วยไหม?
A: ต้องส่งเสมอ — type และ project_code ต้องส่งคู่กัน ใช้ค่า type ที่ได้จาก POST /api/v1/projects ตรงๆ (content, health_science, หรือ room)
Q: data จำเป็นต้องส่งหรือไม่?
A: ไม่จำเป็น — data เป็น optional ทุก type สำหรับ type=room ให้ใช้ data สำหรับค่าใช้จ่ายเพิ่มเติมเท่านั้น (เช่น ค่าน้ำ ค่าไฟ) ไม่ใส่ค่าเช่าหลักใน data (ค่าเช่าหลักอยู่ใน room.unit_price)
Q: amount สำหรับ type=room ต้องส่งหรือไม่?
A: optional — ระบบคำนวณ amount = room.unit_price + sum(data) อัตโนมัติ ถ้าส่ง amount มาต้องตรงกับค่าที่คำนวณได้ (tolerance ±0.01 บาท) ระบบจะตอบ 422 ถ้าไม่ตรง
Q: data sum ต้องตรงกับ amount เสมอไหม?
A: เฉพาะ type=content และ health_science — ถ้าส่ง data, sum(quantity × unit_price) ต้องตรงกับ amount (tolerance ±0.01 บาท) ระบบจะตอบ 422 ถ้าไม่ตรง
Q: type=room ต้องส่ง payer_name, payer_email, payer_phone หรือไม่?
A: ไม่บังคับ — สำหรับ type=room fields เหล่านี้เป็น optional ผู้ชำระสามารถกรอกข้อมูลในหน้าชำระเงินได้โดยตรง
Q: ref_no มีข้อจำกัดอะไรบ้าง?
A: ref_no เป็น string สูงสุด 55 อักษร และจะถูกส่งกลับมาใน webhook payload — ใช้ track payment ระหว่าง 2 ระบบ
Q: ref_no กับ booking_ref ต่างกันอย่างไร?
A: ref_no คือเลขอ้างอิงที่ คุณส่งมา (เช่น เลข order ใน system ของคุณ) — ใช้ track การชำระเงินระหว่าง 2 ระบบ; booking_ref คือเลข booking ที่ ระบบ CU ออกให้ (format: BK-690001)
Q: ถ้า Webhook ล้มเหลวจะเกิดอะไรขึ้น? A: ระบบ retry อัตโนมัติ 3 ครั้ง (1 นาที → 5 นาที → 30 นาที)
Q: ผู้ใช้ไม่ชำระเงินภายใน 30 นาที ทำอย่างไร?
A: payment_url หมดอายุ user จะเห็นหน้า "Link หมดอายุ" — สร้าง Payment Session ใหม่ได้
Q: client_secret หายต้องทำอย่างไร?
A: ติดต่อ thannaphat.j@zenithcomp.co.th เพื่อ regenerate credentials — ไม่สามารถดึงค่าเดิมได้
เอกสารนี้จัดทำโดยทีมพัฒนา CU E-Receipt — จุฬาลงกรณ์มหาวิทยาลัย หากมีข้อสงสัย ติดต่อ: thannaphat.j@zenithcomp.co.th