diff --git a/services/membership-service-net/docs/en/ARCHITECTURE.md b/services/membership-service-net/docs/en/ARCHITECTURE.md index 97967076..7109ac97 100644 --- a/services/membership-service-net/docs/en/ARCHITECTURE.md +++ b/services/membership-service-net/docs/en/ARCHITECTURE.md @@ -171,14 +171,24 @@ public class Member : Entity, IAggregateRoot ```csharp public class LevelDefinition : Entity, IAggregateRoot { + private readonly List _benefits = new(); + public int LevelNumber { get; private set; } // 1, 2, 3... public string Name { get; private set; } // "Bronze", "Silver", "Gold" public int RequiredExp { get; private set; } // 0, 100, 300, 600... public string? Description { get; private set; } public string? IconUrl { get; private set; } - public string? Benefits { get; private set; } // JSON benefits + public string? BadgeColor { get; private set; } // "#CD7F32", "#FFD700" public bool IsActive { get; private set; } + public IReadOnlyCollection Benefits => _benefits.AsReadOnly(); + + public void AddBenefit(LevelBenefit benefit) + { + _benefits.Add(benefit); + UpdatedAt = DateTime.UtcNow; + } + public void UpdateRequiredExp(int newRequiredExp) { if (newRequiredExp < 0) @@ -188,6 +198,61 @@ public class LevelDefinition : Entity, IAggregateRoot } ``` +### LevelBenefit Entity (Rewards per Level) + +```csharp +public class LevelBenefit : Entity +{ + public Guid LevelDefinitionId { get; private set; } + public string BenefitType { get; private set; } // "discount_percent", "free_shipping" + public string BenefitValue { get; private set; } // JSON: {"percent": 10} + public string? Description { get; private set; } + public bool IsActive { get; private set; } +} +``` + +**Benefit Types:** + +| Type | Value Example | Description | +|------|---------------|-------------| +| `discount_percent` | `{"percent": 10}` | 10% discount | +| `free_shipping` | `{"enabled": true}` | Free shipping | +| `priority_support` | `{"enabled": true}` | Priority support | +| `bonus_points` | `{"multiplier": 1.5}` | 1.5x point multiplier | +| `exclusive_access` | `{"feature": "early_sale"}` | Early access | + +### ExperienceTransaction Entity (EXP Tracking) + +Entity to track EXP source and history: + +```csharp +public class ExperienceTransaction : Entity +{ + public Guid MemberId { get; private set; } + public int Points { get; private set; } + public ExperienceSource Source { get; private set; } // Purchase, Referral, Activity... + public string? ReferenceId { get; private set; } // Order ID, Referral Code, etc. + public string? Metadata { get; private set; } // JSON additional info + public int LevelAtTime { get; private set; } // Level when EXP was earned + public DateTime CreatedAt { get; private set; } +} +``` + +**Experience Sources:** + +| Source | ID | Description | +|--------|----|-------------| +| `Purchase` | 1 | From orders | +| `Referral` | 2 | Friend referral | +| `Activity` | 3 | App activity | +| `Promotion` | 4 | Promotions | +| `Review` | 5 | Product reviews | +| `CheckIn` | 6 | Daily check-in | +| `Admin` | 7 | Admin manual grant | + +> [!NOTE] +> **Audit Trail**: Every time a member receives EXP, it's logged in `experience_transactions` with full source information for audit and analytics. + ### Domain Events | Event | Trigger | Purpose | @@ -355,6 +420,10 @@ sequenceDiagram ```mermaid erDiagram + MEMBERS ||--o{ EXPERIENCE_TRANSACTIONS : has + MEMBERS }o--|| LEVEL_DEFINITIONS : current_level + LEVEL_DEFINITIONS ||--o{ LEVEL_BENEFITS : has + MEMBERS { uuid id PK "Same as IAM UserId" varchar country_code "ISO 3166-1 alpha-2" @@ -375,19 +444,38 @@ erDiagram int required_exp "EXP needed for this level" text description varchar icon_url - jsonb benefits "Level benefits/rewards" + varchar badge_color "Hex color code" boolean is_active timestamp created_at timestamp updated_at } - MEMBERS }o..|| LEVEL_DEFINITIONS : "current_level references" + LEVEL_BENEFITS { + uuid id PK + uuid level_definition_id FK + varchar benefit_type "discount_percent, free_shipping..." + jsonb benefit_value "Benefit configuration" + text description + boolean is_active + timestamp created_at + } + + EXPERIENCE_TRANSACTIONS { + uuid id PK + uuid member_id FK + int points "EXP points earned" + int source_id "ExperienceSource enum" + varchar reference_id "Order ID, Referral Code..." + jsonb metadata "Additional info" + int level_at_time "Member level when earned" + timestamp created_at + } ``` ### SQL Schema ```sql --- Level Definitions table (Admin configurable) +-- 1. Level Definitions table (Admin configurable) CREATE TABLE level_definitions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), level_number INT NOT NULL UNIQUE, @@ -395,13 +483,24 @@ CREATE TABLE level_definitions ( required_exp INT NOT NULL DEFAULT 0, description TEXT, icon_url VARCHAR(500), - benefits JSONB, + badge_color VARCHAR(20), is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); --- Members table +-- 2. Level Benefits table (Rewards per level) +CREATE TABLE level_benefits ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + level_definition_id UUID NOT NULL REFERENCES level_definitions(id), + benefit_type VARCHAR(50) NOT NULL, + benefit_value JSONB NOT NULL, + description TEXT, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- 3. Members table CREATE TABLE members ( id UUID PRIMARY KEY, country_code VARCHAR(2) NOT NULL DEFAULT 'VN', @@ -415,20 +514,53 @@ CREATE TABLE members ( updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); +-- 4. Experience Transactions table (EXP History/Tracking) +CREATE TABLE experience_transactions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + member_id UUID NOT NULL REFERENCES members(id), + points INT NOT NULL, + source_id INT NOT NULL, + reference_id VARCHAR(100), + metadata JSONB, + level_at_time INT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + -- Indexes -CREATE INDEX ix_members_current_level ON members(current_level); -CREATE INDEX ix_members_current_exp ON members(current_exp); CREATE INDEX ix_level_definitions_level_number ON level_definitions(level_number); CREATE INDEX ix_level_definitions_is_active ON level_definitions(is_active); +CREATE INDEX ix_level_benefits_level_definition_id ON level_benefits(level_definition_id); +CREATE INDEX ix_members_current_level ON members(current_level); +CREATE INDEX ix_members_current_exp ON members(current_exp); +CREATE INDEX ix_experience_transactions_member_id ON experience_transactions(member_id); +CREATE INDEX ix_experience_transactions_created_at ON experience_transactions(created_at); +CREATE INDEX ix_experience_transactions_source ON experience_transactions(source_id); -- Default Level Definitions -INSERT INTO level_definitions (level_number, name, required_exp, description, is_active) +INSERT INTO level_definitions (level_number, name, required_exp, description, badge_color, is_active) VALUES - (1, 'Bronze', 0, 'Starting level', true), - (2, 'Silver', 100, 'Reach 100 EXP', true), - (3, 'Gold', 300, 'Reach 300 EXP', true), - (4, 'Platinum', 600, 'Reach 600 EXP', true), - (5, 'Diamond', 1000, 'Reach 1000 EXP', true); + (1, 'Bronze', 0, 'Starting level - Welcome!', '#CD7F32', true), + (2, 'Silver', 100, 'Reach 100 EXP', '#C0C0C0', true), + (3, 'Gold', 300, 'Reach 300 EXP', '#FFD700', true), + (4, 'Platinum', 600, 'Reach 600 EXP', '#E5E4E2', true), + (5, 'Diamond', 1000, 'Reach 1000 EXP - Elite!', '#B9F2FF', true); + +-- Default Level Benefits +INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description) +SELECT id, 'discount_percent', '{"percent": 5}', '5% discount on all orders' +FROM level_definitions WHERE level_number = 2; + +INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description) +SELECT id, 'discount_percent', '{"percent": 10}', '10% discount on all orders' +FROM level_definitions WHERE level_number = 3; + +INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description) +SELECT id, 'free_shipping', '{"enabled": true}', 'Free shipping on all orders' +FROM level_definitions WHERE level_number = 4; + +INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description) +SELECT id, 'priority_support', '{"enabled": true}', 'Priority customer support' +FROM level_definitions WHERE level_number = 5; ``` ## API Design @@ -438,13 +570,14 @@ VALUES #### Member Endpoints ``` -GET /api/v1/members # List (paginated, searchable) -GET /api/v1/members/{id} # Get by ID -GET /api/v1/members/me # Get current user -GET /api/v1/members/{id}/progress # Get level progress -POST /api/v1/members # Create -POST /api/v1/members/{id}/experience # Add EXP -PUT /api/v1/members/{id} # Update profile +GET /api/v1/members # List (paginated, searchable) +GET /api/v1/members/{id} # Get by ID +GET /api/v1/members/me # Get current user +GET /api/v1/members/{id}/progress # Get level progress +GET /api/v1/members/{id}/experience # Get EXP history (tracking) +POST /api/v1/members # Create +POST /api/v1/members/{id}/experience # Add EXP +PUT /api/v1/members/{id} # Update profile ``` #### Level Endpoints (Admin) diff --git a/services/membership-service-net/docs/vi/ARCHITECTURE.md b/services/membership-service-net/docs/vi/ARCHITECTURE.md index c2e5b6f4..d27d7e6c 100644 --- a/services/membership-service-net/docs/vi/ARCHITECTURE.md +++ b/services/membership-service-net/docs/vi/ARCHITECTURE.md @@ -171,14 +171,24 @@ public class Member : Entity, IAggregateRoot ```csharp public class LevelDefinition : Entity, IAggregateRoot { + private readonly List _benefits = new(); + public int LevelNumber { get; private set; } // 1, 2, 3... public string Name { get; private set; } // "Bronze", "Silver", "Gold" public int RequiredExp { get; private set; } // 0, 100, 300, 600... public string? Description { get; private set; } public string? IconUrl { get; private set; } - public string? Benefits { get; private set; } // JSON benefits + public string? BadgeColor { get; private set; } // "#CD7F32", "#FFD700" public bool IsActive { get; private set; } + public IReadOnlyCollection Benefits => _benefits.AsReadOnly(); + + public void AddBenefit(LevelBenefit benefit) + { + _benefits.Add(benefit); + UpdatedAt = DateTime.UtcNow; + } + public void UpdateRequiredExp(int newRequiredExp) { if (newRequiredExp < 0) @@ -188,6 +198,61 @@ public class LevelDefinition : Entity, IAggregateRoot } ``` +### LevelBenefit Entity (Rewards cho mỗi Level) + +```csharp +public class LevelBenefit : Entity +{ + public Guid LevelDefinitionId { get; private set; } + public string BenefitType { get; private set; } // "discount_percent", "free_shipping" + public string BenefitValue { get; private set; } // JSON: {"percent": 10} + public string? Description { get; private set; } + public bool IsActive { get; private set; } +} +``` + +**Benefit Types:** + +| Type | Value Example | Description | +|------|---------------|-------------| +| `discount_percent` | `{"percent": 10}` | Giảm giá 10% | +| `free_shipping` | `{"enabled": true}` | Miễn phí ship | +| `priority_support` | `{"enabled": true}` | Hỗ trợ ưu tiên | +| `bonus_points` | `{"multiplier": 1.5}` | Nhân điểm 1.5x | +| `exclusive_access` | `{"feature": "early_sale"}` | Truy cập sớm | + +### ExperienceTransaction Entity (EXP Tracking) + +Entity để tracking nguồn gốc và lịch sử EXP: + +```csharp +public class ExperienceTransaction : Entity +{ + public Guid MemberId { get; private set; } + public int Points { get; private set; } + public ExperienceSource Source { get; private set; } // Purchase, Referral, Activity... + public string? ReferenceId { get; private set; } // Order ID, Referral Code, etc. + public string? Metadata { get; private set; } // JSON additional info + public int LevelAtTime { get; private set; } // Level khi nhận EXP + public DateTime CreatedAt { get; private set; } +} +``` + +**Experience Sources:** + +| Source | ID | Mô tả | +|--------|----|-------| +| `Purchase` | 1 | Từ đơn hàng | +| `Referral` | 2 | Giới thiệu bạn bè | +| `Activity` | 3 | Hoạt động trên app | +| `Promotion` | 4 | Khuyến mãi | +| `Review` | 5 | Đánh giá sản phẩm | +| `CheckIn` | 6 | Check-in hàng ngày | +| `Admin` | 7 | Admin manual grant | + +> [!NOTE] +> **Audit Trail**: Mỗi lần member nhận EXP đều được lưu vào `experience_transactions` với đầy đủ thông tin nguồn gốc để audit và analytics. + ### Domain Events | Event | Trigger | Mục đích | @@ -355,6 +420,10 @@ sequenceDiagram ```mermaid erDiagram + MEMBERS ||--o{ EXPERIENCE_TRANSACTIONS : has + MEMBERS }o--|| LEVEL_DEFINITIONS : current_level + LEVEL_DEFINITIONS ||--o{ LEVEL_BENEFITS : has + MEMBERS { uuid id PK "Same as IAM UserId" varchar country_code "ISO 3166-1 alpha-2" @@ -375,19 +444,38 @@ erDiagram int required_exp "EXP needed for this level" text description varchar icon_url - jsonb benefits "Level benefits/rewards" + varchar badge_color "Hex color code" boolean is_active timestamp created_at timestamp updated_at } - MEMBERS }o..|| LEVEL_DEFINITIONS : "current_level references" + LEVEL_BENEFITS { + uuid id PK + uuid level_definition_id FK + varchar benefit_type "discount_percent, free_shipping..." + jsonb benefit_value "Benefit configuration" + text description + boolean is_active + timestamp created_at + } + + EXPERIENCE_TRANSACTIONS { + uuid id PK + uuid member_id FK + int points "EXP points earned" + int source_id "ExperienceSource enum" + varchar reference_id "Order ID, Referral Code..." + jsonb metadata "Additional info" + int level_at_time "Member level when earned" + timestamp created_at + } ``` ### SQL Schema ```sql --- Bảng Level Definitions (Admin configurable) +-- 1. Bảng Level Definitions (Admin configurable) CREATE TABLE level_definitions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), level_number INT NOT NULL UNIQUE, @@ -395,13 +483,24 @@ CREATE TABLE level_definitions ( required_exp INT NOT NULL DEFAULT 0, description TEXT, icon_url VARCHAR(500), - benefits JSONB, + badge_color VARCHAR(20), is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); --- Bảng Members +-- 2. Bảng Level Benefits (Rewards cho mỗi level) +CREATE TABLE level_benefits ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + level_definition_id UUID NOT NULL REFERENCES level_definitions(id), + benefit_type VARCHAR(50) NOT NULL, + benefit_value JSONB NOT NULL, + description TEXT, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +-- 3. Bảng Members CREATE TABLE members ( id UUID PRIMARY KEY, country_code VARCHAR(2) NOT NULL DEFAULT 'VN', @@ -415,20 +514,53 @@ CREATE TABLE members ( updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); +-- 4. Bảng Experience Transactions (EXP History/Tracking) +CREATE TABLE experience_transactions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + member_id UUID NOT NULL REFERENCES members(id), + points INT NOT NULL, + source_id INT NOT NULL, + reference_id VARCHAR(100), + metadata JSONB, + level_at_time INT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + -- Indexes -CREATE INDEX ix_members_current_level ON members(current_level); -CREATE INDEX ix_members_current_exp ON members(current_exp); CREATE INDEX ix_level_definitions_level_number ON level_definitions(level_number); CREATE INDEX ix_level_definitions_is_active ON level_definitions(is_active); +CREATE INDEX ix_level_benefits_level_definition_id ON level_benefits(level_definition_id); +CREATE INDEX ix_members_current_level ON members(current_level); +CREATE INDEX ix_members_current_exp ON members(current_exp); +CREATE INDEX ix_experience_transactions_member_id ON experience_transactions(member_id); +CREATE INDEX ix_experience_transactions_created_at ON experience_transactions(created_at); +CREATE INDEX ix_experience_transactions_source ON experience_transactions(source_id); -- Default Level Definitions -INSERT INTO level_definitions (level_number, name, required_exp, description, is_active) +INSERT INTO level_definitions (level_number, name, required_exp, description, badge_color, is_active) VALUES - (1, 'Bronze', 0, 'Starting level', true), - (2, 'Silver', 100, 'Reach 100 EXP', true), - (3, 'Gold', 300, 'Reach 300 EXP', true), - (4, 'Platinum', 600, 'Reach 600 EXP', true), - (5, 'Diamond', 1000, 'Reach 1000 EXP', true); + (1, 'Bronze', 0, 'Starting level - Welcome!', '#CD7F32', true), + (2, 'Silver', 100, 'Reach 100 EXP', '#C0C0C0', true), + (3, 'Gold', 300, 'Reach 300 EXP', '#FFD700', true), + (4, 'Platinum', 600, 'Reach 600 EXP', '#E5E4E2', true), + (5, 'Diamond', 1000, 'Reach 1000 EXP - Elite!', '#B9F2FF', true); + +-- Default Level Benefits +INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description) +SELECT id, 'discount_percent', '{"percent": 5}', '5% discount on all orders' +FROM level_definitions WHERE level_number = 2; + +INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description) +SELECT id, 'discount_percent', '{"percent": 10}', '10% discount on all orders' +FROM level_definitions WHERE level_number = 3; + +INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description) +SELECT id, 'free_shipping', '{"enabled": true}', 'Free shipping on all orders' +FROM level_definitions WHERE level_number = 4; + +INSERT INTO level_benefits (level_definition_id, benefit_type, benefit_value, description) +SELECT id, 'priority_support', '{"enabled": true}', 'Priority customer support' +FROM level_definitions WHERE level_number = 5; ``` ## Thiết Kế API @@ -438,13 +570,14 @@ VALUES #### Member Endpoints ``` -GET /api/v1/members # Danh sách (phân trang, search) -GET /api/v1/members/{id} # Lấy theo ID -GET /api/v1/members/me # Lấy user hiện tại -GET /api/v1/members/{id}/progress # Lấy tiến trình level -POST /api/v1/members # Tạo mới -POST /api/v1/members/{id}/experience # Thêm EXP -PUT /api/v1/members/{id} # Cập nhật hồ sơ +GET /api/v1/members # Danh sách (phân trang, search) +GET /api/v1/members/{id} # Lấy theo ID +GET /api/v1/members/me # Lấy user hiện tại +GET /api/v1/members/{id}/progress # Lấy tiến trình level +GET /api/v1/members/{id}/experience # Lấy lịch sử EXP (tracking) +POST /api/v1/members # Tạo mới +POST /api/v1/members/{id}/experience # Thêm EXP +PUT /api/v1/members/{id} # Cập nhật hồ sơ ``` #### Level Endpoints (Admin) diff --git a/services/merchant-service-net/README.md b/services/merchant-service-net/README.md new file mode 100644 index 00000000..47124b97 --- /dev/null +++ b/services/merchant-service-net/README.md @@ -0,0 +1,106 @@ +# Merchant Service .NET 10 + +> **Merchant & Shop Management Service for GoodGo Platform** + +[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/) +[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-4169E1)](https://www.postgresql.org/) +[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED)](https://www.docker.com/) + +## Overview + +Merchant Service manages Shop Owners (Merchants), Shops, and Staff for the GoodGo ecosystem. It supports online stores, physical stores, and hybrid business models. + +### Key Features + +- 🏪 **Shop Management** - Create and manage shops (Online/Physical/Hybrid) +- 👥 **Staff Management** - Employee management with role-based permissions +- 💳 **POS Support** - PIN authentication for Point-of-Sale systems +- 📍 **Branch Management** - Multi-location support with geo-search +- 🔐 **Role-based Access** - Merchant, MerchantStaff, MerchantAdmin roles +- 🔗 **Service Integration** - IAM, Wallet, Membership, Chat services + +## Quick Start + +```bash +# Clone and navigate +cd services/merchant-service-net + +# Restore and build +dotnet restore +dotnet build + +# Run +dotnet run --project src/MerchantService.API +``` + +## Documentation + +| Language | README | Architecture | +|----------|--------|--------------| +| 🇻🇳 Tiếng Việt | [docs/vi/README.md](docs/vi/README.md) | [docs/vi/ARCHITECTURE.md](docs/vi/ARCHITECTURE.md) | +| 🇬🇧 English | [docs/en/README.md](docs/en/README.md) | [docs/en/ARCHITECTURE.md](docs/en/ARCHITECTURE.md) | + +## API Endpoints + +| Resource | Base Path | Description | +|----------|-----------|-------------| +| Merchants | `/api/v1/merchants` | Merchant registration & management | +| Shops | `/api/v1/shops` | Shop CRUD operations | +| Branches | `/api/v1/shops/{id}/branches` | Physical shop locations | +| Staff | `/api/v1/merchants/me/staff` | Staff management | +| POS | `/api/v1/pos` | POS device authentication | +| Admin | `/api/v1/admin/merchants` | Administrative operations | + +## Project Structure + +``` +merchant-service-net/ +├── src/ +│ ├── MerchantService.API/ # Controllers, Commands, Queries +│ ├── MerchantService.Domain/ # Entities, Value Objects, Events +│ └── MerchantService.Infrastructure/# DbContext, Repositories +├── tests/ +│ ├── MerchantService.UnitTests/ +│ └── MerchantService.FunctionalTests/ +├── docs/ +│ ├── en/ # English documentation +│ └── vi/ # Vietnamese documentation +├── Dockerfile +└── MerchantService.slnx +``` + +## Environment Variables + +| Variable | Description | Required | +|----------|-------------|----------| +| `DATABASE_URL` | PostgreSQL connection string | ✅ | +| `Jwt__Authority` | IAM Service URL | ✅ | +| `IamService__BaseUrl` | IAM Service base URL | ✅ | +| `WalletService__BaseUrl` | Wallet Service base URL | ❌ | + +## Docker + +```bash +# Build +docker build -t goodgo/merchant-service:latest . + +# Run +docker run -p 5003:8080 \ + -e DATABASE_URL="Host=db;Port=5432;Database=merchant;..." \ + -e Jwt__Authority="http://iam-service:8080" \ + goodgo/merchant-service:latest +``` + +## Testing + +```bash +# Unit tests +dotnet test tests/MerchantService.UnitTests + +# Integration tests +dotnet test tests/MerchantService.FunctionalTests +``` + +## License + +This project is part of the GoodGo Platform. diff --git a/services/merchant-service-net/docs/en/ARCHITECTURE.md b/services/merchant-service-net/docs/en/ARCHITECTURE.md new file mode 100644 index 00000000..606e07d6 --- /dev/null +++ b/services/merchant-service-net/docs/en/ARCHITECTURE.md @@ -0,0 +1,788 @@ +# Merchant Service Architecture + +## System Overview + +Merchant Service manages **Merchants (Shop Owners)**, **Shops**, and **Merchant Staff** for the GoodGo ecosystem. This service supports multiple business types: online stores, physical stores, and hybrid models. + +```mermaid +graph TB + subgraph Client["Client Layer"] + WEB["🌐 Merchant Portal"] + POS["💳 POS System"] + MOBILE["📱 Customer App"] + end + + subgraph Gateway["API Gateway"] + TRAEFIK["⚡ Traefik"] + end + + subgraph Services["Microservices"] + IAM["🔐 IAM Service
(User & Roles)"] + MERCHANT["🏪 Merchant Service
(Shop, Staff)"] + WALLET["💰 Wallet Service"] + MEMBER["👥 Membership Service"] + VOUCHER["🎁 Voucher Service"] + end + + subgraph Data["Data Layer"] + PG[("🐘 PostgreSQL")] + end + + WEB --> TRAEFIK + POS --> TRAEFIK + MOBILE --> TRAEFIK + TRAEFIK --> |"/api/v1/auth"| IAM + TRAEFIK --> |"/api/v1/merchants
/api/v1/shops"| MERCHANT + TRAEFIK --> |"/api/v1/wallets"| WALLET + + IAM --> |"JWT Token"| MERCHANT + MERCHANT --> |"Assign Roles"| IAM + MERCHANT --> |"Settlement"| WALLET + MERCHANT -.-> |"Customer Visit"| MEMBER + MERCHANT -.-> |"Voucher Redemption"| VOUCHER + MERCHANT --> PG + + style IAM fill:#3b82f6,stroke:#1e40af,color:#fff + style MERCHANT fill:#f97316,stroke:#c2410c,color:#fff + style WALLET fill:#a855f7,stroke:#7e22ce,color:#fff + style MEMBER fill:#22c55e,stroke:#15803d,color:#fff + style VOUCHER fill:#ec4899,stroke:#be185d,color:#fff + style TRAEFIK fill:#6366f1,stroke:#4338ca,color:#fff +``` + +> [!IMPORTANT] +> **Responsibility Separation:** +> - **IAM Service**: Manages User identity, Roles (Merchant, MerchantStaff) +> - **Merchant Service**: Manages Merchant, Shop, ShopBranch, MerchantStaff +> - **Wallet Service**: Settlement, Commission calculation +> - **Voucher Service**: Gift Voucher (planned) + +## Clean Architecture Layers + +``` +┌────────────────────────────────────────────────────────────┐ +│ API Layer (Presentation) │ +│ Controllers, Commands, Queries, Validators, DTOs │ +├────────────────────────────────────────────────────────────┤ +│ Domain Layer (Core) │ +│ Entities, Aggregates, Value Objects, Domain Events │ +│ Merchant, Shop, MerchantStaff Aggregates │ +├────────────────────────────────────────────────────────────┤ +│ Infrastructure Layer │ +│ DbContext, Repositories, Entity Configurations │ +│ External Services (IAM Client, Wallet Client) │ +└────────────────────────────────────────────────────────────┘ +``` + +## Domain Model + +### Aggregates Overview + +```mermaid +classDiagram + class Merchant { + +Guid Id + +Guid UserId + +Guid? OrganizationId + +string BusinessName + +MerchantType Type + +MerchantStatus Status + +BusinessInfo BusinessInfo + +SettlementConfig SettlementConfig + +Register() + +Approve() + +Suspend() + +CreateShop() + } + + class Shop { + +Guid Id + +Guid MerchantId + +string Name + +string Slug + +ShopType Type + +BusinessCategory Category + +ShopStatus Status + +ContactInfo ContactInfo + +OperatingHours? Hours + +AddBranch() + +AssignStaff() + +UpdateSettings() + } + + class ShopBranch { + +Guid Id + +Guid ShopId + +string Name + +string Code + +Address Address + +GeoLocation? Location + +bool IsActive + } + + class MerchantStaff { + +Guid Id + +Guid UserId + +Guid MerchantId + +StaffRole Role + +StaffStatus Status + +string? EmployeeCode + +StaffPermissions Permissions + +SetPinCode() + +RegisterDevice() + } + + class ShopMember { + +Guid StaffId + +Guid ShopId + +Guid? BranchId + +ShopRole Role + +int? CustomPermissions + } + + Merchant "1" --> "*" Shop : owns + Merchant "1" --> "*" MerchantStaff : employs + Shop "1" --> "*" ShopBranch : has + Shop "1" --> "*" ShopMember : assigns + MerchantStaff "1" --> "*" ShopMember : works at + + style Merchant fill:#f97316,stroke:#c2410c,color:#fff + style Shop fill:#3b82f6,stroke:#1e40af,color:#fff + style MerchantStaff fill:#22c55e,stroke:#15803d,color:#fff +``` + +### Merchant Aggregate (Aggregate Root) + +```csharp +public class Merchant : Entity, IAggregateRoot +{ + private Guid _userId; // Reference to IAM User (Owner) + private Guid? _organizationId; // Reference to IAM Organization + private string _businessName; + private MerchantType _type; // Individual, Company + private MerchantStatus _status; // Pending, Active, Suspended, Banned + private MerchantVerification _verification; + private SettlementConfig _settlementConfig; + + // Value Object for business details + public BusinessInfo BusinessInfo { get; private set; } + + // Collections + private readonly List _shops = new(); + private readonly List _staff = new(); + + public IReadOnlyCollection Shops => _shops.AsReadOnly(); + public IReadOnlyCollection Staff => _staff.AsReadOnly(); + + /// + /// Factory method to register a new merchant. + /// + public static Merchant Register(Guid userId, string businessName, MerchantType type) + { + var merchant = new Merchant + { + Id = Guid.NewGuid(), + _userId = userId, + _businessName = businessName, + _type = type, + _status = MerchantStatus.PendingApproval, + _verification = new MerchantVerification() + }; + + merchant.AddDomainEvent(new MerchantRegisteredDomainEvent(merchant)); + return merchant; + } + + /// + /// Approve merchant application. + /// + public void Approve(Guid approvedBy) + { + if (_status != MerchantStatus.PendingApproval) + throw new DomainException("Can only approve pending merchants"); + + _status = MerchantStatus.Active; + AddDomainEvent(new MerchantApprovedDomainEvent(this, approvedBy)); + } + + /// + /// Create a new shop under this merchant. + /// + public Shop CreateShop(string name, string slug, ShopType type, BusinessCategory category) + { + if (_status != MerchantStatus.Active) + throw new DomainException("Only active merchants can create shops"); + + var shop = new Shop(Id, name, slug, type, category); + _shops.Add(shop); + return shop; + } +} +``` + +### Shop Aggregate (Aggregate Root) + +```csharp +public class Shop : Entity, IAggregateRoot +{ + private Guid _merchantId; + private string _name; + private string _slug; + private ShopType _type; + private BusinessCategory _category; + private ShopStatus _status; + private ContactInfo _contactInfo; + private OperatingHours? _operatingHours; + private ShopSettings _settings; + + // Physical locations + private readonly List _branches = new(); + + // Staff assignments + private readonly List _members = new(); + + public IReadOnlyCollection Branches => _branches.AsReadOnly(); + public IReadOnlyCollection Members => _members.AsReadOnly(); + + /// + /// Add a physical branch to the shop. + /// + public ShopBranch AddBranch(string name, Address address, GeoLocation? location = null) + { + if (_type == ShopType.OnlineOnly) + throw new DomainException("Online-only shops cannot have physical branches"); + + var branch = new ShopBranch(Id, name, address, location); + _branches.Add(branch); + AddDomainEvent(new ShopBranchAddedDomainEvent(this, branch)); + return branch; + } + + /// + /// Assign a staff member to this shop. + /// + public void AssignStaff(MerchantStaff staff, ShopRole role, Guid? branchId = null) + { + var member = new ShopMember(staff.Id, Id, role, branchId); + _members.Add(member); + AddDomainEvent(new StaffAssignedToShopDomainEvent(staff, this, role)); + } +} +``` + +### MerchantStaff Aggregate (Aggregate Root) + +```csharp +public class MerchantStaff : Entity, IAggregateRoot +{ + private Guid _userId; // Reference to IAM User + private Guid _merchantId; // Employer + private StaffRole _role; // Cashier, Manager, Admin + private StaffStatus _status; + private string? _employeeCode; + private ContactInfo _contactInfo; + + // For POS System quick authentication + private readonly List _deviceTokens = new(); + private string? _pinCodeHash; // Hashed 4-6 digit PIN + + // Permissions bitmask + private StaffPermissions _permissions; + + /// + /// Set PIN code for POS authentication. + /// + public void SetPinCode(string pin) + { + if (pin.Length < 4 || pin.Length > 6 || !pin.All(char.IsDigit)) + throw new DomainException("PIN must be 4-6 digits"); + + _pinCodeHash = HashPin(pin); + AddDomainEvent(new StaffPinCodeSetDomainEvent(this)); + } + + /// + /// Verify PIN code for POS login. + /// + public bool VerifyPinCode(string pin) + { + if (string.IsNullOrEmpty(_pinCodeHash)) + return false; + + return VerifyHash(pin, _pinCodeHash); + } + + /// + /// Register a device for push notifications. + /// + public void RegisterDevice(string deviceId, string deviceName, string fcmToken) + { + var existingToken = _deviceTokens.FirstOrDefault(t => t.DeviceId == deviceId); + if (existingToken != null) + { + existingToken.Update(fcmToken); + } + else + { + var token = new DeviceToken(deviceId, deviceName, fcmToken); + _deviceTokens.Add(token); + } + + AddDomainEvent(new StaffDeviceRegisteredDomainEvent(this, deviceId)); + } +} +``` + +### Value Objects + +```csharp +/// +/// Business information for merchant verification. +/// +public record BusinessInfo( + string? TaxId, + string? BusinessLicenseNumber, + string? CompanyRegistrationNumber, + DateTime? EstablishedDate +); + +/// +/// Settlement configuration for merchant payouts. +/// +public record SettlementConfig( + BankAccount BankAccount, + decimal CommissionRate, // Platform fee (%) + SettlementCycle Cycle, // Daily, Weekly, Monthly + bool AutoSettlement +); + +/// +/// Bank account for settlement. +/// +public record BankAccount( + string BankCode, + string BankName, + string AccountNumber, + string AccountHolderName +); + +/// +/// Physical address. +/// +public record Address( + string Street, + string Ward, + string District, + string City, + string Province, + string PostalCode, + string CountryCode = "VN" +); + +/// +/// Geographic location for map display and nearby search. +/// +public record GeoLocation(double Latitude, double Longitude); + +/// +/// Contact information. +/// +public record ContactInfo( + string Phone, + string? Email, + string? Website +); + +/// +/// Operating hours for physical shops. +/// +public record OperatingHours( + TimeOnly OpenTime, + TimeOnly CloseTime, + List OpenDays +); +``` + +### Enumerations + +```csharp +// Merchant Types +public class MerchantType : Enumeration +{ + public static readonly MerchantType Individual = new(1, "Individual"); + public static readonly MerchantType Company = new(2, "Company"); +} + +// Merchant Status +public class MerchantStatus : Enumeration +{ + public static readonly MerchantStatus PendingApproval = new(1, "PendingApproval"); + public static readonly MerchantStatus Active = new(2, "Active"); + public static readonly MerchantStatus Suspended = new(3, "Suspended"); + public static readonly MerchantStatus Banned = new(4, "Banned"); +} + +// Shop Types +public class ShopType : Enumeration +{ + public static readonly ShopType OnlineOnly = new(1, "OnlineOnly"); + public static readonly ShopType PhysicalOnly = new(2, "PhysicalOnly"); + public static readonly ShopType Hybrid = new(3, "Hybrid"); +} + +// Business Categories +public class BusinessCategory : Enumeration +{ + public static readonly BusinessCategory FoodBeverage = new(1, "FoodBeverage"); + public static readonly BusinessCategory Fashion = new(2, "Fashion"); + public static readonly BusinessCategory Electronics = new(3, "Electronics"); + public static readonly BusinessCategory Healthcare = new(4, "Healthcare"); + public static readonly BusinessCategory Beauty = new(5, "Beauty"); + public static readonly BusinessCategory Education = new(6, "Education"); + public static readonly BusinessCategory Entertainment = new(7, "Entertainment"); + public static readonly BusinessCategory Services = new(8, "Services"); + public static readonly BusinessCategory Other = new(99, "Other"); +} + +// Shop Roles (for Staff assignment at shop level) +public class ShopRole : Enumeration +{ + public static readonly ShopRole Cashier = new(1, "Cashier"); + public static readonly ShopRole Waiter = new(2, "Waiter"); + public static readonly ShopRole Manager = new(3, "Manager"); + public static readonly ShopRole Owner = new(4, "Owner"); +} + +// Staff Permissions (Bitmask) +[Flags] +public enum StaffPermissions +{ + None = 0, + ViewSales = 1, + ProcessPayment = 2, + RefundOrder = 4, + ManageInventory = 8, + ViewReports = 16, + ManageStaff = 32, + ManageSettings = 64, + All = int.MaxValue +} +``` + +### Domain Events + +| Event | Trigger | Purpose | +|-------|---------|---------| +| `MerchantRegisteredDomainEvent` | New merchant registration | Notify admin for review | +| `MerchantApprovedDomainEvent` | Admin approval | **Assign "Merchant" role via IAM** | +| `MerchantSuspendedDomainEvent` | Merchant suspended | Notify merchant, disable shops | +| `ShopCreatedDomainEvent` | New shop created | Analytics, indexing | +| `ShopBranchAddedDomainEvent` | Branch added | Geo-indexing for nearby search | +| `StaffInvitedDomainEvent` | Staff invitation | Send invitation email | +| `StaffJoinedDomainEvent` | Staff accepted invite | **Assign "MerchantStaff" role via IAM** | +| `StaffDeviceRegisteredDomainEvent` | POS device registered | Enable push notifications | + +## CQRS Pattern + +### Write Side (Commands) + +```mermaid +flowchart LR + subgraph Commands["✏️ Commands"] + CMD1["RegisterMerchantCommand"] + CMD2["CreateShopCommand"] + CMD3["AddShopBranchCommand"] + CMD4["InviteStaffCommand"] + CMD5["SetStaffPinCommand"] + end + + subgraph Handlers["⚙️ Command Handlers"] + H1["RegisterMerchantHandler"] + H2["CreateShopHandler"] + H3["AddShopBranchHandler"] + H4["InviteStaffHandler"] + H5["SetStaffPinHandler"] + end + + subgraph Domain["🏛️ Domain Model"] + DM["EF Core
Repository"] + end + + DB[("🐘 Database")] + + CMD1 --> H1 --> DM --> DB + CMD2 --> H2 --> DM + CMD3 --> H3 --> DM + CMD4 --> H4 --> DM + CMD5 --> H5 --> DM + + style CMD1 fill:#ef4444,stroke:#b91c1c,color:#fff + style CMD2 fill:#ef4444,stroke:#b91c1c,color:#fff + style CMD3 fill:#ef4444,stroke:#b91c1c,color:#fff +``` + +### Read Side (Queries) + +```mermaid +flowchart LR + subgraph Queries["📖 Queries"] + Q1["GetMerchantByIdQuery"] + Q2["GetShopsByMerchantQuery"] + Q3["GetNearbyShopsQuery"] + Q4["GetStaffByShopQuery"] + end + + subgraph Handlers["🔍 Query Handlers"] + QH1["GetMerchantByIdHandler"] + QH2["GetShopsByMerchantHandler"] + QH3["GetNearbyShopsHandler"] + QH4["GetStaffByShopHandler"] + end + + subgraph ReadModel["📊 Read Model"] + RM["Dapper / Raw SQL"] + end + + DB[("🐘 Database")] + + Q1 --> QH1 --> RM --> DB + Q2 --> QH2 --> RM + Q3 --> QH3 --> RM + Q4 --> QH4 --> RM + + style Q1 fill:#22c55e,stroke:#15803d,color:#fff + style Q2 fill:#22c55e,stroke:#15803d,color:#fff + style Q3 fill:#22c55e,stroke:#15803d,color:#fff +``` + +## Database Schema + +```mermaid +erDiagram + MERCHANTS ||--o{ SHOPS : owns + MERCHANTS ||--o{ MERCHANT_STAFF : employs + SHOPS ||--o{ SHOP_BRANCHES : has + SHOPS ||--o{ SHOP_MEMBERS : assigns + MERCHANT_STAFF ||--o{ SHOP_MEMBERS : works_at + MERCHANT_STAFF ||--o{ DEVICE_TOKENS : has + + MERCHANTS { + uuid id PK + uuid user_id FK "IAM.users" + uuid organization_id FK "IAM.organizations" + varchar business_name + int type_id FK + int status_id FK + varchar tax_id + varchar business_license_number + varchar bank_account_number + decimal commission_rate + int settlement_cycle + timestamp created_at + } + + SHOPS { + uuid id PK + uuid merchant_id FK + varchar name + varchar slug UK + int type_id FK + int category_id FK + int status_id FK + varchar phone + jsonb operating_hours + jsonb settings + varchar logo_url + timestamp created_at + } + + SHOP_BRANCHES { + uuid id PK + uuid shop_id FK + varchar name + varchar code + varchar street + varchar district + varchar city + decimal latitude + decimal longitude + boolean is_active + } + + MERCHANT_STAFF { + uuid id PK + uuid user_id FK "IAM.users" + uuid merchant_id FK + int role_id FK + int status_id FK + varchar employee_code + varchar pin_code_hash + int permissions + timestamp joined_at + } + + SHOP_MEMBERS { + uuid id PK + uuid staff_id FK + uuid shop_id FK + uuid branch_id FK + int shop_role_id FK + int custom_permissions + boolean is_primary + } + + DEVICE_TOKENS { + uuid id PK + uuid staff_id FK + varchar device_id UK + varchar device_name + varchar fcm_token + varchar platform + timestamp last_used_at + } +``` + +## Inter-Service Communication + +### Synchronous (HTTP) + +| Target Service | Use Case | Method | +|----------------|----------|--------| +| IAM Service | Validate user token | `GET /connect/userinfo` | +| IAM Service | Assign Merchant role | `POST /api/v1/users/{id}/roles` | +| Wallet Service | Check merchant balance | `GET /api/v1/wallets/{merchantId}` | + +### Asynchronous (Events) + +```mermaid +flowchart LR + MERCHANT["🏪 Merchant Service"] + BUS["📬 Event Bus"] + IAM["🔐 IAM Service"] + WALLET["💰 Wallet Service"] + NOTIF["🔔 Notification"] + + MERCHANT --> |MerchantApprovedEvent| BUS + MERCHANT --> |StaffJoinedEvent| BUS + MERCHANT --> |ShopCreatedEvent| BUS + + BUS --> |Subscribe| IAM + BUS --> |Subscribe| WALLET + BUS --> |Subscribe| NOTIF + + IAM --> |Assign Role| IAM + WALLET --> |Create Wallet| WALLET + NOTIF --> |Send Email| NOTIF + + style MERCHANT fill:#f97316,stroke:#c2410c,color:#fff + style BUS fill:#ef4444,stroke:#b91c1c,color:#fff + style IAM fill:#3b82f6,stroke:#1e40af,color:#fff +``` + +## Security + +### Authorization + +| Role | Permissions | +|------|-------------| +| `User` | Register merchant, Accept staff invite | +| `Merchant` | Manage own shops, Manage own staff | +| `MerchantStaff` | Access POS, View assigned shop data | +| `Admin` | Approve/suspend merchants, View all data | + +### POS Authentication + +```mermaid +sequenceDiagram + participant POS as POS Device + participant API as Merchant Service + participant IAM as IAM Service + + POS->>API: POST /api/v1/pos/auth/pin
{userId, pin, deviceId} + API->>API: Verify PIN hash + + alt PIN Valid + API->>IAM: Validate user & role + IAM-->>API: OK + API->>API: Generate short-lived token + API-->>POS: {posToken, expiresIn: 8h} + else PIN Invalid + API-->>POS: 401 Unauthorized + end +``` + +## Infrastructure + +### Docker Compose + +```yaml +merchant-service-net: + build: + context: ../.. + dockerfile: services/merchant-service-net/Dockerfile + environment: + - ASPNETCORE_ENVIRONMENT=Development + - DATABASE_URL=Host=postgres;Port=5432;Database=merchant;... + - Jwt__Authority=http://iam-service-net:8080 + - IamService__BaseUrl=http://iam-service-net:8080 + - WalletService__BaseUrl=http://wallet-service-net:8080 + depends_on: + - postgres + - iam-service-net + labels: + - "traefik.enable=true" + - "traefik.http.routers.merchant.rule=PathPrefix(`/api/v1/merchants`) || PathPrefix(`/api/v1/shops`) || PathPrefix(`/api/v1/pos`)" + - "traefik.http.services.merchant.loadbalancer.server.port=8080" +``` + +### Health Checks + +| Endpoint | Checks | Purpose | +|----------|--------|---------| +| `/health` | All dependencies | Full health status | +| `/health/live` | App is running | Kubernetes liveness | +| `/health/ready` | DB + IAM Service | Kubernetes readiness | + +## Monitoring + +### Metrics (Prometheus) + +- `merchant_registrations_total` - Number of merchant registrations +- `merchant_approvals_total` - Number of approved merchants +- `shop_creations_total` - Number of shops created +- `pos_auth_attempts_total` - Number of POS login attempts +- `pos_auth_failures_total` - Number of failed POS logins + +### Logging + +Structured Serilog logging with: +- Request/Response logging +- Domain event logging +- POS authentication audit trail +- Error tracking with stack traces + +## Error Handling + +### Error Codes + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `MERCHANT_NOT_FOUND` | 404 | Merchant does not exist | +| `MERCHANT_NOT_ACTIVE` | 400 | Merchant not yet approved | +| `SHOP_SLUG_EXISTS` | 409 | Shop slug already exists | +| `STAFF_ALREADY_EXISTS` | 409 | Staff already exists | +| `INVALID_PIN` | 401 | Incorrect PIN | +| `DEVICE_NOT_REGISTERED` | 403 | Device not registered | + +### Response Format + +```json +{ + "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4", + "title": "Bad Request", + "status": 400, + "detail": "Only active merchants can create shops.", + "errorCode": "MERCHANT_NOT_ACTIVE", + "traceId": "00-1234567890abcdef-1234567890abcdef-00" +} +``` diff --git a/services/merchant-service-net/docs/en/README.md b/services/merchant-service-net/docs/en/README.md new file mode 100644 index 00000000..e1e32275 --- /dev/null +++ b/services/merchant-service-net/docs/en/README.md @@ -0,0 +1,303 @@ +# Merchant Service .NET 10 + +> **Service for managing Merchants (Shop Owners), Shops, and Merchant Staff for the GoodGo ecosystem.** + +## Overview + +Merchant Service provides management capabilities for: + +- **Merchant Management** - Registration, verification, merchant (shop owner) management +- **Shop Management** - Create and manage stores (Online, Physical, Hybrid) +- **Staff Management** - Employee management, permissions for POS System +- **Branch Management** - Shop branch/location management +- **POS Authentication** - PIN authentication for POS devices +- **Multi-category Support** - Support for multiple business categories + +## Requirements + +| Requirement | Version | +|-------------|---------| +| .NET SDK | 10.0.101+ | +| Docker | 24.0+ | +| PostgreSQL | 15+ | + +## Quick Start + +### Run with Docker + +```bash +cd deployments/local +docker-compose up -d +``` + +### Run Local + +```bash +cd services/merchant-service-net +dotnet restore +dotnet build +dotnet run --project src/MerchantService.API +``` + +## Database Migrations + +### Requirements + +```bash +# Install EF Core tools (one-time) +dotnet tool install --global dotnet-ef +``` + +### Create Migration + +```bash +# Create new migration +dotnet ef migrations add \ + --project src/MerchantService.Infrastructure \ + --startup-project src/MerchantService.API +``` + +### Apply Migration + +```bash +# Apply migrations to database +dotnet ef database update \ + --project src/MerchantService.Infrastructure \ + --startup-project src/MerchantService.API +``` + +## API Endpoints + +### Merchant Endpoints (`/api/v1/merchants`) + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| `POST` | `/api/v1/merchants/register` | Register as a Merchant | ✅ User | +| `GET` | `/api/v1/merchants/me` | Get current Merchant info | ✅ Merchant | +| `PUT` | `/api/v1/merchants/me` | Update Merchant info | ✅ Merchant | +| `POST` | `/api/v1/merchants/me/verify` | Submit verification documents | ✅ Merchant | + +### Shop Endpoints (`/api/v1/shops`) + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| `GET` | `/api/v1/shops` | List Merchant's shops | ✅ Merchant | +| `POST` | `/api/v1/shops` | Create new shop | ✅ Merchant | +| `GET` | `/api/v1/shops/{id}` | Get shop by ID | ✅ Merchant | +| `PUT` | `/api/v1/shops/{id}` | Update shop | ✅ Merchant | +| `DELETE` | `/api/v1/shops/{id}` | Close shop | ✅ Merchant | +| `GET` | `/api/v1/shops/slug/{slug}` | Get shop by slug (public) | ❌ | + +### Shop Branch Endpoints + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| `GET` | `/api/v1/shops/{shopId}/branches` | List branches | ✅ | +| `POST` | `/api/v1/shops/{shopId}/branches` | Create new branch | ✅ Merchant | +| `PUT` | `/api/v1/shops/{shopId}/branches/{id}` | Update branch | ✅ Merchant | +| `DELETE` | `/api/v1/shops/{shopId}/branches/{id}` | Delete branch | ✅ Merchant | +| `GET` | `/api/v1/shops/nearby` | Find nearby shops | ❌ | + +### Merchant Staff Endpoints + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| `GET` | `/api/v1/merchants/me/staff` | List staff | ✅ Merchant | +| `POST` | `/api/v1/merchants/me/staff/invite` | Invite staff | ✅ Merchant | +| `PUT` | `/api/v1/merchants/me/staff/{id}` | Update staff | ✅ Merchant | +| `DELETE` | `/api/v1/merchants/me/staff/{id}` | Remove staff | ✅ Merchant | +| `POST` | `/api/v1/staff/accept-invite` | Accept invitation | ✅ User | + +### POS Endpoints (`/api/v1/pos`) + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| `POST` | `/api/v1/pos/auth/pin` | POS login with PIN | ✅ Staff | +| `POST` | `/api/v1/pos/devices/register` | Register POS device | ✅ Staff | +| `GET` | `/api/v1/pos/me` | Get current Staff info | ✅ Staff | + +### Admin Endpoints + +| Method | Endpoint | Description | Auth | +|--------|----------|-------------|------| +| `GET` | `/api/v1/admin/merchants` | List all merchants | ✅ Admin | +| `POST` | `/api/v1/admin/merchants/{id}/approve` | Approve merchant | ✅ Admin | +| `POST` | `/api/v1/admin/merchants/{id}/suspend` | Suspend merchant | ✅ Admin | + +### Health Checks + +| Endpoint | Purpose | +|----------|---------| +| `/health` | Full health status | +| `/health/live` | Liveness check | +| `/health/ready` | Readiness check | + +## Merchant Registration Flow + +### Step 1: Register as Merchant + +```bash +curl -X POST http://localhost:5003/api/v1/merchants/register \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "businessName": "Saigon Coffee", + "type": "Company", + "taxId": "0123456789", + "businessLicenseNumber": "BL-2024-001" + }' +``` + +**Response:** +```json +{ + "success": true, + "data": { + "merchantId": "550e8400-e29b-41d4-a716-446655440000", + "businessName": "Saigon Coffee", + "status": "PendingApproval" + } +} +``` + +### Step 2: Wait for Admin Approval + +Admin will review and approve the merchant. After approval: +- Role `Merchant` will be assigned to the user in IAM Service +- Status changes to `Active` + +### Step 3: Create Shop + +```bash +curl -X POST http://localhost:5003/api/v1/shops \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Saigon Coffee - District 1", + "slug": "saigon-coffee-d1", + "type": "Hybrid", + "category": "FoodBeverage", + "phone": "0901234567", + "email": "d1@saigoncoffee.vn" + }' +``` + +### Step 4: Add Branch (For physical stores) + +```bash +curl -X POST http://localhost:5003/api/v1/shops/{shopId}/branches \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Le Loi Branch", + "street": "123 Le Loi Street", + "district": "District 1", + "city": "Ho Chi Minh City", + "latitude": 10.7769, + "longitude": 106.7009 + }' +``` + +### Step 5: Invite Staff + +```bash +curl -X POST http://localhost:5003/api/v1/merchants/me/staff/invite \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "staff@example.com", + "role": "Cashier", + "shopId": "shop-uuid", + "branchId": "branch-uuid" + }' +``` + +## Configuration + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `ASPNETCORE_ENVIRONMENT` | Environment | `Development` | +| `DATABASE_URL` | PostgreSQL connection | - | +| `Jwt__Authority` | IAM Service URL | `http://iam-service-net:8080` | +| `IamService__BaseUrl` | IAM Service base URL | - | +| `WalletService__BaseUrl` | Wallet Service base URL | - | + +### appsettings.json + +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=merchant;Username=postgres;Password=postgres" + }, + "Jwt": { + "Authority": "http://localhost:5001" + }, + "IamService": { + "BaseUrl": "http://localhost:5001" + }, + "WalletService": { + "BaseUrl": "http://localhost:5004" + } +} +``` + +## Integration with Other Services + +### IAM Service +- Validate User token +- Assign Merchant/MerchantStaff roles +- Get Organization hierarchy + +### Wallet Service +- Merchant settlement +- Commission calculation +- Balance check + +### Membership Service +- Customer visit tracking → Add EXP +- Member level → Special discount at shop + +### Chat Service +- Shop support channel +- Staff-Customer conversation + +### Gift Voucher Service (Planned) +- Create voucher campaigns +- Voucher redemption at shop + +## Testing + +```bash +# Unit tests +dotnet test tests/MerchantService.UnitTests + +# Functional tests +dotnet test tests/MerchantService.FunctionalTests +``` + +## Deployment + +### Docker Build + +```bash +docker build -t goodgo/merchant-service:latest . +docker run -p 5003:8080 --env-file .env goodgo/merchant-service:latest +``` + +### Kubernetes + +See `deployments/kubernetes/merchant-service.yaml` + +## Swagger UI + +After running the service, access Swagger UI at: +- **Local**: http://localhost:5003/swagger +- **Docker**: http://localhost/api/v1/merchants/swagger + +## Resources + +- [Clean Architecture Documentation](../../docs/en/architecture/) +- [API Design Skill](../../.agent/skills/api-design/SKILL.md) +- [CQRS Pattern Skill](../../.agent/skills/cqrs-mediatr/SKILL.md) diff --git a/services/merchant-service-net/docs/vi/ARCHITECTURE.md b/services/merchant-service-net/docs/vi/ARCHITECTURE.md new file mode 100644 index 00000000..223e37f7 --- /dev/null +++ b/services/merchant-service-net/docs/vi/ARCHITECTURE.md @@ -0,0 +1,803 @@ +# Kiến Trúc Merchant Service + +## Tổng Quan Hệ Thống + +Merchant Service quản lý **Merchant (Shop Owner)**, **Shop**, và **Merchant Staff** cho hệ sinh thái GoodGo. Service này hỗ trợ đa loại hình kinh doanh: cửa hàng online, cửa hàng vật lý, và hybrid. + +```mermaid +graph TB + subgraph Client["Client Layer"] + WEB["🌐 Merchant Portal"] + POS["💳 POS System"] + MOBILE["📱 Customer App"] + end + + subgraph Gateway["API Gateway"] + TRAEFIK["⚡ Traefik"] + end + + subgraph Services["Microservices"] + IAM["🔐 IAM Service
(User & Roles)"] + MERCHANT["🏪 Merchant Service
(Shop, Staff)"] + WALLET["💰 Wallet Service"] + MEMBER["👥 Membership Service"] + VOUCHER["🎁 Voucher Service"] + end + + subgraph Data["Data Layer"] + PG[("🐘 PostgreSQL")] + end + + WEB --> TRAEFIK + POS --> TRAEFIK + MOBILE --> TRAEFIK + TRAEFIK --> |"/api/v1/auth"| IAM + TRAEFIK --> |"/api/v1/merchants
/api/v1/shops"| MERCHANT + TRAEFIK --> |"/api/v1/wallets"| WALLET + + IAM --> |"JWT Token"| MERCHANT + MERCHANT --> |"Assign Roles"| IAM + MERCHANT --> |"Settlement"| WALLET + MERCHANT -.-> |"Customer Visit"| MEMBER + MERCHANT -.-> |"Voucher Redemption"| VOUCHER + MERCHANT --> PG + + style IAM fill:#3b82f6,stroke:#1e40af,color:#fff + style MERCHANT fill:#f97316,stroke:#c2410c,color:#fff + style WALLET fill:#a855f7,stroke:#7e22ce,color:#fff + style MEMBER fill:#22c55e,stroke:#15803d,color:#fff + style VOUCHER fill:#ec4899,stroke:#be185d,color:#fff + style TRAEFIK fill:#6366f1,stroke:#4338ca,color:#fff +``` + +> [!IMPORTANT] +> **Phân chia trách nhiệm:** +> - **IAM Service**: Quản lý User identity, Roles (Merchant, MerchantStaff) +> - **Merchant Service**: Quản lý Merchant, Shop, ShopBranch, MerchantStaff +> - **Wallet Service**: Settlement, Commission calculation +> - **Voucher Service**: Gift Voucher (planned) + +## Các Lớp Clean Architecture + +``` +┌────────────────────────────────────────────────────────────┐ +│ Lớp API (Presentation) │ +│ Controllers, Commands, Queries, Validators, DTOs │ +├────────────────────────────────────────────────────────────┤ +│ Lớp Domain (Core) │ +│ Entities, Aggregates, Value Objects, Domain Events │ +│ Merchant, Shop, MerchantStaff Aggregates │ +├────────────────────────────────────────────────────────────┤ +│ Lớp Infrastructure │ +│ DbContext, Repositories, Entity Configurations │ +│ External Services (IAM Client, Wallet Client) │ +└────────────────────────────────────────────────────────────┘ +``` + +## Domain Model + +### Tổng Quan Aggregates + +```mermaid +classDiagram + class Merchant { + +Guid Id + +Guid UserId + +Guid? OrganizationId + +string BusinessName + +MerchantType Type + +MerchantStatus Status + +BusinessInfo BusinessInfo + +SettlementConfig SettlementConfig + +Register() + +Approve() + +Suspend() + +CreateShop() + } + + class Shop { + +Guid Id + +Guid MerchantId + +string Name + +string Slug + +ShopType Type + +BusinessCategory Category + +ShopStatus Status + +ContactInfo ContactInfo + +OperatingHours? Hours + +AddBranch() + +AssignStaff() + +UpdateSettings() + } + + class ShopBranch { + +Guid Id + +Guid ShopId + +string Name + +string Code + +Address Address + +GeoLocation? Location + +bool IsActive + } + + class MerchantStaff { + +Guid Id + +Guid UserId + +Guid MerchantId + +StaffRole Role + +StaffStatus Status + +string? EmployeeCode + +StaffPermissions Permissions + +SetPinCode() + +RegisterDevice() + } + + class ShopMember { + +Guid StaffId + +Guid ShopId + +Guid? BranchId + +ShopRole Role + +int? CustomPermissions + } + + Merchant "1" --> "*" Shop : owns + Merchant "1" --> "*" MerchantStaff : employs + Shop "1" --> "*" ShopBranch : has + Shop "1" --> "*" ShopMember : assigns + MerchantStaff "1" --> "*" ShopMember : works at + + style Merchant fill:#f97316,stroke:#c2410c,color:#fff + style Shop fill:#3b82f6,stroke:#1e40af,color:#fff + style MerchantStaff fill:#22c55e,stroke:#15803d,color:#fff +``` + +### Merchant Aggregate (Aggregate Root) + +```csharp +public class Merchant : Entity, IAggregateRoot +{ + private Guid _userId; // Reference to IAM User (Owner) + private Guid? _organizationId; // Reference to IAM Organization + private string _businessName; + private MerchantType _type; // Individual, Company + private MerchantStatus _status; // Pending, Active, Suspended, Banned + private MerchantVerification _verification; + private SettlementConfig _settlementConfig; + + // Value Object for business details + public BusinessInfo BusinessInfo { get; private set; } + + // Collections + private readonly List _shops = new(); + private readonly List _staff = new(); + + public IReadOnlyCollection Shops => _shops.AsReadOnly(); + public IReadOnlyCollection Staff => _staff.AsReadOnly(); + + /// + /// EN: Factory method to register a new merchant. + /// VI: Factory method để đăng ký merchant mới. + /// + public static Merchant Register(Guid userId, string businessName, MerchantType type) + { + var merchant = new Merchant + { + Id = Guid.NewGuid(), + _userId = userId, + _businessName = businessName, + _type = type, + _status = MerchantStatus.PendingApproval, + _verification = new MerchantVerification() + }; + + merchant.AddDomainEvent(new MerchantRegisteredDomainEvent(merchant)); + return merchant; + } + + /// + /// EN: Approve merchant application. + /// VI: Phê duyệt đơn đăng ký merchant. + /// + public void Approve(Guid approvedBy) + { + if (_status != MerchantStatus.PendingApproval) + throw new DomainException("Can only approve pending merchants"); + + _status = MerchantStatus.Active; + AddDomainEvent(new MerchantApprovedDomainEvent(this, approvedBy)); + } + + /// + /// EN: Create a new shop under this merchant. + /// VI: Tạo shop mới thuộc merchant này. + /// + public Shop CreateShop(string name, string slug, ShopType type, BusinessCategory category) + { + if (_status != MerchantStatus.Active) + throw new DomainException("Only active merchants can create shops"); + + var shop = new Shop(Id, name, slug, type, category); + _shops.Add(shop); + return shop; + } +} +``` + +### Shop Aggregate (Aggregate Root) + +```csharp +public class Shop : Entity, IAggregateRoot +{ + private Guid _merchantId; + private string _name; + private string _slug; + private ShopType _type; + private BusinessCategory _category; + private ShopStatus _status; + private ContactInfo _contactInfo; + private OperatingHours? _operatingHours; + private ShopSettings _settings; + + // Physical locations + private readonly List _branches = new(); + + // Staff assignments + private readonly List _members = new(); + + public IReadOnlyCollection Branches => _branches.AsReadOnly(); + public IReadOnlyCollection Members => _members.AsReadOnly(); + + /// + /// EN: Add a physical branch to the shop. + /// VI: Thêm chi nhánh vật lý cho shop. + /// + public ShopBranch AddBranch(string name, Address address, GeoLocation? location = null) + { + if (_type == ShopType.OnlineOnly) + throw new DomainException("Online-only shops cannot have physical branches"); + + var branch = new ShopBranch(Id, name, address, location); + _branches.Add(branch); + AddDomainEvent(new ShopBranchAddedDomainEvent(this, branch)); + return branch; + } + + /// + /// EN: Assign a staff member to this shop. + /// VI: Gán nhân viên cho shop này. + /// + public void AssignStaff(MerchantStaff staff, ShopRole role, Guid? branchId = null) + { + var member = new ShopMember(staff.Id, Id, role, branchId); + _members.Add(member); + AddDomainEvent(new StaffAssignedToShopDomainEvent(staff, this, role)); + } +} +``` + +### MerchantStaff Aggregate (Aggregate Root) + +```csharp +public class MerchantStaff : Entity, IAggregateRoot +{ + private Guid _userId; // Reference to IAM User + private Guid _merchantId; // Employer + private StaffRole _role; // Cashier, Manager, Admin + private StaffStatus _status; + private string? _employeeCode; + private ContactInfo _contactInfo; + + // For POS System quick authentication + private readonly List _deviceTokens = new(); + private string? _pinCodeHash; // Hashed 4-6 digit PIN + + // Permissions bitmask + private StaffPermissions _permissions; + + /// + /// EN: Set PIN code for POS authentication. + /// VI: Đặt mã PIN cho xác thực POS. + /// + public void SetPinCode(string pin) + { + if (pin.Length < 4 || pin.Length > 6 || !pin.All(char.IsDigit)) + throw new DomainException("PIN must be 4-6 digits"); + + _pinCodeHash = HashPin(pin); + AddDomainEvent(new StaffPinCodeSetDomainEvent(this)); + } + + /// + /// EN: Verify PIN code for POS login. + /// VI: Xác minh mã PIN cho đăng nhập POS. + /// + public bool VerifyPinCode(string pin) + { + if (string.IsNullOrEmpty(_pinCodeHash)) + return false; + + return VerifyHash(pin, _pinCodeHash); + } + + /// + /// EN: Register a device for push notifications. + /// VI: Đăng ký thiết bị cho push notifications. + /// + public void RegisterDevice(string deviceId, string deviceName, string fcmToken) + { + var existingToken = _deviceTokens.FirstOrDefault(t => t.DeviceId == deviceId); + if (existingToken != null) + { + existingToken.Update(fcmToken); + } + else + { + var token = new DeviceToken(deviceId, deviceName, fcmToken); + _deviceTokens.Add(token); + } + + AddDomainEvent(new StaffDeviceRegisteredDomainEvent(this, deviceId)); + } +} +``` + +### Value Objects + +```csharp +/// +/// EN: Business information for merchant verification. +/// VI: Thông tin doanh nghiệp để xác minh merchant. +/// +public record BusinessInfo( + string? TaxId, + string? BusinessLicenseNumber, + string? CompanyRegistrationNumber, + DateTime? EstablishedDate +); + +/// +/// EN: Settlement configuration for merchant payouts. +/// VI: Cấu hình thanh toán cho chi trả merchant. +/// +public record SettlementConfig( + BankAccount BankAccount, + decimal CommissionRate, // Platform fee (%) + SettlementCycle Cycle, // Daily, Weekly, Monthly + bool AutoSettlement +); + +/// +/// EN: Bank account for settlement. +/// VI: Tài khoản ngân hàng để thanh toán. +/// +public record BankAccount( + string BankCode, + string BankName, + string AccountNumber, + string AccountHolderName +); + +/// +/// EN: Physical address. +/// VI: Địa chỉ vật lý. +/// +public record Address( + string Street, + string Ward, + string District, + string City, + string Province, + string PostalCode, + string CountryCode = "VN" +); + +/// +/// EN: Geographic location for map display and nearby search. +/// VI: Vị trí địa lý để hiển thị bản đồ và tìm kiếm gần đây. +/// +public record GeoLocation(double Latitude, double Longitude); + +/// +/// EN: Contact information. +/// VI: Thông tin liên hệ. +/// +public record ContactInfo( + string Phone, + string? Email, + string? Website +); + +/// +/// EN: Operating hours for physical shops. +/// VI: Giờ hoạt động cho cửa hàng vật lý. +/// +public record OperatingHours( + TimeOnly OpenTime, + TimeOnly CloseTime, + List OpenDays +); +``` + +### Enumerations + +```csharp +// Merchant Types +public class MerchantType : Enumeration +{ + public static readonly MerchantType Individual = new(1, "Individual"); // Cá nhân + public static readonly MerchantType Company = new(2, "Company"); // Doanh nghiệp +} + +// Merchant Status +public class MerchantStatus : Enumeration +{ + public static readonly MerchantStatus PendingApproval = new(1, "PendingApproval"); + public static readonly MerchantStatus Active = new(2, "Active"); + public static readonly MerchantStatus Suspended = new(3, "Suspended"); + public static readonly MerchantStatus Banned = new(4, "Banned"); +} + +// Shop Types +public class ShopType : Enumeration +{ + public static readonly ShopType OnlineOnly = new(1, "OnlineOnly"); // Chỉ online + public static readonly ShopType PhysicalOnly = new(2, "PhysicalOnly"); // Chỉ cửa hàng + public static readonly ShopType Hybrid = new(3, "Hybrid"); // Cả hai +} + +// Business Categories (Ngành nghề) +public class BusinessCategory : Enumeration +{ + public static readonly BusinessCategory FoodBeverage = new(1, "FoodBeverage"); + public static readonly BusinessCategory Fashion = new(2, "Fashion"); + public static readonly BusinessCategory Electronics = new(3, "Electronics"); + public static readonly BusinessCategory Healthcare = new(4, "Healthcare"); + public static readonly BusinessCategory Beauty = new(5, "Beauty"); + public static readonly BusinessCategory Education = new(6, "Education"); + public static readonly BusinessCategory Entertainment = new(7, "Entertainment"); + public static readonly BusinessCategory Services = new(8, "Services"); + public static readonly BusinessCategory Other = new(99, "Other"); +} + +// Shop Roles (for Staff assignment at shop level) +public class ShopRole : Enumeration +{ + public static readonly ShopRole Cashier = new(1, "Cashier"); // Thu ngân + public static readonly ShopRole Waiter = new(2, "Waiter"); // Phục vụ + public static readonly ShopRole Manager = new(3, "Manager"); // Quản lý + public static readonly ShopRole Owner = new(4, "Owner"); // Chủ shop +} + +// Staff Permissions (Bitmask) +[Flags] +public enum StaffPermissions +{ + None = 0, + ViewSales = 1, + ProcessPayment = 2, + RefundOrder = 4, + ManageInventory = 8, + ViewReports = 16, + ManageStaff = 32, + ManageSettings = 64, + All = int.MaxValue +} +``` + +### Domain Events + +| Event | Trigger | Mục Đích | +|-------|---------|----------| +| `MerchantRegisteredDomainEvent` | Đăng ký merchant mới | Notify admin for review | +| `MerchantApprovedDomainEvent` | Admin phê duyệt | **Assign "Merchant" role via IAM** | +| `MerchantSuspendedDomainEvent` | Tạm ngưng merchant | Notify merchant, disable shops | +| `ShopCreatedDomainEvent` | Tạo shop mới | Analytics, indexing | +| `ShopBranchAddedDomainEvent` | Thêm chi nhánh | Geo-indexing for nearby search | +| `StaffInvitedDomainEvent` | Mời nhân viên | Send invitation email | +| `StaffJoinedDomainEvent` | Nhân viên chấp nhận | **Assign "MerchantStaff" role via IAM** | +| `StaffDeviceRegisteredDomainEvent` | Đăng ký thiết bị POS | Enable push notifications | + +## CQRS Pattern + +### Write Side (Commands) + +```mermaid +flowchart LR + subgraph Commands["✏️ Commands"] + CMD1["RegisterMerchantCommand"] + CMD2["CreateShopCommand"] + CMD3["AddShopBranchCommand"] + CMD4["InviteStaffCommand"] + CMD5["SetStaffPinCommand"] + end + + subgraph Handlers["⚙️ Command Handlers"] + H1["RegisterMerchantHandler"] + H2["CreateShopHandler"] + H3["AddShopBranchHandler"] + H4["InviteStaffHandler"] + H5["SetStaffPinHandler"] + end + + subgraph Domain["🏛️ Domain Model"] + DM["EF Core
Repository"] + end + + DB[("🐘 Database")] + + CMD1 --> H1 --> DM --> DB + CMD2 --> H2 --> DM + CMD3 --> H3 --> DM + CMD4 --> H4 --> DM + CMD5 --> H5 --> DM + + style CMD1 fill:#ef4444,stroke:#b91c1c,color:#fff + style CMD2 fill:#ef4444,stroke:#b91c1c,color:#fff + style CMD3 fill:#ef4444,stroke:#b91c1c,color:#fff +``` + +### Read Side (Queries) + +```mermaid +flowchart LR + subgraph Queries["📖 Queries"] + Q1["GetMerchantByIdQuery"] + Q2["GetShopsByMerchantQuery"] + Q3["GetNearbyShopsQuery"] + Q4["GetStaffByShopQuery"] + end + + subgraph Handlers["🔍 Query Handlers"] + QH1["GetMerchantByIdHandler"] + QH2["GetShopsByMerchantHandler"] + QH3["GetNearbyShopsHandler"] + QH4["GetStaffByShopHandler"] + end + + subgraph ReadModel["📊 Read Model"] + RM["Dapper / Raw SQL"] + end + + DB[("🐘 Database")] + + Q1 --> QH1 --> RM --> DB + Q2 --> QH2 --> RM + Q3 --> QH3 --> RM + Q4 --> QH4 --> RM + + style Q1 fill:#22c55e,stroke:#15803d,color:#fff + style Q2 fill:#22c55e,stroke:#15803d,color:#fff + style Q3 fill:#22c55e,stroke:#15803d,color:#fff +``` + +## Database Schema + +```mermaid +erDiagram + MERCHANTS ||--o{ SHOPS : owns + MERCHANTS ||--o{ MERCHANT_STAFF : employs + SHOPS ||--o{ SHOP_BRANCHES : has + SHOPS ||--o{ SHOP_MEMBERS : assigns + MERCHANT_STAFF ||--o{ SHOP_MEMBERS : works_at + MERCHANT_STAFF ||--o{ DEVICE_TOKENS : has + + MERCHANTS { + uuid id PK + uuid user_id FK "IAM.users" + uuid organization_id FK "IAM.organizations" + varchar business_name + int type_id FK + int status_id FK + varchar tax_id + varchar business_license_number + varchar bank_account_number + decimal commission_rate + int settlement_cycle + timestamp created_at + } + + SHOPS { + uuid id PK + uuid merchant_id FK + varchar name + varchar slug UK + int type_id FK + int category_id FK + int status_id FK + varchar phone + jsonb operating_hours + jsonb settings + varchar logo_url + timestamp created_at + } + + SHOP_BRANCHES { + uuid id PK + uuid shop_id FK + varchar name + varchar code + varchar street + varchar district + varchar city + decimal latitude + decimal longitude + boolean is_active + } + + MERCHANT_STAFF { + uuid id PK + uuid user_id FK "IAM.users" + uuid merchant_id FK + int role_id FK + int status_id FK + varchar employee_code + varchar pin_code_hash + int permissions + timestamp joined_at + } + + SHOP_MEMBERS { + uuid id PK + uuid staff_id FK + uuid shop_id FK + uuid branch_id FK + int shop_role_id FK + int custom_permissions + boolean is_primary + } + + DEVICE_TOKENS { + uuid id PK + uuid staff_id FK + varchar device_id UK + varchar device_name + varchar fcm_token + varchar platform + timestamp last_used_at + } +``` + +## Inter-Service Communication + +### Synchronous (HTTP) + +| Target Service | Use Case | Method | +|----------------|----------|--------| +| IAM Service | Validate user token | `GET /connect/userinfo` | +| IAM Service | Assign Merchant role | `POST /api/v1/users/{id}/roles` | +| Wallet Service | Check merchant balance | `GET /api/v1/wallets/{merchantId}` | + +### Asynchronous (Events) + +```mermaid +flowchart LR + MERCHANT["🏪 Merchant Service"] + BUS["📬 Event Bus"] + IAM["🔐 IAM Service"] + WALLET["💰 Wallet Service"] + NOTIF["🔔 Notification"] + + MERCHANT --> |MerchantApprovedEvent| BUS + MERCHANT --> |StaffJoinedEvent| BUS + MERCHANT --> |ShopCreatedEvent| BUS + + BUS --> |Subscribe| IAM + BUS --> |Subscribe| WALLET + BUS --> |Subscribe| NOTIF + + IAM --> |Assign Role| IAM + WALLET --> |Create Wallet| WALLET + NOTIF --> |Send Email| NOTIF + + style MERCHANT fill:#f97316,stroke:#c2410c,color:#fff + style BUS fill:#ef4444,stroke:#b91c1c,color:#fff + style IAM fill:#3b82f6,stroke:#1e40af,color:#fff +``` + +## Bảo Mật + +### Authorization + +| Role | Permissions | +|------|-------------| +| `User` | Register merchant, Accept staff invite | +| `Merchant` | Manage own shops, Manage own staff | +| `MerchantStaff` | Access POS, View assigned shop data | +| `Admin` | Approve/suspend merchants, View all data | + +### POS Authentication + +```mermaid +sequenceDiagram + participant POS as POS Device + participant API as Merchant Service + participant IAM as IAM Service + + POS->>API: POST /api/v1/pos/auth/pin
{userId, pin, deviceId} + API->>API: Verify PIN hash + + alt PIN Valid + API->>IAM: Validate user & role + IAM-->>API: OK + API->>API: Generate short-lived token + API-->>POS: {posToken, expiresIn: 8h} + else PIN Invalid + API-->>POS: 401 Unauthorized + end +``` + +## Hạ Tầng + +### Docker Compose + +```yaml +merchant-service-net: + build: + context: ../.. + dockerfile: services/merchant-service-net/Dockerfile + environment: + - ASPNETCORE_ENVIRONMENT=Development + - DATABASE_URL=Host=postgres;Port=5432;Database=merchant;... + - Jwt__Authority=http://iam-service-net:8080 + - IamService__BaseUrl=http://iam-service-net:8080 + - WalletService__BaseUrl=http://wallet-service-net:8080 + depends_on: + - postgres + - iam-service-net + labels: + - "traefik.enable=true" + - "traefik.http.routers.merchant.rule=PathPrefix(`/api/v1/merchants`) || PathPrefix(`/api/v1/shops`) || PathPrefix(`/api/v1/pos`)" + - "traefik.http.services.merchant.loadbalancer.server.port=8080" +``` + +### Health Checks + +| Endpoint | Kiểm tra | Mục đích | +|----------|----------|----------| +| `/health` | Tất cả dependencies | Trạng thái đầy đủ | +| `/health/live` | App đang chạy | Kubernetes liveness | +| `/health/ready` | DB + IAM Service | Kubernetes readiness | + +## Giám Sát + +### Metrics (Prometheus) + +- `merchant_registrations_total` - Số merchant đăng ký +- `merchant_approvals_total` - Số merchant được phê duyệt +- `shop_creations_total` - Số shop được tạo +- `pos_auth_attempts_total` - Số lần đăng nhập POS +- `pos_auth_failures_total` - Số lần đăng nhập POS thất bại + +### Logging + +Structured Serilog logging với: +- Request/Response logging +- Domain event logging +- POS authentication audit trail +- Error tracking với stack traces + +## Xử Lý Lỗi + +### Error Codes + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `MERCHANT_NOT_FOUND` | 404 | Merchant không tồn tại | +| `MERCHANT_NOT_ACTIVE` | 400 | Merchant chưa được phê duyệt | +| `SHOP_SLUG_EXISTS` | 409 | Slug shop đã tồn tại | +| `STAFF_ALREADY_EXISTS` | 409 | Nhân viên đã tồn tại | +| `INVALID_PIN` | 401 | PIN không đúng | +| `DEVICE_NOT_REGISTERED` | 403 | Thiết bị chưa đăng ký | + +### Response Format + +```json +{ + "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4", + "title": "Bad Request", + "status": 400, + "detail": "Only active merchants can create shops.", + "errorCode": "MERCHANT_NOT_ACTIVE", + "traceId": "00-1234567890abcdef-1234567890abcdef-00" +} +``` diff --git a/services/merchant-service-net/docs/vi/README.md b/services/merchant-service-net/docs/vi/README.md new file mode 100644 index 00000000..b0ac5188 --- /dev/null +++ b/services/merchant-service-net/docs/vi/README.md @@ -0,0 +1,303 @@ +# Merchant Service .NET 10 + +> **Service quản lý Merchant (Shop Owner), Shop, và Merchant Staff cho hệ sinh thái GoodGo.** + +## Tổng Quan + +Merchant Service cung cấp các chức năng quản lý: + +- **Merchant Management** - Đăng ký, xác minh, quản lý Merchant (Shop Owner) +- **Shop Management** - Tạo và quản lý cửa hàng (Online, Cửa hàng vật lý, Hybrid) +- **Staff Management** - Quản lý nhân viên, phân quyền cho POS System +- **Branch Management** - Quản lý chi nhánh cửa hàng +- **POS Authentication** - Xác thực PIN cho thiết bị POS +- **Multi-category Support** - Hỗ trợ đa ngành nghề kinh doanh + +## Yêu Cầu + +| Yêu cầu | Phiên bản | +|---------|-----------| +| .NET SDK | 10.0.101+ | +| Docker | 24.0+ | +| PostgreSQL | 15+ | + +## Bắt Đầu Nhanh + +### Chạy với Docker + +```bash +cd deployments/local +docker-compose up -d +``` + +### Chạy Local + +```bash +cd services/merchant-service-net +dotnet restore +dotnet build +dotnet run --project src/MerchantService.API +``` + +## Database Migrations + +### Yêu Cầu + +```bash +# Cài đặt EF Core tools (một lần) +dotnet tool install --global dotnet-ef +``` + +### Tạo Migration + +```bash +# Tạo migration mới +dotnet ef migrations add \ + --project src/MerchantService.Infrastructure \ + --startup-project src/MerchantService.API +``` + +### Áp Dụng Migration + +```bash +# Áp dụng migrations vào database +dotnet ef database update \ + --project src/MerchantService.Infrastructure \ + --startup-project src/MerchantService.API +``` + +## API Endpoints + +### Merchant Endpoints (`/api/v1/merchants`) + +| Method | Endpoint | Mô Tả | Auth | +|--------|----------|-------|------| +| `POST` | `/api/v1/merchants/register` | Đăng ký trở thành Merchant | ✅ User | +| `GET` | `/api/v1/merchants/me` | Lấy thông tin Merchant hiện tại | ✅ Merchant | +| `PUT` | `/api/v1/merchants/me` | Cập nhật thông tin Merchant | ✅ Merchant | +| `POST` | `/api/v1/merchants/me/verify` | Submit verification documents | ✅ Merchant | + +### Shop Endpoints (`/api/v1/shops`) + +| Method | Endpoint | Mô Tả | Auth | +|--------|----------|-------|------| +| `GET` | `/api/v1/shops` | Danh sách Shop của Merchant | ✅ Merchant | +| `POST` | `/api/v1/shops` | Tạo Shop mới | ✅ Merchant | +| `GET` | `/api/v1/shops/{id}` | Lấy Shop theo ID | ✅ Merchant | +| `PUT` | `/api/v1/shops/{id}` | Cập nhật Shop | ✅ Merchant | +| `DELETE` | `/api/v1/shops/{id}` | Đóng Shop | ✅ Merchant | +| `GET` | `/api/v1/shops/slug/{slug}` | Lấy Shop theo slug (public) | ❌ | + +### Shop Branch Endpoints + +| Method | Endpoint | Mô Tả | Auth | +|--------|----------|-------|------| +| `GET` | `/api/v1/shops/{shopId}/branches` | Danh sách chi nhánh | ✅ | +| `POST` | `/api/v1/shops/{shopId}/branches` | Tạo chi nhánh mới | ✅ Merchant | +| `PUT` | `/api/v1/shops/{shopId}/branches/{id}` | Cập nhật chi nhánh | ✅ Merchant | +| `DELETE` | `/api/v1/shops/{shopId}/branches/{id}` | Xóa chi nhánh | ✅ Merchant | +| `GET` | `/api/v1/shops/nearby` | Tìm cửa hàng gần đây | ❌ | + +### Merchant Staff Endpoints + +| Method | Endpoint | Mô Tả | Auth | +|--------|----------|-------|------| +| `GET` | `/api/v1/merchants/me/staff` | Danh sách nhân viên | ✅ Merchant | +| `POST` | `/api/v1/merchants/me/staff/invite` | Mời nhân viên | ✅ Merchant | +| `PUT` | `/api/v1/merchants/me/staff/{id}` | Cập nhật nhân viên | ✅ Merchant | +| `DELETE` | `/api/v1/merchants/me/staff/{id}` | Xóa nhân viên | ✅ Merchant | +| `POST` | `/api/v1/staff/accept-invite` | Chấp nhận lời mời | ✅ User | + +### POS Endpoints (`/api/v1/pos`) + +| Method | Endpoint | Mô Tả | Auth | +|--------|----------|-------|------| +| `POST` | `/api/v1/pos/auth/pin` | Đăng nhập POS bằng PIN | ✅ Staff | +| `POST` | `/api/v1/pos/devices/register` | Đăng ký thiết bị POS | ✅ Staff | +| `GET` | `/api/v1/pos/me` | Thông tin Staff hiện tại | ✅ Staff | + +### Admin Endpoints + +| Method | Endpoint | Mô Tả | Auth | +|--------|----------|-------|------| +| `GET` | `/api/v1/admin/merchants` | Danh sách tất cả Merchants | ✅ Admin | +| `POST` | `/api/v1/admin/merchants/{id}/approve` | Phê duyệt Merchant | ✅ Admin | +| `POST` | `/api/v1/admin/merchants/{id}/suspend` | Tạm ngưng Merchant | ✅ Admin | + +### Health Checks + +| Endpoint | Mục Đích | +|----------|----------| +| `/health` | Trạng thái health đầy đủ | +| `/health/live` | Kiểm tra sống | +| `/health/ready` | Kiểm tra sẵn sàng | + +## Quy Trình Đăng Ký Merchant + +### Bước 1: Đăng Ký Trở Thành Merchant + +```bash +curl -X POST http://localhost:5003/api/v1/merchants/register \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "businessName": "Cà Phê Sài Gòn", + "type": "Company", + "taxId": "0123456789", + "businessLicenseNumber": "BL-2024-001" + }' +``` + +**Response:** +```json +{ + "success": true, + "data": { + "merchantId": "550e8400-e29b-41d4-a716-446655440000", + "businessName": "Cà Phê Sài Gòn", + "status": "PendingApproval" + } +} +``` + +### Bước 2: Chờ Admin Phê Duyệt + +Admin sẽ review và phê duyệt Merchant. Sau khi được phê duyệt: +- Role `Merchant` sẽ được gán cho user trong IAM Service +- Status chuyển thành `Active` + +### Bước 3: Tạo Shop + +```bash +curl -X POST http://localhost:5003/api/v1/shops \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Cà Phê Sài Gòn - Quận 1", + "slug": "ca-phe-sai-gon-q1", + "type": "Hybrid", + "category": "FoodBeverage", + "phone": "0901234567", + "email": "q1@caphesaigon.vn" + }' +``` + +### Bước 4: Thêm Chi Nhánh (Cho cửa hàng vật lý) + +```bash +curl -X POST http://localhost:5003/api/v1/shops/{shopId}/branches \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Chi Nhánh Lê Lợi", + "street": "123 Lê Lợi", + "district": "Quận 1", + "city": "TP. Hồ Chí Minh", + "latitude": 10.7769, + "longitude": 106.7009 + }' +``` + +### Bước 5: Mời Nhân Viên + +```bash +curl -X POST http://localhost:5003/api/v1/merchants/me/staff/invite \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "nhanvien@example.com", + "role": "Cashier", + "shopId": "shop-uuid", + "branchId": "branch-uuid" + }' +``` + +## Cấu Hình + +### Biến Môi Trường + +| Biến | Mô Tả | Mặc định | +|------|-------|----------| +| `ASPNETCORE_ENVIRONMENT` | Môi trường | `Development` | +| `DATABASE_URL` | PostgreSQL connection | - | +| `Jwt__Authority` | IAM Service URL | `http://iam-service-net:8080` | +| `IamService__BaseUrl` | IAM Service base URL | - | +| `WalletService__BaseUrl` | Wallet Service base URL | - | + +### appsettings.json + +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=merchant;Username=postgres;Password=postgres" + }, + "Jwt": { + "Authority": "http://localhost:5001" + }, + "IamService": { + "BaseUrl": "http://localhost:5001" + }, + "WalletService": { + "BaseUrl": "http://localhost:5004" + } +} +``` + +## Integration với Các Services Khác + +### IAM Service +- Validate User token +- Assign Merchant/MerchantStaff roles +- Get Organization hierarchy + +### Wallet Service +- Merchant settlement +- Commission calculation +- Balance check + +### Membership Service +- Customer visit tracking → Add EXP +- Member level → Special discount at shop + +### Chat Service +- Shop support channel +- Staff-Customer conversation + +### Gift Voucher Service (Planned) +- Create voucher campaigns +- Voucher redemption at shop + +## Kiểm Thử + +```bash +# Unit tests +dotnet test tests/MerchantService.UnitTests + +# Functional tests +dotnet test tests/MerchantService.FunctionalTests +``` + +## Triển Khai + +### Docker Build + +```bash +docker build -t goodgo/merchant-service:latest . +docker run -p 5003:8080 --env-file .env goodgo/merchant-service:latest +``` + +### Kubernetes + +Xem file `deployments/kubernetes/merchant-service.yaml` + +## Swagger UI + +Sau khi chạy service, truy cập Swagger UI tại: +- **Local**: http://localhost:5003/swagger +- **Docker**: http://localhost/api/v1/merchants/swagger + +## Tài Nguyên + +- [Clean Architecture Documentation](../../docs/vi/architecture/) +- [API Design Skill](../../.agent/skills/api-design/SKILL.md) +- [CQRS Pattern Skill](../../.agent/skills/cqrs-mediatr/SKILL.md)