@warriorteam/redai-zalo-sdk
Version:
Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account v3.0, ZNS with Full Type Safety, Consultation Service, Broadcast Service, Group Messaging with List APIs, Social APIs, Enhanced Article Management, Promotion Service v3.0 with Multip
1,159 lines (1,158 loc) • 53.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZNSService = void 0;
const common_1 = require("../types/common");
/**
* Service for Zalo Notification Service (ZNS)
*
* Requirements for using ZNS API:
* - Official Account must be approved and have ZNS sending permission
* - Valid Official Account access token
* - ZNS template must be approved before use
* - Recipient phone number must be in correct format and valid
* - Template data must match defined parameters
* - Comply with message quantity limits according to service package
*
* Reference: https://developers.zalo.me/docs/zalo-notification-service/
*/
class ZNSService {
constructor(client) {
this.client = client;
// Zalo ZNS endpoints - organized by functionality
this.endpoints = {
message: {
sendTemplate: "https://business.openapi.zalo.me/message/template",
sendHashPhone: "https://business.openapi.zalo.me/message/template/hash_phone",
sendDev: "https://business.openapi.zalo.me/message/template/dev",
sendRsa: "https://business.openapi.zalo.me/message/template/rsa",
sendJourney: "https://business.openapi.zalo.me/message/template/journey",
status: "https://business.openapi.zalo.me/message/status",
quota: "https://business.openapi.zalo.me/message/quota",
templateTag: "https://business.openapi.zalo.me/message/template-tag",
},
template: {
list: "https://business.openapi.zalo.me/template/all",
detail: "https://business.openapi.zalo.me/template/info/v2",
create: "https://business.openapi.zalo.me/template/create",
edit: "https://business.openapi.zalo.me/template/edit",
uploadImage: "https://business.openapi.zalo.me/upload/image",
quality: "https://business.openapi.zalo.me/template/quality",
sampleData: "https://business.openapi.zalo.me/template/sample-data",
},
rating: {
get: "https://business.openapi.zalo.me/rating/get",
},
quality: {
oa: "https://business.openapi.zalo.me/quality",
},
};
}
/**
* Send ZNS message
* @param accessToken OA access token
* @param message ZNS message data
* @returns Send result
*/
async sendMessage(accessToken, message) {
try {
const response = await this.client.apiPost(this.endpoints.message.sendTemplate, accessToken, message);
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to send ZNS message");
}
}
/**
* Send ZNS message with phone hash
* @param accessToken OA access token
* @param message ZNS message with hashed phone
* @returns Send result
*/
async sendHashPhoneMessage(accessToken, message) {
try {
const response = await this.client.apiPost(this.endpoints.message.sendHashPhone, accessToken, message);
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to send ZNS hash phone message");
}
}
/**
* Send ZNS message in development mode
* @param accessToken OA access token
* @param message ZNS dev mode message
* @returns Send result
*/
async sendDevModeMessage(accessToken, message) {
try {
const response = await this.client.apiPost(this.endpoints.message.sendDev, accessToken, message);
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to send ZNS dev mode message");
}
}
/**
* Send ZNS message with RSA encryption
* @param accessToken OA access token
* @param message ZNS RSA message
* @returns Send result
*/
async sendRsaMessage(accessToken, message) {
try {
const response = await this.client.apiPost(this.endpoints.message.sendRsa, accessToken, message);
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to send ZNS RSA message");
}
}
/**
* Send ZNS journey message
* @param accessToken OA access token
* @param message ZNS journey message
* @returns Send result
*/
async sendJourneyMessage(accessToken, message) {
try {
const response = await this.client.apiPost(this.endpoints.message.sendJourney, accessToken, message);
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to send ZNS journey message");
}
}
/**
* Get ZNS message status
* @param accessToken OA access token
* @param messageId Message ID
* @returns Message status info
*
* Response data:
* - delivery_time: Thời gian thiết bị nhận được thông báo
* - status: Trạng thái thông báo
* * -1: The message does not exist
* * 0: The message is pushed successfully to Zalo server but has not yet delivered to user's phone
* * 1: The message was delivered to the user's phone
* - message: Mô tả trạng thái thông báo
*/
async getMessageStatus(accessToken, messageId) {
try {
const response = await this.client.apiGet(this.endpoints.message.status, accessToken, { message_id: messageId });
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to get ZNS message status");
}
}
/**
* Get ZNS batch message status (custom implementation)
* @param accessToken OA access token
* @param messageIds Array of message IDs
* @returns Batch message status info
*
* This is a custom implementation that calls the single message status API
* multiple times to get status for multiple messages. Since Zalo API doesn't
* provide a native batch endpoint, this method provides convenience by:
* - Making concurrent API calls for better performance
* - Handling errors gracefully (failed requests return error status)
* - Returning consistent response format
*/
async getBatchMessageStatus(accessToken, messageIds) {
try {
// Validate input
if (!Array.isArray(messageIds) || messageIds.length === 0) {
throw new common_1.ZaloSDKError("messageIds phải là array và không được rỗng", 400);
}
if (messageIds.length > 100) {
throw new common_1.ZaloSDKError("Không được kiểm tra quá 100 message cùng lúc", 400);
}
// Remove duplicates
const uniqueMessageIds = [...new Set(messageIds)];
// Make concurrent requests for all message IDs
const statusPromises = uniqueMessageIds.map(async (messageId) => {
try {
const status = await this.getMessageStatus(accessToken, messageId);
return {
messageId,
success: true,
data: status.data
};
}
catch (error) {
return {
messageId,
success: false,
error: error instanceof common_1.ZaloSDKError ? error.message : "Unknown error"
};
}
});
// Wait for all requests to complete
const results = await Promise.all(statusPromises);
// Build response data
const responseData = {};
let hasErrors = false;
results.forEach(result => {
if (result.success && result.data) {
responseData[result.messageId] = result.data;
}
else {
hasErrors = true;
responseData[result.messageId] = {
delivery_time: "",
message: result.error || "Failed to get status",
status: -1
};
}
});
return {
error: hasErrors ? 1 : 0,
message: hasErrors ? "Some messages failed to get status" : "Success",
data: responseData
};
}
catch (error) {
throw this.handleZNSError(error, "Failed to get batch ZNS message status");
}
}
/**
* Get ZNS quota information
* @param accessToken OA access token
* @returns Quota information
*
* Response data:
* - dailyQuota: Số thông báo ZNS OA được gửi trong 1 ngày
* - remainingQuota: Số thông báo ZNS OA được gửi trong ngày còn lại
* - dailyQuotaPromotion: Số tin ZNS hậu mãi OA được gửi trong ngày (null từ 1/11)
* - remainingQuotaPromotion: Số tin ZNS hậu mãi còn lại OA được gửi trong ngày (null từ 1/11)
* - monthlyPromotionQuota: Số tin ZNS hậu mãi OA được gửi trong tháng
* - remainingMonthlyPromotionQuota: Số tin ZNS hậu mãi còn lại OA được gửi trong tháng
* - estimatedNextMonthPromotionQuota: Số tin ZNS hậu mãi dự kiến mà OA có thể gửi trong tháng tiếp theo
*/
async getQuotaInfo(accessToken) {
try {
const response = await this.client.apiGet(this.endpoints.message.quota, accessToken);
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to get ZNS quota info");
}
}
/**
* Get ZNS allowed content types
* @param accessToken OA access token
* @returns Allowed content types
*
* Response data:
* - Mảng các loại nội dung mà OA có thể gửi:
* * "TRANSACTION": Giao dịch (cấp độ 1)
* * "CUSTOMER_CARE": Chăm sóc khách hàng (cấp độ 2)
* * "PROMOTION": Hậu mãi (cấp độ 3)
* - Dựa theo chất lượng gửi ZNS của OA, Zalo sẽ tự động điều chỉnh loại nội dung OA được gửi
*/
async getAllowedContentTypes(accessToken) {
try {
const response = await this.client.apiGet(this.endpoints.message.templateTag, accessToken);
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to get ZNS allowed content types");
}
}
/**
* Get ZNS template list
* @param accessToken OA access token
* @param offset Offset for pagination (template được tạo gần nhất có thứ tự 0)
* @param limit Limit for pagination (tối đa 100)
* @param status Template status filter (optional)
* @returns Template list
*
* Status values:
* - 1: ENABLE templates
* - 2: PENDING_REVIEW templates
* - 3: REJECT templates
* - 4: DISABLE templates
* - undefined: All templates
*/
async getTemplateList(accessToken, offset = 0, limit = 10, status) {
try {
// Validate limit (max 100)
if (limit > 100) {
throw new common_1.ZaloSDKError("Limit không được vượt quá 100", 400);
}
// Prepare query parameters
const params = { offset, limit };
if (status !== undefined) {
params.status = status;
}
const response = await this.client.apiGet(this.endpoints.template.list, accessToken, params);
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to get ZNS template list");
}
}
/**
* Get ZNS template details
* @param accessToken OA access token
* @param templateId Template ID
* @returns Template details
*
* Response data:
* - templateId: ID của template
* - templateName: Tên của template
* - status: Trạng thái template (ENABLE, PENDING_REVIEW, DELETE, REJECT, DISABLE)
* - reason: Lý do template có trạng thái hiện tại
* - listParams: Danh sách các thuộc tính của template
* - listButtons: Danh sách các buttons/CTAs của template
* - timeout: Thời gian timeout của template
* - previewUrl: Đường dẫn đến bản xem trước của template
* - templateQuality: Chất lượng template (null từ 10/12)
* - templateTag: Loại nội dung (TRANSACTION, CUSTOMER_CARE, PROMOTION)
* - price: Đơn giá của template
*/
async getTemplateDetails(accessToken, templateId) {
try {
const response = await this.client.apiGet(this.endpoints.template.detail, accessToken, { template_id: templateId });
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to get ZNS template details");
}
}
/**
* Custom: Lấy tất cả template kèm thông tin chi tiết cho 1 accessToken
* - Tự động phân trang để lấy đủ tổng số template
* - Lấy chi tiết song song theo batch để tối ưu thời gian
*/
async getAllTemplatesWithDetails(accessToken, status) {
try {
const limitPerPage = 100;
let offset = 0;
const templateIds = [];
let total = 0;
// Paginate through template list
// eslint-disable-next-line no-constant-condition
while (true) {
const listResp = await this.getTemplateList(accessToken, offset, limitPerPage, status);
const items = listResp.data || [];
if (listResp.metadata && typeof listResp.metadata.total === "number") {
total = listResp.metadata.total;
}
for (const item of items) {
if (!templateIds.includes(item.templateId)) {
templateIds.push(item.templateId);
}
}
if (items.length < limitPerPage || templateIds.length >= total) {
break;
}
offset += limitPerPage;
}
if (templateIds.length === 0) {
return { error: 0, message: "Success", data: [], metadata: { total } };
}
// Fetch details in batches to avoid burst
const batchSize = 10;
const detailResults = [];
let hasErrors = false;
for (let i = 0; i < templateIds.length; i += batchSize) {
const batchIds = templateIds.slice(i, i + batchSize);
const settled = await Promise.allSettled(batchIds.map((id) => this.getTemplateDetails(accessToken, id)));
for (const res of settled) {
if (res.status === "fulfilled" && res.value && res.value.data) {
detailResults.push(res.value.data);
}
else {
hasErrors = true;
}
}
}
return {
error: hasErrors ? 1 : 0,
message: hasErrors ? "Some template details failed to fetch" : "Success",
data: detailResults,
metadata: { total },
};
}
catch (error) {
throw this.handleZNSError(error, "Failed to get all templates with details");
}
}
/**
* Custom: Lấy tất cả template chi tiết cho nhiều accessToken (nhiều OA)
* Đầu ra nhóm theo từng accessToken
*/
async getAllTemplatesWithDetailsByTokens(accessTokens, status) {
try {
const uniqueTokens = Array.from(new Set(accessTokens.filter((t) => t && t.trim().length > 0)));
if (uniqueTokens.length === 0) {
return { error: 0, message: "Success", data: [] };
}
const settled = await Promise.allSettled(uniqueTokens.map((token) => this.getAllTemplatesWithDetails(token, status)));
const groups = [];
let hasErrors = false;
settled.forEach((res, index) => {
const token = uniqueTokens[index];
if (res.status === "fulfilled") {
const payload = res.value;
groups.push({
accessToken: token,
templates: payload.data,
total: payload.metadata.total,
});
if (payload.error !== 0) {
hasErrors = true;
}
}
else {
hasErrors = true;
groups.push({ accessToken: token, templates: [], total: 0 });
}
});
return {
error: hasErrors ? 1 : 0,
message: hasErrors ? "Some OA groups failed to fetch" : "Success",
data: groups,
};
}
catch (error) {
throw this.handleZNSError(error, "Failed to get templates with details by tokens");
}
}
/**
* Get ZNS template sample data
* @param accessToken OA access token
* @param templateId Template ID
* @returns Template sample data
*
* Response data:
* - Chứa tham số và dữ liệu mẫu của template
* - Ví dụ: { "balance_debt": 2000, "due_date": "01/01/1970", "customer_name": "customer_name_sample" }
*/
async getTemplateSampleData(accessToken, templateId) {
try {
const response = await this.client.apiGet(this.endpoints.template.sampleData, accessToken, { template_id: templateId });
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to get ZNS template sample data");
}
}
/**
* Get customer rating information
* @param accessToken OA access token
* @param templateId Template ID
* @param fromTime Start time (timestamp in milliseconds)
* @param toTime End time (timestamp in milliseconds)
* @param offset Position of first rating to return
* @param limit Maximum number of ratings to return
* @returns Customer rating information
*
* Lưu ý:
* - Chỉ có thể lấy thông tin đánh giá từ template đánh giá dịch vụ được tạo bởi ứng dụng
* - Access token phải ứng với template ID được tạo bởi app và OA
* - Thời gian theo định dạng timestamp (millisecond)
*/
async getCustomerRating(accessToken, templateId, fromTime, toTime, offset, limit) {
try {
const response = await this.client.apiGet(this.endpoints.rating.get, accessToken, {
template_id: templateId,
from_time: fromTime,
to_time: toTime,
offset,
limit,
});
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to get customer rating");
}
}
/**
* Get OA ZNS sending quality information
* @param accessToken OA access token
* @returns OA quality information
*
* Response data:
* - oaCurrentQuality: Chất lượng gửi ZNS trong 48 giờ gần nhất
* - oa7dayQuality: Chất lượng gửi ZNS trong 7 ngày gần nhất
*
* Quality levels:
* - HIGH: Mức độ chất lượng tốt
* - MEDIUM: Mức độ chất lượng trung bình
* - LOW: Mức độ chất lượng kém
* - UNDEFINED: Chưa được xác định (OA không gửi ZNS trong khung thời gian đánh giá)
*/
async getOAQuality(accessToken) {
try {
const response = await this.client.apiGet(this.endpoints.quality.oa, accessToken);
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to get OA quality information");
}
}
/**
* Create ZNS template
* @param accessToken OA access token
* @param templateData Template creation data theo chuẩn Zalo API
* @returns Created template response
*
* API: POST https://business.openapi.zalo.me/template/create
*/
async createTemplate(accessToken, templateData) {
try {
// Comprehensive validation
this.validateZNSTemplateRequest(templateData);
const response = await this.client.apiPost(this.endpoints.template.create, accessToken, templateData);
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to create ZNS template");
}
}
/**
* Edit ZNS template (chỉnh sửa template có trạng thái REJECT)
* @param accessToken OA access token
* @param templateData Template edit data theo chuẩn Zalo API
* @returns Edited template response
*
* Lưu ý:
* - Chỉ có thể chỉnh sửa template có trạng thái REJECT
* - Template sau khi chỉnh sửa sẽ chuyển về trạng thái PENDING_REVIEW
* - Daily quota: 100 requests/ngày
* - Cần quyền "Quản lý tài sản"
*
* API: POST https://business.openapi.zalo.me/template/edit
*/
async updateTemplate(accessToken, templateData) {
try {
// Validate template_id first
if (!templateData.template_id) {
throw new common_1.ZaloSDKError("template_id là bắt buộc", 400);
}
// Comprehensive validation (same as createTemplate)
this.validateZNSTemplateRequest(templateData);
const response = await this.client.apiPost(this.endpoints.template.edit, accessToken, templateData);
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to edit ZNS template");
}
}
/**
* Upload image for ZNS template
* @param accessToken OA access token
* @param imageFile Image file (Buffer or ReadableStream)
* @param filename Filename with extension
* @returns Upload result with media_id
*
* Lưu ý:
* - Định dạng hỗ trợ: JPG, PNG
* - Dung lượng tối đa: 500KB
* - Hạn mức: 5000 ảnh/tháng/app
* - Logo: PNG, 400x96px
* - Hình ảnh: JPG/PNG, tỉ lệ 16:9
* - Cần quyền "Quản lý tài sản"
*/
async uploadImage(accessToken, imageFile, filename = "image.jpg") {
try {
// Validate file extension
const allowedExtensions = [".jpg", ".jpeg", ".png"];
const fileExtension = filename
.toLowerCase()
.substring(filename.lastIndexOf("."));
if (!allowedExtensions.includes(fileExtension)) {
throw new common_1.ZaloSDKError("File phải có định dạng JPG hoặc PNG", 400);
}
// Validate file size if it's a Buffer (max 500KB)
if (Buffer.isBuffer(imageFile)) {
const maxSize = 500 * 1024; // 500KB
if (imageFile.length > maxSize) {
throw new common_1.ZaloSDKError("Dung lượng file không được vượt quá 500KB", 400);
}
}
const response = await this.client.apiUploadFile(this.endpoints.template.uploadImage, accessToken, imageFile, filename);
return response;
}
catch (error) {
throw this.handleZNSError(error, "Failed to upload ZNS image");
}
}
/**
* Comprehensive validation for ZNS template request
*/
validateZNSTemplateRequest(templateData) {
// 1. Basic required fields validation
this.validateBasicFields(templateData);
// 2. Template type and tag compatibility
this.validateTemplateTypeTagCompatibility(templateData.template_type, templateData.tag);
// 3. Layout structure validation
this.validateLayoutStructure(templateData.template_type, templateData.layout);
// 4. Params validation if provided
if (templateData.params && templateData.params.length > 0) {
this.validateParams(templateData.params, templateData.layout);
}
// 5. Note validation if provided
if (templateData.note) {
if (templateData.note.length < 1 || templateData.note.length > 400) {
throw new common_1.ZaloSDKError("note phải có độ dài từ 1 đến 400 ký tự", 400);
}
}
}
/**
* Validate basic required fields
*/
validateBasicFields(templateData) {
if (!templateData.template_name) {
throw new common_1.ZaloSDKError("template_name là bắt buộc", 400);
}
if (templateData.template_name.length < 10 || templateData.template_name.length > 60) {
throw new common_1.ZaloSDKError("template_name phải có độ dài từ 10 đến 60 ký tự", 400);
}
if (!templateData.template_type) {
throw new common_1.ZaloSDKError("template_type là bắt buộc", 400);
}
if (![1, 2, 3, 4, 5].includes(templateData.template_type)) {
throw new common_1.ZaloSDKError("template_type phải là 1, 2, 3, 4 hoặc 5", 400);
}
if (!templateData.tag) {
throw new common_1.ZaloSDKError("tag là bắt buộc", 400);
}
if (!["1", "2", "3"].includes(templateData.tag)) {
throw new common_1.ZaloSDKError("tag phải là '1', '2' hoặc '3'", 400);
}
if (!templateData.layout) {
throw new common_1.ZaloSDKError("layout là bắt buộc", 400);
}
if (!Array.isArray(templateData.layout) && typeof templateData.layout !== 'object') {
throw new common_1.ZaloSDKError("layout phải là array hoặc object", 400);
}
if (!templateData.tracking_id) {
throw new common_1.ZaloSDKError("tracking_id là bắt buộc", 400);
}
}
/**
* Validate template_type and tag compatibility
*/
validateTemplateTypeTagCompatibility(templateType, tag) {
const compatibility = {
1: ["1", "2", "3"], // ZNS tùy chỉnh
2: ["1"], // ZNS xác thực
3: ["1"], // ZNS yêu cầu thanh toán
4: ["1", "2", "3"], // ZNS voucher
5: ["2"] // ZNS đánh giá dịch vụ
};
if (!compatibility[templateType]?.includes(tag)) {
throw new common_1.ZaloSDKError(`template_type ${templateType} không tương thích với tag ${tag}`, 400);
}
}
/**
* Validate layout structure based on template type
*/
validateLayoutStructure(templateType, layout) {
// Handle both array format (Zalo API) and object format (our types)
let headerComponents = [];
let bodyComponents = [];
let footerComponents = [];
let allComponents = [];
if (Array.isArray(layout)) {
// Array format - direct from Zalo API docs
headerComponents = layout.filter(item => this.isHeaderComponent(item));
bodyComponents = layout.filter(item => this.isBodyComponent(item));
footerComponents = layout.filter(item => this.isFooterComponent(item));
allComponents = layout;
}
else if (layout && typeof layout === 'object') {
// Object format - our structured type
if (layout.header?.components) {
headerComponents = layout.header.components;
}
if (layout.body?.components) {
bodyComponents = layout.body.components;
}
if (layout.footer?.components) {
footerComponents = layout.footer.components;
}
allComponents = [...headerComponents, ...bodyComponents, ...footerComponents];
}
else {
throw new common_1.ZaloSDKError("layout phải là array hoặc object có cấu trúc header/body/footer", 400);
}
switch (templateType) {
case 1: // ZNS tùy chỉnh
this.validateCustomTemplateLayout(headerComponents, bodyComponents, footerComponents);
break;
case 2: // ZNS xác thực
this.validateAuthTemplateLayout(headerComponents, bodyComponents, footerComponents);
break;
case 3: // ZNS yêu cầu thanh toán
this.validatePaymentTemplateLayout(headerComponents, bodyComponents, footerComponents);
break;
case 4: // ZNS voucher
this.validateVoucherTemplateLayout(headerComponents, bodyComponents, footerComponents);
break;
case 5: // ZNS đánh giá dịch vụ
this.validateRatingTemplateLayout(headerComponents, bodyComponents, footerComponents);
break;
}
// Validate individual components
allComponents.forEach(component => this.validateComponent(component));
}
/**
* Check if component belongs to header
*/
isHeaderComponent(component) {
return 'LOGO' in component || 'IMAGES' in component;
}
/**
* Check if component belongs to body
*/
isBodyComponent(component) {
return 'TITLE' in component || 'PARAGRAPH' in component || 'TABLE' in component ||
'OTP' in component || 'PAYMENT' in component || 'VOUCHER' in component || 'RATING' in component;
}
/**
* Check if component belongs to footer
*/
isFooterComponent(component) {
return 'BUTTONS' in component;
}
/**
* Validate custom template layout (type 1)
*/
validateCustomTemplateLayout(header, body, footer) {
// Header: Logo OR Image (exactly 1)
if (header.length !== 1) {
throw new common_1.ZaloSDKError("ZNS tùy chỉnh phải có đúng 1 component trong header (Logo hoặc Image)", 400);
}
// Body: Title (1) + Paragraph (0-4) + Table (0-1)
const titles = body.filter(c => 'TITLE' in c);
const paragraphs = body.filter(c => 'PARAGRAPH' in c);
const tables = body.filter(c => 'TABLE' in c);
if (titles.length !== 1) {
throw new common_1.ZaloSDKError("ZNS tùy chỉnh phải có đúng 1 component TITLE", 400);
}
if (paragraphs.length > 4) {
throw new common_1.ZaloSDKError("ZNS tùy chỉnh không được có quá 4 component PARAGRAPH", 400);
}
if (tables.length > 1) {
throw new common_1.ZaloSDKError("ZNS tùy chỉnh không được có quá 1 component TABLE", 400);
}
// Footer: Buttons (0-2)
if (footer.length > 2) {
throw new common_1.ZaloSDKError("ZNS tùy chỉnh không được có quá 2 button", 400);
}
// If has image, must have at least 1 button
const hasImage = header.some(c => 'IMAGES' in c);
if (hasImage && footer.length === 0) {
throw new common_1.ZaloSDKError("ZNS có component Image phải có ít nhất 1 button", 400);
}
}
/**
* Validate auth template layout (type 2)
*/
validateAuthTemplateLayout(header, body, footer) {
// Header: Logo only (1)
if (header.length !== 1 || !('LOGO' in header[0])) {
throw new common_1.ZaloSDKError("ZNS xác thực phải có đúng 1 component LOGO trong header", 400);
}
// Body: OTP (1) + Paragraph (1)
const otps = body.filter(c => 'OTP' in c);
const paragraphs = body.filter(c => 'PARAGRAPH' in c);
if (otps.length !== 1) {
throw new common_1.ZaloSDKError("ZNS xác thực phải có đúng 1 component OTP", 400);
}
if (paragraphs.length !== 1) {
throw new common_1.ZaloSDKError("ZNS xác thực phải có đúng 1 component PARAGRAPH", 400);
}
// Footer: None
if (footer.length > 0) {
throw new common_1.ZaloSDKError("ZNS xác thực không được có component trong footer", 400);
}
}
/**
* Validate payment template layout (type 3)
*/
validatePaymentTemplateLayout(header, body, footer) {
// Header: Logo only (1)
if (header.length !== 1 || !('LOGO' in header[0])) {
throw new common_1.ZaloSDKError("ZNS yêu cầu thanh toán phải có đúng 1 component LOGO trong header", 400);
}
// Body: Title (1) + Paragraph (0-4) + Table (0-1) + Payment (1)
const titles = body.filter(c => 'TITLE' in c);
const paragraphs = body.filter(c => 'PARAGRAPH' in c);
const tables = body.filter(c => 'TABLE' in c);
const payments = body.filter(c => 'PAYMENT' in c);
if (titles.length !== 1) {
throw new common_1.ZaloSDKError("ZNS yêu cầu thanh toán phải có đúng 1 component TITLE", 400);
}
if (paragraphs.length > 4) {
throw new common_1.ZaloSDKError("ZNS yêu cầu thanh toán không được có quá 4 component PARAGRAPH", 400);
}
if (tables.length > 1) {
throw new common_1.ZaloSDKError("ZNS yêu cầu thanh toán không được có quá 1 component TABLE", 400);
}
if (payments.length !== 1) {
throw new common_1.ZaloSDKError("ZNS yêu cầu thanh toán phải có đúng 1 component PAYMENT", 400);
}
// Footer: Buttons (0-2)
if (footer.length > 2) {
throw new common_1.ZaloSDKError("ZNS yêu cầu thanh toán không được có quá 2 button", 400);
}
}
/**
* Validate voucher template layout (type 4)
*/
validateVoucherTemplateLayout(header, body, footer) {
// Header: Logo OR Image (exactly 1)
if (header.length !== 1) {
throw new common_1.ZaloSDKError("ZNS voucher phải có đúng 1 component trong header (Logo hoặc Image)", 400);
}
// Body: Title (1) + Paragraph (0-4) + Table (0-1) + Voucher (1)
const titles = body.filter(c => 'TITLE' in c);
const paragraphs = body.filter(c => 'PARAGRAPH' in c);
const tables = body.filter(c => 'TABLE' in c);
const vouchers = body.filter(c => 'VOUCHER' in c);
if (titles.length !== 1) {
throw new common_1.ZaloSDKError("ZNS voucher phải có đúng 1 component TITLE", 400);
}
if (paragraphs.length > 4) {
throw new common_1.ZaloSDKError("ZNS voucher không được có quá 4 component PARAGRAPH", 400);
}
if (tables.length > 1) {
throw new common_1.ZaloSDKError("ZNS voucher không được có quá 1 component TABLE", 400);
}
if (vouchers.length !== 1) {
throw new common_1.ZaloSDKError("ZNS voucher phải có đúng 1 component VOUCHER", 400);
}
// Footer: Buttons (0-2)
if (footer.length > 2) {
throw new common_1.ZaloSDKError("ZNS voucher không được có quá 2 button", 400);
}
// If has image, must have at least 1 button, max 2
const hasImage = header.some(c => 'IMAGES' in c);
if (hasImage && footer.length === 0) {
throw new common_1.ZaloSDKError("ZNS voucher có component Image phải có ít nhất 1 button", 400);
}
}
/**
* Validate rating template layout (type 5)
*/
validateRatingTemplateLayout(header, body, footer) {
// Header: Logo only (1)
if (header.length !== 1 || !('LOGO' in header[0])) {
throw new common_1.ZaloSDKError("ZNS đánh giá dịch vụ phải có đúng 1 component LOGO trong header", 400);
}
// Body: Title (1) + Paragraph (0-1) + Rating (1)
const titles = body.filter(c => 'TITLE' in c);
const paragraphs = body.filter(c => 'PARAGRAPH' in c);
const ratings = body.filter(c => 'RATING' in c);
if (titles.length !== 1) {
throw new common_1.ZaloSDKError("ZNS đánh giá dịch vụ phải có đúng 1 component TITLE", 400);
}
if (paragraphs.length > 1) {
throw new common_1.ZaloSDKError("ZNS đánh giá dịch vụ không được có quá 1 component PARAGRAPH", 400);
}
if (ratings.length !== 1) {
throw new common_1.ZaloSDKError("ZNS đánh giá dịch vụ phải có đúng 1 component RATING", 400);
}
// Footer: Buttons (0-2)
if (footer.length > 2) {
throw new common_1.ZaloSDKError("ZNS đánh giá dịch vụ không được có quá 2 button", 400);
}
}
/**
* Validate individual component content
*/
validateComponent(component) {
if ('TITLE' in component) {
this.validateTitleComponent(component.TITLE);
}
if ('PARAGRAPH' in component) {
this.validateParagraphComponent(component.PARAGRAPH);
}
if ('OTP' in component) {
this.validateOTPComponent(component.OTP);
}
if ('TABLE' in component) {
this.validateTableComponent(component.TABLE);
}
if ('LOGO' in component) {
this.validateLogoComponent(component.LOGO);
}
if ('IMAGES' in component) {
this.validateImagesComponent(component.IMAGES);
}
if ('BUTTONS' in component) {
this.validateButtonsComponent(component.BUTTONS);
}
if ('PAYMENT' in component) {
this.validatePaymentComponent(component.PAYMENT);
}
if ('VOUCHER' in component) {
this.validateVoucherComponent(component.VOUCHER);
}
if ('RATING' in component) {
this.validateRatingComponent(component.RATING);
}
}
/**
* Validate TITLE component
*/
validateTitleComponent(title) {
if (!title.value || typeof title.value !== 'string') {
throw new common_1.ZaloSDKError("TITLE component phải có field value kiểu string", 400);
}
if (title.value.length < 9 || title.value.length > 65) {
throw new common_1.ZaloSDKError("TITLE value phải có độ dài từ 9 đến 65 ký tự", 400);
}
// Check max 4 params
const paramCount = (title.value.match(/\{\{[^}]+\}\}/g) || []).length;
if (paramCount > 4) {
throw new common_1.ZaloSDKError("TITLE component không được có quá 4 params", 400);
}
}
/**
* Validate PARAGRAPH component
*/
validateParagraphComponent(paragraph) {
if (!paragraph.value || typeof paragraph.value !== 'string') {
throw new common_1.ZaloSDKError("PARAGRAPH component phải có field value kiểu string", 400);
}
if (paragraph.value.length < 9 || paragraph.value.length > 400) {
throw new common_1.ZaloSDKError("PARAGRAPH value phải có độ dài từ 9 đến 400 ký tự", 400);
}
// Check max 10 params
const paramCount = (paragraph.value.match(/\{\{[^}]+\}\}/g) || []).length;
if (paramCount > 10) {
throw new common_1.ZaloSDKError("PARAGRAPH component không được có quá 10 params", 400);
}
}
/**
* Validate OTP component
*/
validateOTPComponent(otp) {
if (!otp.value || typeof otp.value !== 'string') {
throw new common_1.ZaloSDKError("OTP component phải có field value kiểu string", 400);
}
if (otp.value.length < 1 || otp.value.length > 10) {
throw new common_1.ZaloSDKError("OTP value phải có độ dài từ 1 đến 10 ký tự", 400);
}
}
/**
* Validate TABLE component
*/
validateTableComponent(table) {
if (!table.rows || !Array.isArray(table.rows)) {
throw new common_1.ZaloSDKError("TABLE component phải có field rows kiểu array", 400);
}
if (table.rows.length < 2 || table.rows.length > 8) {
throw new common_1.ZaloSDKError("TABLE rows phải có từ 2 đến 8 objects", 400);
}
table.rows.forEach((row, index) => {
if (!row.title || typeof row.title !== 'string') {
throw new common_1.ZaloSDKError(`TABLE row ${index + 1} phải có field title kiểu string`, 400);
}
if (row.title.length < 3 || row.title.length > 36) {
throw new common_1.ZaloSDKError(`TABLE row ${index + 1} title phải có độ dài từ 3 đến 36 ký tự`, 400);
}
if (!row.value || typeof row.value !== 'string') {
throw new common_1.ZaloSDKError(`TABLE row ${index + 1} phải có field value kiểu string`, 400);
}
if (row.value.length < 3 || row.value.length > 90) {
throw new common_1.ZaloSDKError(`TABLE row ${index + 1} value phải có độ dài từ 3 đến 90 ký tự`, 400);
}
if (row.row_type !== undefined && ![0, 1, 2, 3, 4, 5].includes(row.row_type)) {
throw new common_1.ZaloSDKError(`TABLE row ${index + 1} row_type phải là 0, 1, 2, 3, 4 hoặc 5`, 400);
}
});
}
/**
* Validate LOGO component
*/
validateLogoComponent(logo) {
if (!logo.light || !logo.dark) {
throw new common_1.ZaloSDKError("LOGO component phải có cả light và dark attachment", 400);
}
this.validateAttachment(logo.light, "LOGO light");
this.validateAttachment(logo.dark, "LOGO dark");
}
/**
* Validate IMAGES component
*/
validateImagesComponent(images) {
if (!images.items || !Array.isArray(images.items)) {
throw new common_1.ZaloSDKError("IMAGES component phải có field items kiểu array", 400);
}
if (images.items.length < 1 || images.items.length > 3) {
throw new common_1.ZaloSDKError("IMAGES items phải có từ 1 đến 3 attachments", 400);
}
images.items.forEach((item, index) => {
this.validateAttachment(item, `IMAGES item ${index + 1}`);
});
}
/**
* Validate BUTTONS component
*/
validateButtonsComponent(buttons) {
if (!buttons.items || !Array.isArray(buttons.items)) {
throw new common_1.ZaloSDKError("BUTTONS component phải có field items kiểu array", 400);
}
if (buttons.items.length < 1 || buttons.items.length > 2) {
throw new common_1.ZaloSDKError("BUTTONS items phải có từ 1 đến 2 objects", 400);
}
buttons.items.forEach((button, index) => {
if (!button.type || ![1, 2, 3, 4, 5, 6, 7, 8, 9].includes(button.type)) {
throw new common_1.ZaloSDKError(`BUTTON ${index + 1} type phải là 1, 2, 3, 4, 5, 6, 7, 8 hoặc 9`, 400);
}
if (!button.title || typeof button.title !== 'string') {
throw new common_1.ZaloSDKError(`BUTTON ${index + 1} phải có field title kiểu string`, 400);
}
if (button.title.length < 5 || button.title.length > 30) {
throw new common_1.ZaloSDKError(`BUTTON ${index + 1} title phải có độ dài từ 5 đến 30 ký tự`, 400);
}
if (!button.content || typeof button.content !== 'string') {
throw new common_1.ZaloSDKError(`BUTTON ${index + 1} phải có field content kiểu string`, 400);
}
});
}
/**
* Validate PAYMENT component
*/
validatePaymentComponent(payment) {
if (!payment.bank_code || typeof payment.bank_code !== 'string') {
throw new common_1.ZaloSDKError("PAYMENT component phải có field bank_code kiểu string", 400);
}
if (!payment.account_name || typeof payment.account_name !== 'string') {
throw new common_1.ZaloSDKError("PAYMENT component phải có field account_name kiểu string", 400);
}
if (payment.account_name.length < 1 || payment.account_name.length > 100) {
throw new common_1.ZaloSDKError("PAYMENT account_name phải có độ dài từ 1 đến 100 ký tự", 400);
}
if (!payment.bank_account || typeof payment.bank_account !== 'string') {
throw new common_1.ZaloSDKError("PAYMENT component phải có field bank_account kiểu string", 400);
}
if (payment.bank_account.length < 1 || payment.bank_account.length > 100) {
throw new common_1.ZaloSDKError("PAYMENT bank_account phải có độ dài từ 1 đến 100 ký tự", 400);
}
if (!payment.amount) {
throw new common_1.ZaloSDKError("PAYMENT component phải có field amount", 400);
}
if (payment.note && (payment.note.length < 1 || payment.note.length > 90)) {
throw new common_1.ZaloSDKError("PAYMENT note phải có độ dài từ 1 đến 90 ký tự", 400);
}
}
/**
* Validate VOUCHER component
*/
validateVoucherComponent(voucher) {
if (!voucher.name || typeof voucher.name !== 'string') {
throw new common_1.ZaloSDKError("VOUCHER component phải có field name kiểu string", 400);
}
if (voucher.name.length < 1 || voucher.name.length > 30) {
throw new common_1.ZaloSDKError("VOUCHER name phải có độ dài từ 1 đến 30 ký tự", 400);
}
if (!voucher.condition || typeof voucher.condition !== 'string') {
throw new common_1.ZaloSDKError("VOUCHER component phải có field condition kiểu string", 400);
}
if (voucher.condition.length < 1 || voucher.condition.length > 40) {
throw new common_1.ZaloSDKError("VOUCHER condition phải có độ dài từ 1 đến 40 ký tự", 400);
}
if (!voucher.end_date || typeof voucher.end_date !== 'string') {
throw new common_1.ZaloSDKError("VOUCHER component phải có field end_date kiểu string", 400);
}
if (!voucher.voucher_code || typeof voucher.voucher_code !== 'string') {
throw new common_1.ZaloSDKError("VOUCHER component phải có field voucher_code kiểu string", 400);
}
if (voucher.voucher_code.length < 1 || voucher.voucher_code.length > 25) {
throw new common_1.ZaloSDKError("VOUCHER voucher_code phải có độ dài từ 1 đến 25 ký tự", 400);
}
if (voucher.display_code !== undefined && ![1, 2, 3].includes(voucher.display_code)) {
throw new common_1.ZaloSDKError("VOUCHER display_code phải là 1, 2 hoặc 3", 400);
}
}
/**
* Validate RATING component
*/
validateRatingComponent(rating) {
if (!rating.items || !Array.isArray(rating.items)) {
throw new common_1.ZaloSDKError("RATING component phải có field items kiểu array", 400);
}
if (rating.items.length !== 5) {
throw new common_1.ZaloSDKError("RATING items phải có đúng 5 objects", 400);
}
rating.items.forEach((item, index) => {
const starValue = (index + 1);
if (item.star !== starValue) {
throw new common_1.ZaloSDKError(`RATING item ${index + 1} phải có star = ${starValue}`, 400);
}
if (!item.title || typeof item.title !== 'string') {
throw new common_1.ZaloSDKError(`RATING item ${index + 1} phải có field title kiểu string`, 400);
}
if (item.title.length < 1 || item.title.length > 50) {
throw new common_1.ZaloSDKError(`RATING item ${index + 1} title phải có độ dài từ 1 đến 50 ký tự`, 400);
}
if (item.question && (item.question.length < 1 || item.question.length > 100)) {
throw new common_1.ZaloSDKError(`RATING item ${index + 1} question phải có độ dài từ 1 đến 100 ký tự`, 400);
}
if (item.answers && Array.isArray(item.answers)) {
if (item.answers.length < 1 || item.answers.length > 5) {
throw new common_1.ZaloSDKError(`RATING item ${index + 1} answers phải có từ 1 đến 5 câu trả lời`, 400);
}
item.answers.forEach((answer, answerIndex) => {
if (!answer || answer.length < 1 || answer.length > 50) {
throw new common_1.ZaloSDKError(`RATING item ${index + 1} answer ${answerIndex + 1} phải có độ dài từ 1 đến 50 ký tự`, 400);
}
});
}
if (!item.thanks || typeof item.thanks !== 'string') {
throw new common_1.ZaloSDKError(`RATING item ${index + 1} phải có field thanks kiểu string`, 400);
}
if (item.thanks.length < 1 || item.thanks.length > 100) {
throw new common_1.ZaloSDKError(`RATING item ${index + 1} thanks phải có độ dài từ 1 đến 100 ký tự`, 400);
}
if (!item.description || typeof item.description !== 'string') {
throw new common_1.ZaloSDKError(`RATING item ${index + 1} phải có field description kiểu string`, 400);
}
if (item.description.length < 1 || item.description.length > 200) {
throw new common_1.ZaloSDKError(`RATING item ${index + 1} description phải có độ dài từ 1 đến 200 ký tự`, 400);
}
});
}
/**
* Validate attachment object
*/
validateAttachment(attachment, context) {
if (!attachment.type || !attachment.media_id) {
throw new common_1.ZaloSDKError(`${context} phải có type và media_id`, 400);
}
if (attachment.type !== "IMAGE") {
throw new common_1.ZaloSDKError(`${context} type phải là "IMAGE"`, 400);
}
}
/**
* Validate params array
*/
validateParams(params, layout) {
const paramTypeConstraints = {
"1": 30, // Tên khách hàng
"2": 15, // Số điện thoại
"3": 200, // Địa chỉ
"4": 30, // Mã số
"5": 30, // Nhãn tùy chỉnh
"6": 30, // Trạng thái giao dịch
"7": 50, // Thông tin liên hệ
"8": 5, // Giới tính / Danh xưng
"9": 200, // Tên sản phẩm / Thương hiệu
"10": 20, // Số lượng / Số tiền
"11": 20, // Thời gian
"12": 10, // OTP
"13": 200, // URL
"14": 12, // Tiền tệ (VNĐ)
"15": 90 // Bank transfer note
};
// Get all params used in layout
const layoutString = JSON.stringify(layout);
const usedParams = new Set();