# 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
1. LoggingBehavior — Request/response logging with Stopwatch
2. ValidatorBehavior — FluentValidation in pipeline
3. TransactionBehavior — Auto transaction for Commands (skips Queries)

### Health Checks
- PostgreSQL health check via NpgSql

### Seed Data (DbSeeder)
- **Chatbot Rules** (5 default rules):
  1. Welcome Greeting (priority 100) — Keywords: xin chào, hello, hi, chào
  2. FAQ: Price Inquiry (priority 80) — Keywords: giá, bao nhiêu, price, cost
  3. FAQ: Opening Hours (priority 80) — Keywords: giờ mở cửa, mấy giờ, opening, hours
  4. FAQ: Location (priority 80) — Keywords: địa chỉ, ở đâu, location, address
  5. Request Human Support (priority 90) — Keywords: gặp nhân viên, hỗ trợ, human, support, agent → ForwardToHuman action
- **Message Templates** (2 default templates):
  1. Welcome Message (welcome_001) — Parameter: customer_name (required), pre-approved
  2. 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)
