@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
819 lines • 36.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PromotionService = void 0;
const common_1 = require("../types/common");
/**
* Service xử lý các API tin nhắn truyền thông/quảng cáo của Zalo Official Account
*
* ĐIỀU KIỆN GỬI TIN TRUYỀN THÔNG:
*
* 1. THỜI GIAN GỬI:
* - Chỉ được gửi trong khung giờ từ 8:00 - 22:00 hàng ngày
* - Không được gửi vào các ngày lễ, tết theo quy định
*
* 2. NỘI DUNG TIN NHẮN:
* - Phải là nội dung quảng cáo, khuyến mãi, tin tức
* - Không được chứa nội dung vi phạm pháp luật
* - Phải tuân thủ quy định về quảng cáo của Việt Nam
*
* 3. TẦN SUẤT GỬI:
* - Tối đa 1 tin nhắn truyền thông/ngày cho mỗi người dùng
* - Người dùng có thể từ chối nhận tin truyền thông
*
* 4. ĐỊNH DẠNG:
* - Phải sử dụng template được Zalo phê duyệt trước
* - Template phải tuân thủ format chuẩn của tin truyền thông
*
* 5. NGƯỜI DÙNG:
* - Người dùng phải đã follow OA
* - Người dùng không được block OA
* - Người dùng không được từ chối nhận tin truyền thông
*
* 6. OFFICIAL ACCOUNT:
* - OA phải được xác minh (verified)
* - OA phải có quyền gửi tin truyền thông được Zalo cấp phép
* - OA không được vi phạm chính sách về quảng cáo
*
* LỖI THƯỜNG GẶP:
* - 2001: Ngoài khung giờ cho phép (8:00-22:00)
* - 2002: Người dùng đã từ chối nhận tin truyền thông
* - 2003: Vượt quá giới hạn 1 tin/ngày
* - 2004: Template chưa được phê duyệt
* - 2005: Nội dung vi phạm chính sách quảng cáo
* - 2006: OA chưa có quyền gửi tin truyền thông
*/
class PromotionService {
constructor(client) {
this.client = client;
this.promotionApiUrl = "https://openapi.zalo.me/v3.0/oa/message/promotion";
}
/**
* Convert input button to proper typed button for API
* @param button Input button with flexible typing
* @returns Properly typed button for API
*/
convertToTypedButton(button) {
const baseButton = {
title: button.title,
image_icon: button.imageIcon || "",
type: button.type,
};
// Convert payload based on button type using discriminated union
switch (button.type) {
case "oa.open.url":
return {
...baseButton,
payload: { url: button.payload.url }
};
case "oa.query.show":
case "oa.query.hide":
return {
...baseButton,
payload: button.payload // Already string type
};
case "oa.open.sms":
return {
...baseButton,
payload: {
content: button.payload.content,
phone_code: button.payload.phone_code || button.payload.phoneCode || ""
}
};
case "oa.open.phone":
return {
...baseButton,
payload: typeof button.payload === 'string'
? { phone_code: button.payload }
: { phone_code: button.payload.phone_code || button.payload.phoneCode || "" }
};
default:
// This should never happen with proper typing
return {
...baseButton,
payload: {}
};
}
}
/**
* Gửi tin nhắn truyền thông/quảng cáo
* @param accessToken Access token của Official Account
* @param recipient Thông tin người nhận
* @param message Nội dung tin nhắn truyền thông
* @returns Thông tin tin nhắn đã gửi
*/
async sendPromotionMessage(accessToken, recipient, message) {
try {
// Validate promotion message
this.validatePromotionMessage(message);
// Check sending time (8:00 - 22:00)
this.validateSendingTime();
const request = {
recipient,
message,
};
const result = await this.client.apiPost(this.promotionApiUrl, accessToken, request);
if (result.error !== 0) {
throw new common_1.ZaloSDKError(result.message || "Failed to send promotion message", result.error, result);
}
if (!result.data) {
throw new common_1.ZaloSDKError("No response data received", -1);
}
return result.data;
}
catch (error) {
if (error instanceof common_1.ZaloSDKError) {
throw error;
}
throw new common_1.ZaloSDKError(`Failed to send promotion message: ${error.message}`, -1, error);
}
}
/**
* Gửi tin nhắn khuyến mãi sản phẩm
* @param accessToken Access token của Official Account
* @param recipient Thông tin người nhận
* @param promotionInfo Thông tin khuyến mãi
* @returns Thông tin tin nhắn đã gửi
*/
async sendProductPromotion(accessToken, recipient, promotionInfo) {
try {
const message = {
type: "promotion",
attachment: {
type: "template",
payload: {
template_type: "promotion",
language: "VI",
elements: [
// Banner element
{
type: "banner",
...(promotionInfo.attachmentId
? { attachment_id: promotionInfo.attachmentId }
: { image_url: promotionInfo.imageUrl }),
},
// Header element
{
type: "header",
content: `💥💥 ${promotionInfo.title} 💥💥`,
align: "center",
},
// Description text
{
type: "text",
content: promotionInfo.description,
align: "left",
},
// Price table
{
type: "table",
content: [
{
key: "Giá gốc",
value: `${promotionInfo.originalPrice.toLocaleString('vi-VN')}đ`,
},
{
key: "Giá khuyến mãi",
value: `${promotionInfo.discountPrice.toLocaleString('vi-VN')}đ`,
},
{
key: "Giảm giá",
value: `${promotionInfo.discountPercent}%`,
},
{
key: "Hạn sử dụng",
value: promotionInfo.validUntil,
},
],
},
// Footer text
{
type: "text",
content: "🛒 Nhanh tay đặt hàng để không bỏ lỡ ưu đãi!",
align: "center",
},
],
buttons: [
{
title: "Mua ngay",
image_icon: "",
type: "oa.open.url",
payload: {
url: promotionInfo.productUrl,
},
},
{
title: "Xem thêm sản phẩm",
image_icon: "",
type: "oa.query.hide",
payload: "#xemthem",
},
],
},
},
};
return this.sendPromotionMessage(accessToken, recipient, message);
}
catch (error) {
throw new common_1.ZaloSDKError(`Failed to send product promotion: ${error.message}`, -1, error);
}
}
/**
* Gửi tin nhắn thông báo sự kiện
* @param accessToken Access token của Official Account
* @param recipient Thông tin người nhận
* @param eventInfo Thông tin sự kiện
* @returns Thông tin tin nhắn đã gửi
*/
async sendEventNotification(accessToken, recipient, eventInfo) {
try {
const message = {
type: "promotion",
attachment: {
type: "template",
payload: {
template_type: "promotion",
language: "VI",
elements: [
// Banner element
{
type: "banner",
...(eventInfo.attachmentId
? { attachment_id: eventInfo.attachmentId }
: { image_url: eventInfo.imageUrl }),
},
// Header element
{
type: "header",
content: `🎉 ${eventInfo.title} 🎉`,
align: "center",
},
// Description text
{
type: "text",
content: eventInfo.description,
align: "left",
},
// Event details table
{
type: "table",
content: [
{
key: "Thời gian",
value: eventInfo.eventDate,
},
{
key: "Địa điểm",
value: eventInfo.location,
},
],
},
// Call to action text
{
type: "text",
content: "🎫 Đăng ký ngay để không bỏ lỡ sự kiện!",
align: "center",
},
],
buttons: [
{
title: "Đăng ký tham gia",
image_icon: "",
type: "oa.open.url",
payload: {
url: eventInfo.registrationUrl,
},
},
{
title: "Chia sẻ sự kiện",
image_icon: "",
type: "oa.query.hide",
payload: "#chiase",
},
],
},
},
};
return this.sendPromotionMessage(accessToken, recipient, message);
}
catch (error) {
throw new common_1.ZaloSDKError(`Failed to send event notification: ${error.message}`, -1, error);
}
}
/**
* Gửi tin nhắn newsletter
* @param accessToken Access token của Official Account
* @param recipient Thông tin người nhận
* @param newsletterInfo Thông tin newsletter
* @returns Thông tin tin nhắn đã gửi
*/
async sendNewsletter(accessToken, recipient, newsletterInfo) {
try {
const message = {
type: "promotion",
attachment: {
type: "template",
payload: {
template_type: "promotion",
language: "VI",
elements: [
// Banner element
{
type: "banner",
...(newsletterInfo.attachmentId
? { attachment_id: newsletterInfo.attachmentId }
: { image_url: newsletterInfo.imageUrl }),
},
// Header element
{
type: "header",
content: `📰 ${newsletterInfo.title}`,
align: "center",
},
// Summary text
{
type: "text",
content: newsletterInfo.summary,
align: "left",
},
// Articles table
{
type: "table",
content: newsletterInfo.articles.slice(0, 5).map((article, index) => ({
key: `Bài viết ${index + 1}`,
value: article.title,
})),
},
// Footer text
{
type: "text",
content: "📖 Đọc ngay để cập nhật thông tin mới nhất!",
align: "center",
},
],
buttons: [
{
title: "Đọc toàn bộ",
image_icon: "",
type: "oa.open.url",
payload: {
url: newsletterInfo.articles[0]?.url || "#",
},
},
{
title: "Hủy đăng ký",
image_icon: "",
type: "oa.open.url",
payload: {
url: newsletterInfo.unsubscribeUrl,
},
},
],
},
},
};
return this.sendPromotionMessage(accessToken, recipient, message);
}
catch (error) {
throw new common_1.ZaloSDKError(`Failed to send newsletter: ${error.message}`, -1, error);
}
}
/**
* Tạo promotion message theo format chuẩn v3.0 (như trong docs)
* @param accessToken Access token của Official Account
* @param recipient Thông tin người nhận
* @param promotionData Dữ liệu promotion
* @returns Thông tin tin nhắn đã gửi
*/
async sendCustomPromotion(accessToken, recipient, promotionData) {
try {
// Validate banner configuration
this.validateBannerConfig(promotionData.banner);
const elements = [
// Banner element
{
type: "banner",
...promotionData.banner,
},
// Header element
{
type: "header",
content: promotionData.headerContent,
},
// Text element
{
type: "text",
align: "left",
content: promotionData.textContent,
},
];
// Add table if provided
if (promotionData.tableData && promotionData.tableData.length > 0) {
elements.push({
type: "table",
content: promotionData.tableData,
});
}
// Add footer text if provided
if (promotionData.footerText) {
elements.push({
type: "text",
align: "center",
content: promotionData.footerText,
});
}
const message = {
type: "promotion",
attachment: {
type: "template",
payload: {
template_type: "promotion",
language: promotionData.language || "VI",
elements,
buttons: promotionData.buttons.map(btn => this.convertToTypedButton(btn)),
},
},
};
return this.sendPromotionMessage(accessToken, recipient, message);
}
catch (error) {
throw new common_1.ZaloSDKError(`Failed to send custom promotion: ${error.message}`, -1, error);
}
}
/**
* Validate banner configuration
* @param banner Banner configuration to validate
*/
validateBannerConfig(banner) {
if (!banner.image_url && !banner.attachment_id) {
throw new common_1.ZaloSDKError("Banner must have either image_url or attachment_id", -1);
}
if (banner.image_url && banner.attachment_id) {
throw new common_1.ZaloSDKError("Banner cannot have both image_url and attachment_id. Use only one.", -1);
}
}
/**
* Validate promotion message format
* @param message Promotion message to validate
*/
validatePromotionMessage(message) {
if (!message.attachment) {
throw new common_1.ZaloSDKError("Promotion message must have attachment", -1);
}
if (!message.attachment.payload) {
throw new common_1.ZaloSDKError("Promotion message attachment must have payload", -1);
}
if (message.attachment.payload.template_type !== "promotion") {
throw new common_1.ZaloSDKError("Promotion message must use promotion template type", -1);
}
if (!message.attachment.payload.elements || message.attachment.payload.elements.length === 0) {
throw new common_1.ZaloSDKError("Promotion message must have at least one element", -1);
}
}
/**
* Gửi promotion message đến nhiều user_ids với callback tracking
* @param accessToken Access token của Official Account
* @param userIds Danh sách user IDs cần gửi
* @param promotionData Dữ liệu promotion
* @param options Tùy chọn gửi và tracking
* @returns Kết quả gửi đến tất cả users
*/
async sendCustomPromotionToMultipleUsers(accessToken, userIds, promotionData, options) {
try {
// Validate inputs
if (!userIds || userIds.length === 0) {
throw new common_1.ZaloSDKError("User IDs array cannot be empty", -1);
}
// Remove duplicates
const uniqueUserIds = [...new Set(userIds)];
// Validate sending time
this.validateSendingTime();
const startTime = Date.now();
const results = [];
const mode = options?.mode || "sequential";
const delayMs = options?.delayMs || 1000;
const continueOnError = options?.continueOnError !== false;
// Initial progress callback
if (options?.onProgress) {
options.onProgress({
total: uniqueUserIds.length,
completed: 0,
successful: 0,
failed: 0,
currentUserId: null,
startTime,
estimatedTimeRemaining: null,
});
}
if (mode === "parallel") {
// Gửi song song
const promises = uniqueUserIds.map(async (userId) => {
try {
const result = await this.sendCustomPromotion(accessToken, { user_id: userId }, promotionData);
const userResult = {
userId,
success: true,
result,
error: null,
timestamp: new Date(),
};
// Callback cho từng user
if (options?.onUserComplete) {
options.onUserComplete(userResult);
}
return userResult;
}
catch (error) {
const userResult = {
userId,
success: false,
result: null,
error: error instanceof common_1.ZaloSDKError ? error : new common_1.ZaloSDKError(`Failed to send to user ${userId}: ${error.message}`, -1, error),
timestamp: new Date(),
};
// Callback cho từng user
if (options?.onUserComplete) {
options.onUserComplete(userResult);
}
if (!continueOnError) {
throw userResult.error;
}
return userResult;
}
});
const parallelResults = await Promise.all(promises);
results.push(...parallelResults);
}
else {
// Gửi tuần tự
for (let i = 0; i < uniqueUserIds.length; i++) {
const userId = uniqueUserIds[i];
try {
// Progress callback
if (options?.onProgress) {
const completed = i;
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
const elapsed = Date.now() - startTime;
const avgTimePerUser = elapsed / Math.max(completed, 1);
const remaining = uniqueUserIds.length - completed;
const estimatedTimeRemaining = remaining * avgTimePerUser;
options.onProgress({
total: uniqueUserIds.length,
completed,
successful,
failed,
currentUserId: userId,
startTime,
estimatedTimeRemaining,
});
}
const result = await this.sendCustomPromotion(accessToken, { user_id: userId }, promotionData);
const userResult = {
userId,
success: true,
result,
error: null,
timestamp: new Date(),
};
results.push(userResult);
// Callback cho từng user
if (options?.onUserComplete) {
options.onUserComplete(userResult);
}
// Delay giữa các tin nhắn (trừ tin cuối cùng)
if (i < uniqueUserIds.length - 1 && delayMs > 0) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
catch (error) {
const userResult = {
userId,
success: false,
result: null,
error: error instanceof common_1.ZaloSDKError ? error : new common_1.ZaloSDKError(`Failed to send to user ${userId}: ${error.message}`, -1, error),
timestamp: new Date(),
};
results.push(userResult);
// Callback cho từng user
if (options?.onUserComplete) {
options.onUserComplete(userResult);
}
if (!continueOnError) {
throw userResult.error;
}
}
}
}
const endTime = Date.now();
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
// Final progress callback
if (options?.onProgress) {
options.onProgress({
total: uniqueUserIds.length,
completed: uniqueUserIds.length,
successful: successful.length,
failed: failed.length,
currentUserId: null,
startTime,
estimatedTimeRemaining: 0,
});
}
return {
total: uniqueUserIds.length,
successful: successful.length,
failed: failed.length,
results,
executionTime: endTime - startTime,
mode,
startTime: new Date(startTime),
endTime: new Date(endTime),
successRate: (successful.length / uniqueUserIds.length) * 100,
};
}
catch (error) {
throw new common_1.ZaloSDKError(`Failed to send promotion to multiple users: ${error.message}`, -1, error);
}
}
/**
* Gửi promotion message đến nhiều user_ids với promotionData riêng cho từng user
* @param accessToken Access token của Official Account
* @param userPromotions Danh sách user và promotionData tương ứng
* @param options Tùy chọn gửi và tracking
* @returns Kết quả gửi đến tất cả users
*/
async sendPersonalizedPromotionToMultipleUsers(accessToken, userPromotions, options) {
try {
// Validate inputs
if (!userPromotions || userPromotions.length === 0) {
throw new common_1.ZaloSDKError("User promotions array cannot be empty", -1);
}
// Extract unique user IDs
const userIds = userPromotions.map(up => up.userId);
const uniqueUserIds = [...new Set(userIds)];
if (uniqueUserIds.length !== userPromotions.length) {
throw new common_1.ZaloSDKError("Duplicate user IDs found in user promotions", -1);
}
// Validate sending time
this.validateSendingTime();
const startTime = Date.now();
const results = [];
const mode = options?.mode || "sequential";
const delayMs = options?.delayMs || 1000;
const continueOnError = options?.continueOnError !== false;
// Initial progress callback
if (options?.onProgress) {
options.onProgress({
total: uniqueUserIds.length,
completed: 0,
successful: 0,
failed: 0,
currentUserId: null,
startTime,
estimatedTimeRemaining: null,
});
}
if (mode === "parallel") {
// Gửi song song
const promises = userPromotions.map(async (userPromotion) => {
try {
const result = await this.sendCustomPromotion(accessToken, { user_id: userPromotion.userId }, userPromotion.promotionData);
const userResult = {
userId: userPromotion.userId,
success: true,
result,
error: null,
timestamp: new Date(),
};
// Callback cho từng user
if (options?.onUserComplete) {
options.onUserComplete(userResult);
}
return userResult;
}
catch (error) {
const userResult = {
userId: userPromotion.userId,
success: false,
result: null,
error: error instanceof common_1.ZaloSDKError ? error : new common_1.ZaloSDKError(`Failed to send to user ${userPromotion.userId}: ${error.message}`, -1, error),
timestamp: new Date(),
};
// Callback cho từng user
if (options?.onUserComplete) {
options.onUserComplete(userResult);
}
if (!continueOnError) {
throw userResult.error;
}
return userResult;
}
});
const parallelResults = await Promise.all(promises);
results.push(...parallelResults);
}
else {
// Gửi tuần tự
for (let i = 0; i < userPromotions.length; i++) {
const userPromotion = userPromotions[i];
try {
// Progress callback
if (options?.onProgress) {
const completed = i;
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
const elapsed = Date.now() - startTime;
const avgTimePerUser = elapsed / Math.max(completed, 1);
const remaining = userPromotions.length - completed;
const estimatedTimeRemaining = remaining * avgTimePerUser;
options.onProgress({
total: userPromotions.length,
completed,
successful,
failed,
currentUserId: userPromotion.userId,
startTime,
estimatedTimeRemaining,
});
}
const result = await this.sendCustomPromotion(accessToken, { user_id: userPromotion.userId }, userPromotion.promotionData);
const userResult = {
userId: userPromotion.userId,
success: true,
result,
error: null,
timestamp: new Date(),
};
results.push(userResult);
// Callback cho từng user
if (options?.onUserComplete) {
options.onUserComplete(userResult);
}
// Delay giữa các tin nhắn (trừ tin cuối cùng)
if (i < userPromotions.length - 1 && delayMs > 0) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
catch (error) {
const userResult = {
userId: userPromotion.userId,
success: false,
result: null,
error: error instanceof common_1.ZaloSDKError ? error : new common_1.ZaloSDKError(`Failed to send to user ${userPromotion.userId}: ${error.message}`, -1, error),
timestamp: new Date(),
};
results.push(userResult);
// Callback cho từng user
if (options?.onUserComplete) {
options.onUserComplete(userResult);
}
if (!continueOnError) {
throw userResult.error;
}
}
}
}
const endTime = Date.now();
const successful = results.filter(r => r.success);
const failed = results.filter(r => !r.success);
// Final progress callback
if (options?.onProgress) {
options.onProgress({
total: userPromotions.length,
completed: userPromotions.length,
successful: successful.length,
failed: failed.length,
currentUserId: null,
startTime,
estimatedTimeRemaining: 0,
});
}
return {
total: userPromotions.length,
successful: successful.length,
failed: failed.length,
results,
executionTime: endTime - startTime,
mode,
startTime: new Date(startTime),
endTime: new Date(endTime),
successRate: (successful.length / userPromotions.length) * 100,
};
}
catch (error) {
throw new common_1.ZaloSDKError(`Failed to send personalized promotion to multiple users: ${error.message}`, -1, error);
}
}
/**
* Validate sending time (8:00 - 22:00)
*/
validateSendingTime() {
const now = new Date();
const hour = now.getHours();
if (hour < 8 || hour >= 22) {
throw new common_1.ZaloSDKError("Tin nhắn truyền thông chỉ được gửi trong khung giờ từ 8:00 - 22:00", 2001);
}
}
}
exports.PromotionService = PromotionService;
//# sourceMappingURL=promotion.service.js.map