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

985 lines (793 loc) 26.1 kB
# RedAI Zalo SDK - ZNS Service Guide ## Tổng quan **Zalo Notification Service (ZNS)** là dịch vụ gửi tin nhắn thông báo chính thức từ doanh nghiệp đến khách hàng thông qua các template được duyệt trước. ZNS phù hợp cho: - 📋 **Thông báo đơn hàng** - Xác nhận, cập nhật trạng thái đơn hàng - 🎫 **Thông báo lịch hẹn** - Nhắc nhở cuộc hẹn, booking - 💰 **Thông báo thanh toán** - Hóa đơn, biên lai, giao dịch - 📢 **Thông báo khuyến mại** - Ưu đãi, chương trình đặc biệt - ⚠️ **Cảnh báo bảo mật** - OTP, xác thực 2FA - 📊 **Báo cáo định kỳ** - Báo cáo tài chính, thống kê --- ## Khởi tạo ZNS Service ```typescript import { ZaloSDK } from "@warriorteam/redai-zalo-sdk"; const zalo = new ZaloSDK({ appId: "your-oa-app-id", appSecret: "your-oa-app-secret", debug: true }); // Access ZNS service const znsService = zalo.zns; ``` --- ## Gửi Tin nhắn ZNS ### 1. Gửi tin nhắn cơ bản ```typescript // Gửi thông báo đơn hàng const znsResponse = await zalo.zns.sendMessage(accessToken, { phone: "0123456789", // Số điện thoại người nhận template_id: "your-template-id", // ID template đã được duyệt template_data: { customer_name: "Nguyễn Văn An", order_id: "DH001234", total_amount: "500,000", delivery_date: "15/12/2024" }, tracking_id: "tracking_001" // ID để track (tùy chọn) }); console.log("Message sent:", znsResponse.msg_id); ``` ### 2. Gửi với chế độ development ```typescript // Test trong môi trường development const testResponse = await zalo.zns.sendMessage(accessToken, { phone: "0123456789", template_id: "template_id", template_data: { customer_name: "Test User", order_id: "TEST001" }, mode: "development" // Không trừ quota, chỉ test }); ``` ### 3. Gửi tin nhắn có mã hóa (Hash Phone) ```typescript // Hash phone number để bảo mật import crypto from 'crypto'; function hashPhone(phone: string, secretKey: string): string { return crypto .createHmac('sha256', secretKey) .update(phone) .digest('hex'); } const hashedPhone = hashPhone("0123456789", "your-secret-key"); const response = await zalo.zns.sendMessage(accessToken, { phone: hashedPhone, template_id: "template_id", template_data: { name: "User" }, is_hash_phone: true // Báo cho Zalo biết phone đã được hash }); ``` --- ## Quản lý Templates ### 1. Lấy danh sách templates ```typescript const templates = await zalo.zns.getTemplateList(accessToken, { offset: 0, limit: 20, status: 1 // 1: active, 2: pending, 3: rejected }); console.log("Total templates:", templates.total); templates.data.forEach(template => { console.log(`Template ID: ${template.templateId}`); console.log(`Name: ${template.templateName}`); console.log(`Status: ${template.status}`); console.log(`Content: ${template.previewContent}`); }); ``` ### 2. Tạo template mới ```typescript const newTemplate = await zalo.zns.createTemplate(accessToken, { templateName: "Xác nhận đơn hàng", templateContent: `Xin chào {{customer_name}}, Đơn hàng {{order_id}} của bạn đã được xác nhận. Tổng tiền: {{total_amount}} VNĐ Ngày giao hàng dự kiến: {{delivery_date}} Cảm ơn bạn đã mua hàng!`, templateType: 3, // 1: OTP, 2: Thông báo, 3: Xác nhận, 4: Khuyến mại timeout: 3600, // Thời gian hiệu lực (seconds) previewData: JSON.stringify({ customer_name: "Nguyễn Văn A", order_id: "DH001", total_amount: "299,000", delivery_date: "20/12/2024" }), hasHashPhone: false, // Có hỗ trợ hash phone không hasAttachment: false // Có đính kèm file không }); console.log("Template created:", newTemplate.templateId); console.log("Status:", newTemplate.status); // 2: pending approval ``` ### 3. Cập nhật template ```typescript const updatedTemplate = await zalo.zns.updateTemplate(accessToken, "template-id", { templateName: "Xác nhận đơn hàng - Updated", templateContent: `Kính chào {{customer_name}}, Đơn hàng #{{order_id}} đã được xác nhận. Giá trị: {{total_amount}} VNĐ Giao hàng: {{delivery_date}} Hotline: 1800-xxx-xxx`, previewData: JSON.stringify({ customer_name: "Khách hàng", order_id: "DH999", total_amount: "1,500,000", delivery_date: "25/12/2024" }) }); ``` ### 4. Kiểm tra trạng thái template ```typescript const templateStatus = await zalo.zns.getTemplateStatus(accessToken, "template-id"); console.log("Template Status:", templateStatus.status); // 1: Active (đã duyệt) // 2: Pending (chờ duyệt) // 3: Rejected (bị từ chối) // 4: Disabled (đã tắt) if (templateStatus.status === 3) { console.log("Rejection reason:", templateStatus.rejectReason); } ``` --- ## Quản lý Quota ### 1. Kiểm tra quota hiện tại ```typescript const quotaInfo = await zalo.zns.getQuotaInfo(accessToken); console.log("Remaining quota:", quotaInfo.remainingQuota); console.log("Daily quota:", quotaInfo.dailyQuota); console.log("Monthly quota:", quotaInfo.monthlyQuota); // Quota theo loại template quotaInfo.quotaByType?.forEach(quota => { console.log(`Type ${quota.type}: ${quota.remaining}/${quota.total}`); }); ``` ### 2. Kiểm tra quota trước khi gửi ```typescript async function sendZNSWithQuotaCheck( accessToken: string, request: ZNSMessageRequest ): Promise<boolean> { // Kiểm tra quota trước const quota = await zalo.zns.getQuotaInfo(accessToken); if (quota.remainingQuota <= 0) { console.log("⚠️ Không đủ quota để gửi tin nhắn"); return false; } if (quota.remainingQuota < 10) { console.log(`⚠️ Cảnh báo: Chỉ còn ${quota.remainingQuota} quota`); } try { const result = await zalo.zns.sendMessage(accessToken, request); console.log(`✅ Gửi thành công. Quota còn lại: ${quota.remainingQuota - 1}`); return true; } catch (error) { console.error("❌ Gửi thất bại:", error); return false; } } ``` --- ## Template Data & Variables ### 1. Template Variables Templates hỗ trợ các biến động với format `{{variable_name}}`: ```typescript // Template content const templateContent = ` Xin chào {{customer_name}}, Mã OTP của bạn là: {{otp_code}} Mã này có hiệu lực trong {{validity_minutes}} phút. Vui lòng không chia sẻ mã này với bất kỳ ai. `; // Template data const templateData = { customer_name: "Nguyễn Văn A", otp_code: "123456", validity_minutes: "5" }; ``` ### 2. Data Type Handling ```typescript // Số và ngày tháng nên format thành string const templateData = { // ✅ Đúng amount: "1,500,000", date: "25/12/2024", phone: "0123-456-789", // ❌ Sai - sẽ gây lỗi amount: 1500000, date: new Date(), phone: 123456789 }; ``` ### 3. Conditional Variables ```typescript // Template với logic điều kiện const templateContent = ` {{#if is_vip}} Kính gửi Khách hàng VIP {{customer_name}}, {{else}} Xin chào {{customer_name}}, {{/if}} Đơn hàng {{order_id}} đã được {{status}}. {{#if has_discount}} Bạn được giảm {{discount_amount}} cho đơn hàng này. {{/if}} Cảm ơn bạn! `; const templateData = { customer_name: "Nguyễn Văn A", order_id: "DH001", status: "xác nhận", is_vip: true, has_discount: true, discount_amount: "50,000 VNĐ" }; ``` --- ## Use Cases & Examples ### 1. E-commerce Order Notifications ```typescript class OrderNotificationService { constructor(private zalo: ZaloSDK, private accessToken: string) {} async sendOrderConfirmation(order: Order) { return await this.zalo.zns.sendMessage(this.accessToken, { phone: order.customer.phone, template_id: "order_confirmation_template", template_data: { customer_name: order.customer.name, order_id: order.id, total_amount: this.formatCurrency(order.totalAmount), items_summary: this.getItemsSummary(order.items), delivery_date: this.formatDate(order.estimatedDelivery), tracking_url: `https://mystore.com/track/${order.id}` }, tracking_id: `order_${order.id}` }); } async sendShippingUpdate(order: Order, status: string) { return await this.zalo.zns.sendMessage(this.accessToken, { phone: order.customer.phone, template_id: "shipping_update_template", template_data: { customer_name: order.customer.name, order_id: order.id, status: status, tracking_code: order.trackingCode, estimated_delivery: this.formatDate(order.estimatedDelivery) } }); } private formatCurrency(amount: number): string { return new Intl.NumberFormat('vi-VN').format(amount) + ' VNĐ'; } private formatDate(date: Date): string { return date.toLocaleDateString('vi-VN'); } private getItemsSummary(items: OrderItem[]): string { return items.map(item => `${item.name} x${item.quantity}`).join(', '); } } ``` ### 2. Booking & Appointment System ```typescript class BookingNotificationService { constructor(private zalo: ZaloSDK, private accessToken: string) {} async sendBookingConfirmation(booking: Booking) { return await this.zalo.zns.sendMessage(this.accessToken, { phone: booking.customer.phone, template_id: "booking_confirmation_template", template_data: { customer_name: booking.customer.name, service_name: booking.service.name, booking_date: this.formatDateTime(booking.appointmentTime), location: booking.location.address, staff_name: booking.staff.name, booking_id: booking.id, cancel_url: `https://mybusiness.com/cancel/${booking.id}` } }); } async sendReminder(booking: Booking, hoursUntilAppointment: number) { return await this.zalo.zns.sendMessage(this.accessToken, { phone: booking.customer.phone, template_id: "appointment_reminder_template", template_data: { customer_name: booking.customer.name, service_name: booking.service.name, appointment_time: this.formatDateTime(booking.appointmentTime), hours_until: hoursUntilAppointment.toString(), location: booking.location.address, contact_phone: "1800-xxx-xxx" } }); } private formatDateTime(date: Date): string { return date.toLocaleString('vi-VN'); } } ``` ### 3. Payment & Financial Notifications ```typescript class PaymentNotificationService { constructor(private zalo: ZaloSDK, private accessToken: string) {} async sendPaymentConfirmation(payment: Payment) { return await this.zalo.zns.sendMessage(this.accessToken, { phone: payment.customer.phone, template_id: "payment_confirmation_template", template_data: { customer_name: payment.customer.name, amount: this.formatCurrency(payment.amount), payment_method: payment.method, transaction_id: payment.transactionId, payment_date: this.formatDateTime(payment.createdAt), merchant_name: payment.merchant.name, receipt_url: `https://pay.com/receipt/${payment.id}` } }); } async sendInvoice(invoice: Invoice) { return await this.zalo.zns.sendMessage(this.accessToken, { phone: invoice.customer.phone, template_id: "invoice_template", template_data: { customer_name: invoice.customer.name, invoice_number: invoice.number, amount: this.formatCurrency(invoice.totalAmount), due_date: this.formatDate(invoice.dueDate), payment_url: `https://pay.com/invoice/${invoice.id}`, company_name: invoice.company.name } }); } } ``` ### 4. OTP & Security Messages ```typescript class SecurityNotificationService { constructor(private zalo: ZaloSDK, private accessToken: string) {} async sendOTP(phone: string, otpCode: string, purpose: string) { return await this.zalo.zns.sendMessage(this.accessToken, { phone: phone, template_id: "otp_template", template_data: { otp_code: otpCode, purpose: purpose, validity_minutes: "5", app_name: "MyApp" } }); } async sendSecurityAlert(user: User, activity: string, location: string) { return await this.zalo.zns.sendMessage(this.accessToken, { phone: user.phone, template_id: "security_alert_template", template_data: { customer_name: user.name, activity: activity, time: this.formatDateTime(new Date()), location: location, ip_address: this.getCurrentIP(), support_phone: "1800-xxx-xxx" } }); } } ``` --- ## Batch Processing ### 1. Gửi tin nhắn hàng loạt ```typescript class BatchZNSService { constructor(private zalo: ZaloSDK, private accessToken: string) {} async sendBatchMessages( templateId: string, recipients: Array<{phone: string, data: Record<string, any>}> ) { const results = []; const batchSize = 10; // Gửi 10 tin mỗi lần để tránh rate limit for (let i = 0; i < recipients.length; i += batchSize) { const batch = recipients.slice(i, i + batchSize); const batchPromises = batch.map(async (recipient) => { try { const result = await this.zalo.zns.sendMessage(this.accessToken, { phone: recipient.phone, template_id: templateId, template_data: recipient.data, tracking_id: `batch_${Date.now()}_${i}` }); return { phone: recipient.phone, success: true, messageId: result.msg_id }; } catch (error) { return { phone: recipient.phone, success: false, error: error.message }; } }); const batchResults = await Promise.all(batchPromises); results.push(...batchResults); // Delay giữa các batch để tránh rate limit if (i + batchSize < recipients.length) { await this.delay(1000); // 1 second delay } } return results; } private delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } } // Usage const batchService = new BatchZNSService(zalo, accessToken); const recipients = [ { phone: "0123456789", data: { name: "Nguyễn Văn A", code: "ABC123" } }, { phone: "0987654321", data: { name: "Trần Thị B", code: "XYZ789" } } // ... more recipients ]; const results = await batchService.sendBatchMessages("template_id", recipients); console.log(`Sent ${results.filter(r => r.success).length}/${results.length} messages successfully`); ``` --- ## Error Handling ### 1. Common ZNS Errors ```typescript try { await zalo.zns.sendMessage(accessToken, request); } catch (error) { switch (error.code) { case -216: console.error("Invalid access token"); break; case -223: console.error("Quota exceeded"); break; case -224: console.error("Template not found or not approved"); break; case -225: console.error("Invalid phone number"); break; case -226: console.error("Invalid template data"); break; case -227: console.error("Template content mismatch"); break; default: console.error("Unexpected error:", error.message); } } ``` ### 2. Retry Logic với Exponential Backoff ```typescript class ZNSRetryService { constructor(private zalo: ZaloSDK, private accessToken: string) {} async sendWithRetry( request: ZNSMessageRequest, maxRetries = 3 ): Promise<any> { let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await this.zalo.zns.sendMessage(this.accessToken, request); } catch (error) { lastError = error; // Không retry với một số lỗi cố định if (this.shouldNotRetry(error.code)) { throw error; } if (attempt < maxRetries) { const delay = Math.pow(2, attempt) * 1000; // Exponential backoff console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError; } private shouldNotRetry(errorCode: number): boolean { // Không retry với các lỗi sau const nonRetryableCodes = [-224, -225, -226, -227]; // Template/data errors return nonRetryableCodes.includes(errorCode); } } ``` --- ## Monitoring & Analytics ### 1. Message Tracking ```typescript class ZNSAnalyticsService { constructor(private zalo: ZaloSDK, private accessToken: string) {} async trackMessageDelivery(trackingId: string) { // Implement your tracking logic const deliveryStatus = await this.getDeliveryStatus(trackingId); return { trackingId, status: deliveryStatus.status, // sent, delivered, failed sentAt: deliveryStatus.sentAt, deliveredAt: deliveryStatus.deliveredAt, failureReason: deliveryStatus.failureReason }; } async generateQuotaReport(period: 'daily' | 'monthly') { const quota = await this.zalo.zns.getQuotaInfo(this.accessToken); return { period, totalQuota: quota.dailyQuota, usedQuota: quota.dailyQuota - quota.remainingQuota, remainingQuota: quota.remainingQuota, utilizationRate: ((quota.dailyQuota - quota.remainingQuota) / quota.dailyQuota * 100).toFixed(2) + '%' }; } } ``` ### 2. Performance Monitoring ```typescript class ZNSPerformanceMonitor { private metrics = { totalSent: 0, successful: 0, failed: 0, averageResponseTime: 0 }; async sendMessageWithMonitoring( zalo: ZaloSDK, accessToken: string, request: ZNSMessageRequest ) { const startTime = Date.now(); try { const result = await zalo.zns.sendMessage(accessToken, request); this.metrics.totalSent++; this.metrics.successful++; this.updateResponseTime(Date.now() - startTime); console.log(`✅ Message sent successfully. Success rate: ${this.getSuccessRate()}%`); return result; } catch (error) { this.metrics.totalSent++; this.metrics.failed++; console.log(`❌ Message failed. Success rate: ${this.getSuccessRate()}%`); throw error; } } private getSuccessRate(): number { if (this.metrics.totalSent === 0) return 0; return (this.metrics.successful / this.metrics.totalSent * 100).toFixed(2); } private updateResponseTime(responseTime: number) { this.metrics.averageResponseTime = (this.metrics.averageResponseTime * (this.metrics.successful - 1) + responseTime) / this.metrics.successful; } getMetrics() { return { ...this.metrics }; } } ``` --- ## Best Practices ### 1. Template Design ```typescript // ✅ Template tốt - rõ ràng, súc tích const goodTemplate = ` Xin chào {{customer_name}}, Đơn hàng {{order_id}} đã được xác nhận thành công. • Tổng tiền: {{total_amount}} VNĐ • Ngày giao: {{delivery_date}} • Mã vận đơn: {{tracking_code}} Cảm ơn bạn đã tin tưởng! Hotline: {{support_phone}} `; // ❌ Template kém - quá dài, không có cấu trúc const badTemplate = ` Kính chào quý khách hàng {{customer_name}} của công ty chúng tôi. Chúng tôi xin thông báo rằng đơn hàng có mã số {{order_id}} của quý khách đã được xác nhận và đang trong quá trình xử lý... `; ``` ### 2. Phone Number Validation ```typescript function validatePhoneNumber(phone: string): boolean { // Vietnamese phone number regex const phoneRegex = /^(0[3-9][0-9]{8})$/; return phoneRegex.test(phone.replace(/[\s\-\.]/g, '')); } function formatPhoneNumber(phone: string): string { // Remove all non-digits const cleaned = phone.replace(/\D/g, ''); // Add country code if missing if (cleaned.length === 10 && cleaned.startsWith('0')) { return cleaned; } else if (cleaned.length === 9) { return '0' + cleaned; } throw new Error('Invalid phone number format'); } ``` ### 3. Template Data Validation ```typescript function validateTemplateData(template: string, data: Record<string, any>): boolean { // Extract variables from template const variables = template.match(/\{\{(\w+)\}\}/g)?.map(match => match.replace(/\{\{|\}\}/g, '') ) || []; // Check if all variables are provided const missingVars = variables.filter(variable => !(variable in data)); if (missingVars.length > 0) { throw new Error(`Missing template variables: ${missingVars.join(', ')}`); } // Validate data types (all should be strings) const invalidTypes = Object.entries(data) .filter(([key, value]) => typeof value !== 'string') .map(([key]) => key); if (invalidTypes.length > 0) { throw new Error(`Invalid data types for: ${invalidTypes.join(', ')}. All values must be strings.`); } return true; } ``` --- ## Testing ZNS ### 1. Development Mode Testing ```typescript // Test template trong development mode async function testTemplate(templateId: string) { const testData = { customer_name: "Nguyễn Test", order_id: "TEST001", total_amount: "100,000" }; try { const result = await zalo.zns.sendMessage(accessToken, { phone: "0123456789", // Test phone number template_id: templateId, template_data: testData, mode: "development" // Không trừ quota }); console.log("✅ Template test successful:", result.msg_id); return true; } catch (error) { console.error("❌ Template test failed:", error.message); return false; } } ``` ### 2. Unit Tests ```typescript // zns.test.ts import { ZaloSDK } from '@warriorteam/redai-zalo-sdk'; describe('ZNS Service', () => { const zalo = new ZaloSDK({ appId: 'test_app_id', appSecret: 'test_app_secret' }); it('should send ZNS message successfully', async () => { const mockResponse = { error: 0, message: "Success", data: { msg_id: "test_msg_id" } }; // Mock the API call jest.spyOn(zalo.zns, 'sendMessage').mockResolvedValue(mockResponse); const result = await zalo.zns.sendMessage('test_token', { phone: '0123456789', template_id: 'test_template', template_data: { name: 'Test User' } }); expect(result.msg_id).toBe('test_msg_id'); }); }); ``` --- ## Production Checklist ### 1. Trước khi Go Live - [ ] Templates đã được Zalo duyệt - [ ] Test tất cả templates trong development mode - [ ] Setup monitoring và logging - [ ] Implement error handling và retry logic - [ ] Validate phone numbers trước khi gửi - [ ] Setup quota monitoring và alerts - [ ] Backup và recovery plan - [ ] Performance testing với volume cao ### 2. Monitoring Production ```typescript // Production monitoring setup class ProductionZNSMonitor { constructor(private zalo: ZaloSDK, private accessToken: string) {} async healthCheck(): Promise<boolean> { try { const quota = await this.zalo.zns.getQuotaInfo(this.accessToken); // Alert nếu quota thấp if (quota.remainingQuota < 100) { this.sendAlert(`⚠️ ZNS Quota thấp: ${quota.remainingQuota} còn lại`); } return true; } catch (error) { this.sendAlert(`❌ ZNS Health check failed: ${error.message}`); return false; } } private sendAlert(message: string) { // Send to your monitoring system (Slack, email, etc.) console.error(message); } } ``` --- ## Troubleshooting ### 1. Common Issues **Q: Template không gửi được** ``` A: Kiểm tra: - Template đã được duyệt chưa (status = 1) - Template data có đúng format không - Phone number có hợp lệ không - Còn quota không ``` **Q: "Template content mismatch" error** ``` A: Template data phải khớp chính xác với template content - Tên biến phải giống nhau - Không được thiếu biến - Tất cả giá trị phải là string ``` ### 2. Debug ZNS Issues ```typescript async function debugZNSMessage( zalo: ZaloSDK, accessToken: string, request: ZNSMessageRequest ) { console.log("🔍 Debug ZNS Request:"); console.log("Phone:", request.phone); console.log("Template ID:", request.template_id); console.log("Template Data:", JSON.stringify(request.template_data, null, 2)); // Check quota first try { const quota = await zalo.zns.getQuotaInfo(accessToken); console.log("📊 Quota Info:", quota); } catch (error) { console.error("❌ Failed to get quota:", error); } // Check template status try { const template = await zalo.zns.getTemplateStatus(accessToken, request.template_id); console.log("📝 Template Status:", template); } catch (error) { console.error("❌ Failed to get template status:", error); } // Attempt to send try { const result = await zalo.zns.sendMessage(accessToken, request); console.log("✅ Message sent successfully:", result); return result; } catch (error) { console.error("❌ Failed to send message:", error); throw error; } } ``` --- ## Next Steps Sau khi làm chủ ZNS Service: 1. **[Message Services](./MESSAGE_SERVICES.md)** - Tìm hiểu các loại tin nhắn khác 2. **[Webhook Events](./WEBHOOK_EVENTS.md)** - Xử lý ZNS delivery events 3. **[User Management](./USER_MANAGEMENT.md)** - Quản lý danh sách khách hàng 4. **[Error Handling](./ERROR_HANDLING.md)** - Xử lý lỗi toàn diện Tham khảo **[API Reference](./API_REFERENCE.md)** để biết chi tiết về tất cả ZNS methods.