Files
pos-system/services/merchant-service-net/docs/vi/ARCHITECTURE.md

24 KiB

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.

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<br/>(User & Roles)"]
        MERCHANT["🏪 Merchant Service<br/>(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<br/>/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

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)

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<Shop> _shops = new();
    private readonly List<MerchantStaff> _staff = new();
    
    public IReadOnlyCollection<Shop> Shops => _shops.AsReadOnly();
    public IReadOnlyCollection<MerchantStaff> Staff => _staff.AsReadOnly();
    
    /// <summary>
    /// EN: Factory method to register a new merchant.
    /// VI: Factory method để đăng ký merchant mới.
    /// </summary>
    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;
    }
    
    /// <summary>
    /// EN: Approve merchant application.
    /// VI: Phê duyệt đơn đăng ký merchant.
    /// </summary>
    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));
    }
    
    /// <summary>
    /// EN: Create a new shop under this merchant.
    /// VI: Tạo shop mới thuộc merchant này.
    /// </summary>
    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)

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<ShopBranch> _branches = new();
    
    // Staff assignments
    private readonly List<ShopMember> _members = new();
    
    public IReadOnlyCollection<ShopBranch> Branches => _branches.AsReadOnly();
    public IReadOnlyCollection<ShopMember> Members => _members.AsReadOnly();
    
    /// <summary>
    /// EN: Add a physical branch to the shop.
    /// VI: Thêm chi nhánh vật lý cho shop.
    /// </summary>
    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;
    }
    
    /// <summary>
    /// EN: Assign a staff member to this shop.
    /// VI: Gán nhân viên cho shop này.
    /// </summary>
    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)

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<DeviceToken> _deviceTokens = new();
    private string? _pinCodeHash;   // Hashed 4-6 digit PIN
    
    // Permissions bitmask
    private StaffPermissions _permissions;
    
    /// <summary>
    /// EN: Set PIN code for POS authentication.
    /// VI: Đặt mã PIN cho xác thực POS.
    /// </summary>
    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));
    }
    
    /// <summary>
    /// EN: Verify PIN code for POS login.
    /// VI: Xác minh mã PIN cho đăng nhập POS.
    /// </summary>
    public bool VerifyPinCode(string pin)
    {
        if (string.IsNullOrEmpty(_pinCodeHash))
            return false;
        
        return VerifyHash(pin, _pinCodeHash);
    }
    
    /// <summary>
    /// EN: Register a device for push notifications.
    /// VI: Đăng ký thiết bị cho push notifications.
    /// </summary>
    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

/// <summary>
/// EN: Business information for merchant verification.
/// VI: Thông tin doanh nghiệp để xác minh merchant.
/// </summary>
public record BusinessInfo(
    string? TaxId,
    string? BusinessLicenseNumber,
    string? CompanyRegistrationNumber,
    DateTime? EstablishedDate
);

/// <summary>
/// EN: Settlement configuration for merchant payouts.
/// VI: Cấu hình thanh toán cho chi trả merchant.
/// </summary>
public record SettlementConfig(
    BankAccount BankAccount,
    decimal CommissionRate,      // Platform fee (%)
    SettlementCycle Cycle,       // Daily, Weekly, Monthly
    bool AutoSettlement
);

/// <summary>
/// EN: Bank account for settlement.
/// VI: Tài khoản ngân hàng để thanh toán.
/// </summary>
public record BankAccount(
    string BankCode,
    string BankName,
    string AccountNumber,
    string AccountHolderName
);

/// <summary>
/// EN: Physical address.
/// VI: Địa chỉ vật lý.
/// </summary>
public record Address(
    string Street,
    string Ward,
    string District,
    string City,
    string Province,
    string PostalCode,
    string CountryCode = "VN"
);

/// <summary>
/// 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.
/// </summary>
public record GeoLocation(double Latitude, double Longitude);

/// <summary>
/// EN: Contact information.
/// VI: Thông tin liên hệ.
/// </summary>
public record ContactInfo(
    string Phone,
    string? Email,
    string? Website
);

/// <summary>
/// EN: Operating hours for physical shops.
/// VI: Giờ hoạt động cho cửa hàng vật lý.
/// </summary>
public record OperatingHours(
    TimeOnly OpenTime,
    TimeOnly CloseTime,
    List<DayOfWeek> OpenDays
);

Enumerations

// 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)

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<br/>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)

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

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)

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

sequenceDiagram
    participant POS as POS Device
    participant API as Merchant Service
    participant IAM as IAM Service
    
    POS->>API: POST /api/v1/pos/auth/pin<br/>{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

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

{
  "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"
}