UNPKG

@warriorteam/redai-zalo-sdk

Version:

Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account, ZNS, Consultation Service, Group Messaging, and Social APIs

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.