UNPKG

@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
"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