MktZaloService (Zalo Marketing Integration)
Overview
- Purpose: Microservice for integrating with Zalo Official Account (OA) — managing conversations, customers, chatbot automation rules, ZNS message templates, and webhook event processing.
- Port: 5000 (configured in Program.cs via ASPNETCORE_URLS)
- Database: PostgreSQL (connection string:
DefaultConnection or DATABASE_URL)
- Cache: Redis (connection string:
Redis or REDIS_URL) with fallback to in-memory cache
- Architecture: Clean Architecture + CQRS (MediatR 12.4.1)
- API Versioning: URL segment
api/v{version:apiVersion} (v1.0)
- Note: Some .csproj files still use template naming (MyService.API.csproj, MyService.Domain.csproj, MyService.Infrastructure.csproj)
API Endpoints
WebhooksController (api/v1/webhooks) — AllowAnonymous
| Method |
Route |
Description |
| GET |
/webhooks/zalo |
Zalo webhook challenge verification (returns challenge value) |
| POST |
/webhooks/zalo |
Receive Zalo webhook events; verifies X-ZaloOA-Signature via HMAC-SHA256 |
Supported webhook events:
user_send_text — Text message from user
user_send_image — Image message from user
user_send_file — File message from user
user_send_sticker — Sticker message from user
follow — User follows OA
unfollow — User unfollows OA
ConversationsController (api/v1/conversations) — Authorize
| Method |
Route |
Description |
| GET |
/conversations/{id}?skip&take |
Get conversation with paginated messages |
| POST |
/conversations/{id}/messages |
Send a message in a conversation |
CustomersController (api/v1/customers) — Authorize
| Method |
Route |
Description |
| GET |
/customers/{id} |
Get customer by ID |
| GET |
/customers/by-zalo/{zaloUserId} |
Get customer by Zalo user ID |
| PATCH |
/customers/{id} |
Update customer profile |
ChatbotRulesController (api/v1/chatbot-rules) — Authorize
| Method |
Route |
Description |
| GET |
/chatbot-rules?includeInactive |
List chatbot rules |
| POST |
/chatbot-rules |
Create a new chatbot rule |
| DELETE |
/chatbot-rules/{id} |
Delete a chatbot rule |
| PATCH |
/chatbot-rules/{id}/toggle?activate |
Toggle rule active/inactive |
Commands (6 total)
Webhook Commands
| Command |
Result |
Description |
ProcessWebhookCommand(EventName, ZaloUserId, MessageText?, MessageId?, Attachments?) |
ProcessWebhookResult(Success, Error?) |
Process incoming Zalo webhook event. Handles: text/image/file/sticker messages, follow/unfollow. Creates customer via GetOrCreate, finds/creates active conversation, adds message, triggers domain event for chatbot auto-response. |
Message Commands
| Command |
Result |
Description |
SendMessageCommand(ConversationId, Content, IsFromBot?) |
SendMessageResult(Success, MessageId?, Error?) |
Send outgoing message via Zalo OA API. Adds message to conversation, sends via IZaloOfficialAccountClient, updates ZaloMessageId on success. |
Customer Commands
| Command |
Result |
Description |
UpdateCustomerCommand(CustomerId, DisplayName, AvatarUrl?, PhoneNumber?, Email?) |
UpdateCustomerResult(Success, Error?) |
Update customer profile fields. |
Chatbot Rule Commands
| Command |
Result |
Description |
CreateChatbotRuleCommand(Name, Description?, Type, Priority, ActionType, ResponseText?, TemplateId?, Conditions) |
CreateChatbotRuleResult(Success, RuleId?, Error?) |
Create a new chatbot automation rule with conditions. |
DeleteChatbotRuleCommand(RuleId) |
DeleteChatbotRuleResult(Success, Error?) |
Delete a chatbot rule. |
ToggleChatbotRuleCommand(RuleId, Activate) |
ToggleChatbotRuleResult(Success, Error?) |
Activate or deactivate a chatbot rule. |
Queries (4 total)
| Query |
Result |
Description |
GetConversationHistoryQuery(ConversationId, Skip?, Take?) |
ConversationHistoryDto? |
Get conversation with paginated messages (default: skip=0, take=50) |
GetCustomerQuery(CustomerId) |
CustomerDto? |
Get customer by internal ID |
GetCustomerByZaloIdQuery(ZaloUserId) |
CustomerDto? |
Get customer by Zalo user ID |
GetChatbotRulesQuery(IncludeInactive?) |
List<ChatbotRuleDto> |
List chatbot rules (optionally include inactive) |
DTOs
- ConversationHistoryDto: ConversationId, ZaloUserId, CustomerId, Status, MessageCount, LastMessagePreview, LastMessageAt, StartedAt, EndedAt, Messages (list of MessageDto)
- MessageDto: Id, Type, Content, Direction, IsFromBot, SentAt, ZaloMessageId
- CustomerDto: Id, ZaloUserId, DisplayName, AvatarUrl, PhoneNumber, Email, Segment, ConversationCount, TotalMessageCount, IsActive, FirstInteractionAt, LastInteractionAt, Tags
- ChatbotRuleDto: Id, Name, Description, Type, Priority, IsActive, ActionType, ResponseText, TemplateId, Conditions (list of RuleConditionDto), MatchCount, LastMatchedAt, CreatedAt
- RuleConditionDto: Field, Operator, Value
Domain Model
Aggregates
ZaloCustomer (Aggregate Root)
- Properties: ZaloUserId (unique), Profile (CustomerProfile value object), Segment (CustomerSegment enum), FirstInteractionAt, LastInteractionAt, ConversationCount, TotalMessageCount, IsActive, CreatedAt, UpdatedAt
- Child Entity: Tag — Name (normalized lowercase), CreatedAt, CustomerId (FK)
- Value Object: CustomerProfile — DisplayName, AvatarUrl, PhoneNumber, Email
- Methods: UpdateProfile(), AddTag() (idempotent), RemoveTag(), RecordInteraction(), IncrementConversationCount(), IncrementMessageCount(), MarkInactive(), MarkActive()
- Auto-segmentation: UpdateSegment() based on engagement:
- VIP: >50 conversations AND >500 messages
- Active: >20 conversations AND >200 messages
- Regular: >5 conversations AND >50 messages
- New: default
- Domain Events: CustomerCreatedDomainEvent, CustomerProfileUpdatedDomainEvent
Conversation (Aggregate Root)
- Properties: ZaloUserId, CustomerId, Status (ConversationStatus enum), StartedAt, EndedAt, MessageCount, LastMessagePreview (max 100 chars + "..."), LastMessageAt, CreatedAt, UpdatedAt
- Child Entity: Message — Type (MessageType enum), Content, Direction (MessageDirection enum), IsFromBot, SentAt, ZaloMessageId, ConversationId (FK)
- Methods: AddMessage() (throws ConversationClosedException if closed), Close() (idempotent), Reopen() (idempotent)
- Domain Events: ConversationStartedDomainEvent, MessageReceivedDomainEvent, ConversationClosedDomainEvent
ChatbotRule (Aggregate Root)
- Properties: Name, Description, Type (RuleType enum), Action (RuleAction value object), Priority (0-100, higher = evaluated first), IsActive, MatchCount, LastMatchedAt, CreatedAt, UpdatedAt
- Child Entity: ChatbotRuleCondition — Field, Operator, Value, RuleId (FK), CreatedAt
- Value Objects:
- RuleCondition — Field, Operator, Value
- RuleAction — ActionType (ActionType enum), ResponseText, TemplateId. Factory methods: SendText(), SendTemplate(), ForwardToHuman()
- Methods: AddCondition(), ClearConditions(), Evaluate(userMessage), RecordMatch(), Activate(), Deactivate(), Update()
- Evaluation Logic:
- Keyword: any condition with "contains" operator matches (case-insensitive)
- Regex: any condition matches regex pattern (1-second timeout)
- Intent: delegated to AI engine (returns false in Evaluate)
- Domain Events: ChatbotRuleMatchedDomainEvent
MessageTemplate (Aggregate Root)
- Properties: ZaloTemplateId (unique, Zalo's ID), Name, Content, Status (TemplateStatus enum), SendCount, CreatedAt, UpdatedAt
- Child Entity: TemplateParameter — Name, IsRequired, DefaultValue, TemplateId (FK)
- Methods: AddParameter() (duplicate check), ValidateAndFillParameters() (fills defaults, throws MissingTemplateParametersException), Approve(), Reject(), RecordSend(), Update()
Enums
| Enum |
Values |
| ConversationStatus |
Active(0), Closed(1) |
| MessageType |
Text(0), Image(1), Link(2), Sticker(3), Audio(4) |
| MessageDirection |
Incoming(0), Outgoing(1) |
| CustomerSegment |
New(0), Regular(1), Active(2), VIP(3) |
| RuleType |
Keyword(0), Regex(1), Intent(2) |
| ActionType |
SendText(0), SendTemplate(1), ForwardToHuman(2) |
| TemplateStatus |
Pending(0), Approved(1), Rejected(2) |
Exceptions
| Exception |
Description |
| ZaloDomainException |
Base domain exception |
| ConversationClosedException |
Cannot operate on closed conversation |
| CustomerNotFoundException |
Customer not found by Zalo user ID |
| InvalidRuleConfigurationException |
Invalid rule configuration |
| MissingTemplateParametersException |
Missing required template parameters |
Database Schema
DbContext: MktZaloServiceContext (8 DbSets)
- Implements IUnitOfWork, dispatches domain events before SaveChanges
- Transaction support: BeginTransactionAsync, CommitTransactionAsync, RollbackTransaction
- Migration: 20260118181258_InitialSchema
Tables
ZaloCustomers
| Column |
Type |
Constraints |
| Id |
uuid |
PK |
| ZaloUserId |
varchar(50) |
NOT NULL, unique index |
| DisplayName |
varchar(255) |
NOT NULL (owned, from Profile) |
| AvatarUrl |
varchar(500) |
nullable (owned, from Profile) |
| PhoneNumber |
varchar(20) |
nullable (owned, from Profile) |
| Email |
varchar(255) |
nullable (owned, from Profile) |
| Segment |
varchar(20) |
string conversion (enum), indexed |
| FirstInteractionAt |
timestamp |
|
| LastInteractionAt |
timestamp |
indexed |
| ConversationCount |
int |
default 0 |
| TotalMessageCount |
int |
default 0 |
| IsActive |
bool |
default true |
| CreatedAt |
timestamp |
NOT NULL |
| UpdatedAt |
timestamp |
NOT NULL |
Indexes: IX_ZaloCustomers_ZaloUserId (unique), IX_ZaloCustomers_Segment, IX_ZaloCustomers_LastInteractionAt
CustomerTags
| Column |
Type |
Constraints |
| Id |
uuid |
PK |
| Name |
varchar(100) |
NOT NULL |
| CustomerId |
uuid |
FK -> ZaloCustomers (cascade delete) |
| CreatedAt |
timestamp |
NOT NULL |
Indexes: IX_CustomerTags_CustomerId_Name (unique composite)
Conversations
| Column |
Type |
Constraints |
| Id |
uuid |
PK |
| ZaloUserId |
varchar(50) |
NOT NULL, indexed |
| CustomerId |
uuid |
indexed |
| Status |
varchar(20) |
string conversion (enum) |
| StartedAt |
timestamp |
|
| EndedAt |
timestamp |
nullable |
| MessageCount |
int |
default 0 |
| LastMessagePreview |
varchar(200) |
|
| LastMessageAt |
timestamp |
indexed |
| CreatedAt |
timestamp |
NOT NULL |
| UpdatedAt |
timestamp |
NOT NULL |
Indexes: IX_Conversations_CustomerId, IX_Conversations_ZaloUserId, IX_Conversations_Status_StartedAt (composite), IX_Conversations_LastMessageAt
Messages
| Column |
Type |
Constraints |
| Id |
uuid |
PK |
| Type |
varchar(20) |
string conversion (enum) |
| Content |
text |
NOT NULL |
| Direction |
varchar(20) |
string conversion (enum) |
| IsFromBot |
bool |
|
| SentAt |
timestamp |
NOT NULL |
| ZaloMessageId |
varchar(100) |
indexed |
| ConversationId |
uuid |
FK -> Conversations (cascade delete) |
Indexes: IX_Messages_ConversationId_SentAt (composite), IX_Messages_ZaloMessageId
ChatbotRules
| Column |
Type |
Constraints |
| Id |
uuid |
PK |
| Name |
varchar(255) |
NOT NULL, indexed |
| Description |
varchar(1000) |
nullable |
| Type |
varchar(20) |
string conversion (enum) |
| Priority |
int |
default 50 |
| IsActive |
bool |
default true |
| ActionType |
varchar(30) |
string conversion (owned from Action) |
| ActionResponseText |
varchar(2000) |
nullable (owned from Action) |
| ActionTemplateId |
uuid |
nullable (owned from Action) |
| MatchCount |
int |
default 0 |
| LastMatchedAt |
timestamp |
nullable |
| CreatedAt |
timestamp |
NOT NULL |
| UpdatedAt |
timestamp |
NOT NULL |
Indexes: IX_ChatbotRules_IsActive_Priority (IsActive asc, Priority desc), IX_ChatbotRules_Name
RuleConditions
| Column |
Type |
Constraints |
| Id |
uuid |
PK |
| Field |
varchar(50) |
NOT NULL |
| Operator |
varchar(50) |
NOT NULL |
| Value |
text |
NOT NULL |
| RuleId |
uuid |
FK -> ChatbotRules (cascade delete), indexed |
| CreatedAt |
timestamp |
NOT NULL |
Indexes: IX_RuleConditions_RuleId
MessageTemplates
| Column |
Type |
Constraints |
| Id |
uuid |
PK |
| ZaloTemplateId |
varchar(50) |
NOT NULL, unique index |
| Name |
varchar(255) |
NOT NULL |
| Content |
text |
NOT NULL |
| Status |
varchar(20) |
string conversion (enum), indexed |
| SendCount |
int |
default 0 |
| CreatedAt |
timestamp |
NOT NULL |
| UpdatedAt |
timestamp |
NOT NULL |
Indexes: IX_MessageTemplates_ZaloTemplateId (unique), IX_MessageTemplates_Status
TemplateParameters
| Column |
Type |
Constraints |
| Id |
uuid |
PK |
| Name |
varchar(100) |
NOT NULL |
| IsRequired |
bool |
default true |
| DefaultValue |
varchar(500) |
nullable |
| TemplateId |
uuid |
FK -> MessageTemplates (cascade delete) |
Indexes: IX_TemplateParameters_TemplateId_Name (unique composite)
Domain Events (7 total)
| Event |
Payload |
| MessageReceivedDomainEvent |
ConversationId, MessageId, Content, IsFromBot |
| ConversationClosedDomainEvent |
ConversationId |
| ConversationStartedDomainEvent |
ConversationId, CustomerId, ZaloUserId |
| CustomerProfileUpdatedDomainEvent |
CustomerId, ZaloUserId |
| CustomerCreatedDomainEvent |
CustomerId, ZaloUserId, DisplayName |
| ChatbotRuleMatchedDomainEvent |
RuleId, ConversationId, MatchedMessage |
| MessageSentDomainEvent |
ConversationId, MessageId, ZaloMessageId |
Domain Event Handlers
- MessageReceivedDomainEventHandler: Triggered on MessageReceivedDomainEvent. Skips bot messages (avoids loops). Finds matching chatbot rule via IChatbotRulesService (priority order). Records rule match, then handles action: SendText sends auto-response via SendMessageCommand, SendTemplate logs template ID (not yet implemented), ForwardToHuman logs forwarding.
Application Services
ChatbotRulesService (IChatbotRulesService)
- FindMatchingRuleAsync: Gets active rules (cached via IChatbotRuleCacheService), evaluates rules in priority order, returns first match
- GetResponseText: Returns response text for SendText actions, null for SendTemplate/ForwardToHuman
AiChatbotEngine (IChatbotEngine)
- Status: Placeholder implementation (not connected to real AI)
- Name: "AI"
- CanHandleAsync: Always returns true (fallback engine)
- GenerateResponseAsync: Builds conversation history from cache context, returns placeholder Vietnamese responses based on keyword matching. System prompt instructs friendly Vietnamese assistant behavior.
- Placeholder responses: Thanks messages, question redirection, default acknowledgment
Caching Services
ChatbotRuleCacheService (IChatbotRuleCacheService)
- Key:
chatbot:rules:active
- Expiration: 5 minutes (not fully implemented — always loads from DB in MVP)
- InvalidateRulesCacheAsync: Removes cache key
ConversationCacheService (IConversationCacheService)
- Key pattern:
conversation:{conversationId}
- Expiration: 30 minutes
- ConversationContext: ConversationId, CustomerId, ZaloUserId, CustomerName, RecentMessages (list of CachedMessage), LastUpdated
- GetOrSetAsync: Generic factory pattern for cache-aside
External Services
Zalo Official Account Client (IZaloOfficialAccountClient / ZaloOfficialAccountClient)
- Base URL:
https://openapi.zalo.me (configurable)
- Authentication: access_token header
- Resilience: Polly retry (configurable attempts, exponential backoff) + circuit breaker (5 failures, 30s break)
- JSON: snake_case naming policy
- Methods:
SendTextMessageAsync(zaloUserId, text) — POST /v3.0/oa/message/cs
SendTemplateMessageAsync(phoneNumber, templateId, parameters) — POST /v3.0/oa/message/template (ZNS)
GetUserProfileAsync(zaloUserId) — GET /v3.0/oa/user/detail?data={json}
Zalo Webhook Verifier (IZaloWebhookVerifier / ZaloWebhookVerifier)
- Algorithm: HMAC-SHA256
- Secret: Configured via ZaloOAOptions.WebhookSecret
- VerifySignature: Computes HMAC-SHA256 of request body with secret, compares to X-ZaloOA-Signature header
Dependencies (NuGet)
API Project
- MediatR 12.4.1
- FluentValidation.DependencyInjectionExtensions 11.11.0
- Swashbuckle.AspNetCore 7.2.0
- Asp.Versioning.Mvc 8.1.0
- Asp.Versioning.Mvc.ApiExplorer 8.1.0
- Hellang.Middleware.ProblemDetails 6.5.1
- Serilog.AspNetCore 8.0.3
- AspNetCore.HealthChecks.NpgSql 8.0.2
Infrastructure Project
- Microsoft.EntityFrameworkCore 10.0.0
- Microsoft.EntityFrameworkCore.Design 10.0.0
- Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0
- MediatR 12.4.1
- Dapper 2.1.35
- Polly 8.5.0
- Polly.Extensions.Http 3.0.0
- StackExchange.Redis 2.8.16
- Microsoft.Extensions.Caching.StackExchangeRedis 9.0.0
Configuration
Environment Variables / Settings
ConnectionStrings:DefaultConnection or DATABASE_URL — PostgreSQL connection
ConnectionStrings:Redis or REDIS_URL — Redis connection (optional, falls back to in-memory)
ZaloOA:AppId — Zalo application ID
ZaloOA:SecretKey — Zalo application secret key
ZaloOA:AccessToken — Zalo OA access token (1-year validity)
ZaloOA:WebhookSecret — Webhook signature verification secret
ZaloOA:BaseUrl — Zalo OpenAPI base URL (default: https://openapi.zalo.me)
ZaloOA:TimeoutSeconds — HTTP timeout (default: 30)
ZaloOA:MaxRetryAttempts — Retry attempts (default: 3)
MediatR Pipeline Behaviors
- LoggingBehavior — Request/response logging with Stopwatch
- ValidatorBehavior — FluentValidation in pipeline
- TransactionBehavior — Auto transaction for Commands (skips Queries)
Health Checks
- PostgreSQL health check via NpgSql
Seed Data (DbSeeder)
- Chatbot Rules (5 default rules):
- Welcome Greeting (priority 100) — Keywords: xin chào, hello, hi, chào
- FAQ: Price Inquiry (priority 80) — Keywords: giá, bao nhiêu, price, cost
- FAQ: Opening Hours (priority 80) — Keywords: giờ mở cửa, mấy giờ, opening, hours
- FAQ: Location (priority 80) — Keywords: địa chỉ, ở đâu, location, address
- Request Human Support (priority 90) — Keywords: gặp nhân viên, hỗ trợ, human, support, agent → ForwardToHuman action
- Message Templates (2 default templates):
- Welcome Message (welcome_001) — Parameter: customer_name (required), pre-approved
- Order Confirmation (order_confirm_001) — Parameters: order_id, total_amount, delivery_date (all required), pre-approved
DI Registration (All Repositories Registered)
IConversationRepository -> ConversationRepository (scoped)
ICustomerRepository -> CustomerRepository (scoped)
IChatbotRuleRepository -> ChatbotRuleRepository (scoped)
IMessageTemplateRepository -> MessageTemplateRepository (scoped)
IZaloOfficialAccountClient -> ZaloOfficialAccountClient (HttpClient factory)
IZaloWebhookVerifier -> ZaloWebhookVerifier (singleton)
IConversationCacheService -> ConversationCacheService (scoped)
IChatbotRuleCacheService -> ChatbotRuleCacheService (scoped)
IRequestManager -> RequestManager (scoped)
IChatbotRulesService -> ChatbotRulesService (registered in Program.cs)