Task BE - Hỗ trợ cấu hình WhatsApp bằng Template ID

1. Mục tiêu

Bổ sung một chế độ cấu hình action WhatsApp bằng template đã được phê duyệt trên Meta/ADA. Người dùng có thể:

Đây là một task backend. Tài liệu mô tả cả thao tác của người dùng và FE để làm rõ contract BE cần cung cấp, nhưng không bao gồm việc triển khai giao diện.

Kết quả cần đạt:

2. Hiện trạng

BE đã có:

GET /api/v1/whatsapp/message_templates
GET /api/v1/whatsapp/{id}

WhatsappService đã gọi Meta Graph API. Tuy nhiên API chi tiết hiện tại:

WhatsappData hiện chỉ lưu:

content, trackOpen, trackingLinks, brandId

Worker hiện render toàn bộ content, chèn tracking rồi gửi ADA bằng template name cố định trong biến môi trường:

{
  "type": "template",
  "templateName": "<fixed-template-name>",
  "templateLang": "en",
  "templateData": ["<rendered-content>"]
}

Vì vậy, chỉ thêm ô nhập Template ID trên FE là chưa đủ. BE phải resolve Template ID thành templateName, templateLanguage và danh sách parameter theo đúng thứ tự mà ADA yêu cầu.

3. Phạm vi

Task này bao gồm:

Phiên bản đầu chỉ hỗ trợ parameter dạng text trong BODY, vì payload ADA hiện tại chỉ thể hiện parameter bằng List templateData.

Header và button không có biến vẫn được phép. Template có biến trong HEADER, media, dynamic URL button, OTP hoặc Flow phải được trả về là không hỗ trợ. Không được âm thầm bỏ qua parameter rồi vẫn cho lưu Campaign.

Không thuộc task này:

4. Sơ đồ luồng cấu hình

[Diagram]

FE không gọi Meta trực tiếp. Token, WABA ID và lỗi kỹ thuật từ Meta chỉ tồn tại ở BE.

5. Sơ đồ luồng gửi tin

[Diagram]

6. API lấy chi tiết template

Tái sử dụng endpoint hiện có:

GET /api/v1/whatsapp/{templateId}

Xử lý tại BE

  1. Trim và kiểm tra templateId không rỗng, đúng định dạng ID dự kiến.
  2. Gọi Meta bằng WHATSAPP_TOKEN, WHATSAPP_BASE_URL, WHATSAPP_API_VERSION và WHATSAPP_BUSINESS_ACCOUNT_ID hiện có.
  3. Yêu cầu tối thiểu các field:
id,name,language,status,category,components
  1. Xác minh template có thể được sử dụng bởi WABA đang cấu hình.
  2. Chỉ cho phép template có status = APPROVED.
  3. Đọc component BODY, trích xuất {{n}} và sắp xếp tăng dần.
  4. Kiểm tra các parameter có liên tục từ 1 đến n.
  5. Đánh dấu không hỗ trợ nếu template có component động ngoài contract ADA hiện tại.

Không coi name, language, status hoặc components do FE gửi lên là dữ liệu tin cậy.

Response đề xuất

Giữ wrapper ResponseDto hiện tại. Phần data:

{
  "id": "123456789",
  "name": "campaign_offer",
  "language": "vi",
  "status": "APPROVED",
  "category": "MARKETING",
  "supported": true,
  "unsupportedReason": null,
  "components": [
    {
      "type": "BODY",
      "text": "Xin chào {{1}}, xem ưu đãi tại {{2}}"
    }
  ],
  "parameters": [
    {
      "key": "BODY.1",
      "componentType": "BODY",
      "position": 1,
      "example": "Nguyễn Văn A",
      "trackingAllowed": true
    },
    {
      "key": "BODY.2",
      "componentType": "BODY",
      "position": 2,
      "example": "https://example.com",
      "trackingAllowed": true
    }
  ]
}

key là định danh ổn định giữa FE và BE. BE tự parse placeholder; FE không tự regex {{n}} từ nội dung raw của Meta.

7. Dữ liệu lưu trong Campaign

Mở rộng WhatsappData theo hướng tối thiểu:

{
  "mode": "TEMPLATE_ID",
  "templateId": "123456789",
  "templateName": "campaign_offer",
  "templateLanguage": "vi",
  "templateParameters": [
    {
      "key": "BODY.1",
      "position": 1,
      "value": "{{fullName}}"
    },
    {
      "key": "BODY.2",
      "position": 2,
      "value": "{{track.offerLink}}"
    }
  ],
  "trackingLinks": [
    {
      "key": "offerLink",
      "originalUrl": "https://example.com/promotion",
      "trackClick": true
    }
  ],
  "brandId": "..."
}

Quy tắc tương thích:

Không cần thêm enum cho từng loại nguồn parameter trong phiên bản đầu. Trường value tái sử dụng cú pháp placeholder hiện tại; tracking tiếp tục dùng TrackingLinkConfig.

8. Validation khi lưu Campaign

Với TEMPLATE_ID, CampaignService phải:

  1. Lấy lại template từ Meta.
  2. Kiểm tra template vẫn APPROVED và được hỗ trợ.
  3. So sánh parameter Meta yêu cầu với templateParameters FE gửi.
  4. Từ chối key thiếu, key thừa, position trùng hoặc position không liên tục.
  5. Sắp xếp theo position, không dựa vào thứ tự mảng FE gửi.
  6. Validate campaign placeholder trong từng value.
  7. Materialize {{track}} thành {{track.key}}.
  8. Validate placeholder tracking với trackingLinks.
  9. Từ chối tracking config không được tham chiếu hoặc placeholder thiếu config.
  10. Ghi đè metadata template bằng kết quả Meta mới nhất trước khi lưu.

OnlinePurchaseCustomerCareValidator hiện cũng yêu cầu trực tiếp whatsappData.content. Điều kiện này phải đổi thành:

CUSTOM_CONTENT -> content có giá trị
TEMPLATE_ID    -> templateId và templateParameters hợp lệ

Nếu Meta tạm thời không truy cập được, không lưu một cấu hình mới dưới trạng thái đã xác minh. Campaign đã được lưu trước đó vẫn chạy bằng snapshot.

9. Xử lý tracking link

Tracking link không được tạo khi lấy template hoặc khi lưu Campaign. Mỗi recipient cần một short code riêng nên link chỉ được tạo tại worker.

[Diagram]

Ví dụ:

Recipient A -> https://laoads.com/s/<shortCode-A>
Recipient B -> https://laoads.com/s/<shortCode-B>

Cần mở rộng logic tracking dùng chung để xử lý toàn bộ danh sách parameter và chỉ tạo một tracking record cho mỗi config/recipient. Không gọi applyWhatsappTracking() riêng cho từng parameter với toàn bộ trackingLinks, vì có thể tạo bản ghi trùng.

10. Xử lý tại WhatsApp worker

Chế độ CUSTOM_CONTENT

Giữ nguyên luồng hiện tại để không ảnh hưởng Campaign cũ.

Chế độ TEMPLATE_ID

  1. Kiểm tra template snapshot và parameter list tồn tại.
  2. Sắp xếp parameter theo position.
  3. Render campaign/recipient placeholder trong từng value.
  4. Tạo tracking URL và thay placeholder trên toàn bộ parameter.
  5. Kiểm tra không còn placeholder chưa render.
  6. Nếu dữ liệu không hợp lệ, phát result thất bại và không gọi ADA.
  7. Gửi payload:
{
  "platform": "WA",
  "from": "<resolved-brand-number>",
  "to": "<recipient-phone>",
  "type": "template",
  "templateName": "campaign_offer",
  "templateLang": "vi",
  "templateData": [
    "Nguyễn Văn A",
    "https://laoads.com/s/AbCd1234"
  ]
}

Trong chế độ này, worker không dùng WHATSAPP_TEMPLATE_URL_* để chọn template. ADA nhận template name và language, không nhận Meta Template ID. Vì vậy Template ID phải được resolve khi validate và lưu Campaign.

Luồng check status, lưu pending, reconcile và phát delivery event giữ nguyên.

11. Xử lý lỗi

Map lỗi thành LaoAdsExceptionStatusCodeEnum; không trả nguyên message nội bộ của Meta cho client.

Trường hợp HTTP Mã lỗi đề xuất
ID rỗng hoặc sai định dạng 400 WHATSAPP_TEMPLATE_INVALID_ID
Không tồn tại hoặc không có quyền truy cập 404 WHATSAPP_TEMPLATE_NOT_ACCESSIBLE
Không thuộc WABA đang cấu hình 422 WHATSAPP_TEMPLATE_ACCOUNT_MISMATCH
Template chưa APPROVED 422 WHATSAPP_TEMPLATE_NOT_APPROVED
Có component ADA chưa hỗ trợ 422 WHATSAPP_TEMPLATE_UNSUPPORTED
Thiếu, thừa hoặc sai parameter 400 WHATSAPP_TEMPLATE_PARAMETER_INVALID
Meta timeout hoặc lỗi 5xx 502 WHATSAPP_TEMPLATE_SERVICE_UNAVAILABLE
Worker không render đủ parameter Result FAILED WORKER_EVENT1001 hoặc mã riêng nếu cần

Nếu Meta không phân biệt được “không tồn tại” và “không có quyền”, BE trả chung WHATSAPP_TEMPLATE_NOT_ACCESSIBLE, không suy đoán nguyên nhân.

12. Bảo mật và vận hành

13. File dự kiến thay đổi

laoads/.../controller/WhatsappController.java
laoads/.../service/WhatsappService.java
laoads/.../model/dto/response/WhatsappMessageTemplateDetailResponse.java
laoads/.../service/CampaignService.java
laoads/.../service/campaign/OnlinePurchaseCustomerCareValidator.java
common/.../campaign/action_data/WhatsappData.java
common/.../campaign/action_data/WhatsappTemplateParameter.java
common/.../enums/StatusCodeEnum.java
common/.../service/tracking/WorkerTrackingService.java
whatsapp-worker/.../WhatsappActionExecutor.java
whatsapp-worker/.../WhatsappClient.java

Không tạo controller hoặc service mới khi WhatsappControllerWhatsappService hiện tại đã đúng ranh giới trách nhiệm.

14. Điều kiện nghiệm thu

15. Kiểm tra tối thiểu