Files
pos-system/services/merchant-service-net/SERVICE_DOCS.md
Ho Ngoc Hai f3779c4ebe docs: add SERVICE_DOCS.md for all 24 microservices from per-service code audit
Each SERVICE_DOCS.md documents: Overview, API Endpoints, Commands, Queries,
Domain Model, Database Schema, Integration Events, Dependencies, Configuration.
Generated by 23 parallel audit agents reading actual source code.

Key corrections from audit:
- inventory-service: 12 commands/6 queries (was listed as scaffold)
- promotion-service: 12 commands/10 queries (was listed as 0)
- mission-service: 4 commands/7 queries (was listed as 0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:54:53 +07:00

39 KiB

MerchantService - Service Documentation

Auto-generated from source code audit. Last updated: 2026-03-13.


Overview

Property Value
Service Name MerchantService
Framework .NET 10.0 / C# 14
Architecture Clean Architecture + CQRS (MediatR 12.4)
Database PostgreSQL (Neon) via EF Core 10 + Npgsql 10
Auth JWT Bearer (Duende IdentityServer OIDC)
Port 5005 (development)
Database Name merchant_service
Health Checks /health, /health/live, /health/ready

Aggregates: Merchant, Shop (+ ShopBranch), MerchantStaff (+ DeviceToken, ShopMember), AttendanceRecord, LeaveRequest

MediatR Pipeline: LoggingBehavior -> ValidatorBehavior -> TransactionBehavior -> Handler


API Endpoints

MerchantsController

Route: api/v1/merchants | Auth: [Authorize]

Method Path Description Auth
GET /me Get current merchant profile JWT
POST /register Register a new merchant JWT
PUT /me Update current merchant JWT
POST /me/verify Submit merchant verification JWT
GET /{merchantId}/shops Get merchant's shops (paginated) JWT
PUT /{merchantId}/shops/{shopId}/set-default Set default shop JWT
POST /{merchantId}/shops/{shopId}/transfer Transfer shop ownership JWT

ShopsController

Route: api/v1/shops | Auth: [Authorize]

Method Path Description Auth
GET /stats Get shop statistics JWT
GET /{shopId}/settings Get shop settings JWT
PUT /{shopId}/settings Update shop settings JWT
GET / Get my shops JWT
GET /{shopId} Get shop by ID JWT
GET /slug/{slug} Get shop by slug Anonymous
POST / Create a new shop JWT
PUT /{shopId} Update shop JWT
POST /{shopId}/publish Publish shop (make visible) JWT
POST /{shopId}/deactivate Deactivate shop JWT
POST /{shopId}/close Close shop permanently JWT
POST /{shopId}/branches Add branch to shop JWT
GET /{shopId}/branches Get shop branches Anonymous
PUT /{shopId}/branches/{branchId} Update branch JWT
DELETE /{shopId}/branches/{branchId} Delete branch JWT
GET /nearby Get nearby shops (Haversine) Anonymous

StaffController

Route: api/v1/merchants/me/staff | Auth: [Authorize]

Method Path Description Auth
GET / Get merchant's staff list JWT
POST /create-active Create staff directly as Active JWT
POST /invite Invite a new staff member JWT
PUT /{staffId} Update staff member JWT
DELETE /{staffId} Delete/terminate staff member JWT

StaffPublicController

Route: api/v1/staff | Auth: [Authorize]

Method Path Description Auth
GET /roles Get available staff roles Anonymous
POST /accept-invite Accept staff invitation JWT
GET /debug/all Debug: list all staff Anonymous
POST /debug/seed Debug: seed test staff data Anonymous
POST /debug/update-userid Debug: update staff userId Anonymous
POST /debug/update-merchant Debug: update merchant userId Anonymous
GET /lookup Lookup staff by email Anonymous

AttendanceController

Route: api/v1/attendance

Method Path Description Auth
GET /staff/{staffId} Get attendance by staff (month/year) -
GET /shop/{shopId} Get attendance by shop (month/year) -
POST /check-in Staff check-in -
POST /check-out Staff check-out -

LeaveRequestsController

Route: api/v1/leave-requests

Method Path Description Auth
GET /staff/{staffId} Get leave requests by staff -
GET /shop/{shopId} Get leave requests by shop -
POST / Create leave request -
POST /{id}/approve Approve leave request -
POST /{id}/reject Reject leave request -

PosController

Route: api/v1/pos

Method Path Description Auth
POST /auth/pin PIN-based POS authentication Anonymous
POST /devices/register Register POS device JWT
GET /me Get current POS staff profile JWT

DevicesController

Route: api/v1/devices | Auth: [Authorize]

Method Path Description Auth
GET / List registered devices JWT

SubscriptionsController

Route: api/v1/subscriptions | Auth: [Authorize]

Method Path Description Auth
GET /me Get current subscription JWT
GET /plans Get available plans Anonymous
POST /subscribe Subscribe to a plan JWT
GET /usage Get subscription usage JWT

AdminMerchantsController

Route: api/v1/admin/merchants | Auth: [Authorize(Roles="Admin,SuperAdmin")]

Method Path Description Auth
GET / List all merchants (paginated) Admin
GET /{merchantId} Get merchant detail Admin
GET /statistics Get platform statistics Admin
POST /{merchantId}/approve Approve merchant Admin
POST /{merchantId}/reject Reject merchant Admin
POST /{merchantId}/suspend Suspend merchant Admin
POST /{merchantId}/reactivate Reactivate merchant Admin
POST /{merchantId}/ban Ban merchant Admin

AdminShopsController

Route: api/v1/admin/shops | Auth: [Authorize(Roles="Admin,SuperAdmin")]

Method Path Description Auth
GET / List all shops (paginated) Admin
GET /{shopId} Get shop detail Admin
POST /{shopId}/suspend Suspend shop Admin
POST /{shopId}/reactivate Reactivate shop Admin
POST /{shopId}/close Close shop Admin

Commands

# Command Handler Location
1 RegisterMerchantCommand RegisterMerchantCommandHandler Commands/Merchants/
2 UpdateMerchantCommand UpdateMerchantCommandHandler Commands/Merchants/
3 SubmitMerchantVerificationCommand SubmitMerchantVerificationCommandHandler Commands/Merchants/
4 CreateShopCommand CreateShopCommandHandler Commands/Shops/
5 UpdateShopCommand UpdateShopCommandHandler Commands/Shops/
6 UpdateShopSettingsCommand UpdateShopSettingsCommandHandler Commands/Shops/
7 SetDefaultShopCommand SetDefaultShopCommandHandler Commands/Shops/
8 TransferShopCommand TransferShopCommandHandler Commands/Shops/
9 PublishShopCommand PublishShopCommandHandler Commands/Shops/
10 SetShopInactiveCommand SetShopInactiveCommandHandler Commands/Shops/
11 CloseShopCommand CloseShopCommandHandler Commands/Shops/
12 AddShopBranchCommand AddShopBranchCommandHandler Commands/Shops/
13 UpdateBranchCommand UpdateBranchCommandHandler Commands/Shops/
14 DeleteBranchCommand DeleteBranchCommandHandler Commands/Shops/
15 InviteStaffCommand InviteStaffCommandHandler Commands/Staff/
16 CreateActiveStaffCommand CreateActiveStaffCommandHandler Commands/Staff/
17 UpdateStaffCommand UpdateStaffCommandHandler Commands/Staff/
18 DeleteStaffCommand DeleteStaffCommandHandler Commands/Staff/
19 AcceptInviteCommand AcceptInviteCommandHandler Commands/Staff/
20 PinAuthCommand PinAuthCommandHandler Commands/Pos/
21 RegisterDeviceCommand RegisterDeviceCommandHandler Commands/Pos/
22 CheckInCommand CheckInCommandHandler Commands/Attendance/
23 CheckOutCommand CheckOutCommandHandler Commands/Attendance/
24 CreateLeaveRequestCommand CreateLeaveRequestCommandHandler Commands/LeaveRequests/
25 ApproveLeaveRequestCommand ApproveLeaveRequestCommandHandler Commands/LeaveRequests/
26 RejectLeaveRequestCommand RejectLeaveRequestCommandHandler Commands/LeaveRequests/
27 ApproveMerchantCommand ApproveMerchantCommandHandler Commands/Admin/
28 RejectMerchantCommand RejectMerchantCommandHandler Commands/Admin/
29 SuspendMerchantCommand SuspendMerchantCommandHandler Commands/Admin/
30 ReactivateMerchantCommand ReactivateMerchantCommandHandler Commands/Admin/
31 BanMerchantCommand BanMerchantCommandHandler Commands/Admin/
32 SubscribeCommand SubscribeCommandHandler Commands/Subscriptions/

Queries

# Query Handler Location
1 GetMerchantProfileQuery GetMerchantProfileQueryHandler Queries/Merchants/
2 GetMerchantByIdQuery GetMerchantByIdQueryHandler Queries/Merchants/
3 GetMyShopsQuery GetMyShopsQueryHandler Queries/Shops/
4 GetShopByIdQuery GetShopByIdQueryHandler Queries/Shops/
5 GetShopBySlugQuery GetShopBySlugQueryHandler Queries/Shops/
6 GetMerchantShopsQuery GetMerchantShopsQueryHandler Queries/Shops/ (paginated)
7 GetShopSettingsQuery GetShopSettingsQueryHandler Queries/Shops/
8 GetShopStatsQuery GetShopStatsQueryHandler Queries/Shops/
9 GetBranchesQuery GetBranchesQueryHandler Queries/Shops/
10 GetNearbyShopsQuery GetNearbyShopsQueryHandler Queries/Shops/ (Haversine formula)
11 GetMyStaffQuery GetMyStaffQueryHandler Queries/Staff/
12 GetStaffRolesQuery GetStaffRolesQueryHandler Queries/Staff/
13 GetPosStaffQuery GetPosStaffQueryHandler Queries/Pos/
14 GetDevicesQuery GetDevicesQueryHandler Queries/Pos/
15 GetAllMerchantsQuery GetAllMerchantsQueryHandler Queries/Admin/ (paginated)
16 GetAllShopsQuery GetAllShopsQueryHandler Queries/Admin/ (paginated)
17 GetMerchantDetailQuery GetMerchantDetailQueryHandler Queries/Admin/
18 GetMerchantStatisticsQuery GetMerchantStatisticsQueryHandler Queries/Admin/
19 GetAttendanceByStaffQuery GetAttendanceByStaffQueryHandler Queries/Attendance/
20 GetAttendanceByShopQuery GetAttendanceByShopQueryHandler Queries/Attendance/
21 GetLeaveRequestsByStaffQuery GetLeaveRequestsByStaffQueryHandler Queries/LeaveRequests/
22 GetLeaveRequestsByShopQuery GetLeaveRequestsByShopQueryHandler Queries/LeaveRequests/
23 GetSubscriptionQuery GetSubscriptionQueryHandler Queries/Subscriptions/
24 GetSubscriptionPlansQuery GetSubscriptionPlansQueryHandler Queries/Subscriptions/
25 GetSubscriptionUsageQuery GetSubscriptionUsageQueryHandler Queries/Subscriptions/

Validators

# Validator Target Command
1 RegisterMerchantCommandValidator RegisterMerchantCommand
2 UpdateMerchantCommandValidator UpdateMerchantCommand
3 CreateShopCommandValidator CreateShopCommand
4 UpdateShopCommandValidator UpdateShopCommand
5 SetDefaultShopCommandValidator SetDefaultShopCommand
6 TransferShopCommandValidator TransferShopCommand
7 AddShopBranchCommandValidator AddShopBranchCommand
8 ApproveMerchantCommandValidator ApproveMerchantCommand
9 RejectMerchantCommandValidator RejectMerchantCommand
10 SuspendMerchantCommandValidator SuspendMerchantCommand
11 ReactivateMerchantCommandValidator ReactivateMerchantCommand
12 BanMerchantCommandValidator BanMerchantCommand
13 CheckInCommandValidator CheckInCommand
14 CheckOutCommandValidator CheckOutCommand
15 CreateLeaveRequestCommandValidator CreateLeaveRequestCommand
16 ApproveLeaveRequestCommandValidator ApproveLeaveRequestCommand
17 RejectLeaveRequestCommandValidator RejectLeaveRequestCommand
18 SubscribeCommandValidator SubscribeCommand

Domain Model

Aggregate: Merchant

Entity: Merchant (Aggregate Root) | Table: merchants

Fields:

Field Type Column Required
Id Guid id Yes
_userId Guid user_id Yes
_businessName string business_name (max 200) Yes
TypeId int type_id Yes
StatusId int status_id Yes
VerificationStatusId int verification_status_id Yes
_businessInfo BusinessInfo (owned) tax_id, business_license_number, company_registration_number, established_date No
_settlementConfig SettlementConfig (owned) commission_rate, settlement_cycle_id, auto_settlement, bank_* No
VerifiedAt DateTime? verified_at No
VerifiedBy Guid? verified_by No
_subscriptionPlanId int subscription_plan_id (default 0) Yes
_createdAt DateTime created_at Yes
_updatedAt DateTime? updated_at No
_isDeleted bool is_deleted (default false) Yes

Behavior Methods: Register (static factory), UpdateBusinessName, UpdateBusinessInfo, UpdateSettlementConfig, SubmitForVerification, Approve, Suspend, Reactivate, Ban, UpdateSubscriptionPlan, Delete

Domain Events: MerchantRegisteredDomainEvent, MerchantVerificationSubmittedDomainEvent, MerchantApprovedDomainEvent, MerchantSuspendedDomainEvent, MerchantBannedDomainEvent

Enumerations:

  • MerchantType: Individual(1), Company(2) | Table: merchant_types
  • MerchantStatus: PendingApproval(1), Active(2), Suspended(3), Banned(4) | Table: merchant_statuses
  • VerificationStatus: Unverified(1), Pending(2), Verified(3), Rejected(4) | Table: verification_statuses
  • SettlementCycle: Daily(1), Weekly(2), Monthly(3) | Table: settlement_cycles

Value Objects:

  • BusinessInfo: TaxId, BusinessLicenseNumber, CompanyRegistrationNumber, EstablishedDate
  • SettlementConfig: CommissionRate, SettlementCycleId, AutoSettlement, BankAccount
  • BankAccount: BankCode, BankName, AccountNumber, AccountHolderName

Aggregate: Shop

Entity: Shop (Aggregate Root) | Table: shops

Fields:

Field Type Column Required
Id Guid id Yes
_merchantId Guid merchant_id Yes
_name string name (max 100) Yes
_slug string slug (max 100, unique) Yes
TypeId int type_id Yes
CategoryId int category_id Yes
StatusId int status_id Yes
_description string? description (max 2000) No
_logoUrl string? logo_url (max 500) No
_coverImageUrl string? cover_image_url (max 500) No
_contactInfo ContactInfo (owned) phone, email, website No
_operatingHours OperatingHours (owned) open_time, close_time, open_days No
_features ShopFeatures (owned) features_config (JSONB) No
_isDefault bool is_default (default false) Yes
_createdAt DateTime created_at Yes
_updatedAt DateTime? updated_at No
_isDeleted bool is_deleted (default false) Yes
Branches List<ShopBranch> FK: shop_id -> shop_branches Navigation

Behavior Methods: UpdateInfo, UpdateSlug, UpdateContactInfo, UpdateOperatingHours, UpdateImages, UpdateFeatures, Publish, SetInactive, Close, AddBranch, RemoveBranch, SetAsDefault, ClearDefault, TransferOwnership, Delete

Domain Events: ShopCreatedDomainEvent, ShopPublishedDomainEvent, ShopClosedDomainEvent, ShopBranchAddedDomainEvent, ShopSetAsDefaultDomainEvent, ShopTransferredDomainEvent

Enumerations:

  • ShopType: OnlineOnly(1), PhysicalOnly(2), Hybrid(3) | Table: shop_types
  • ShopStatus: Draft(1), Active(2), Inactive(3), Closed(4) | Table: shop_statuses
  • BusinessCategory: FoodBeverage(1), Fashion(2), Electronics(3), Healthcare(4), Beauty(5), Education(6), Entertainment(7), Services(8), Grocery(9), HomeFurniture(10), Other(11), Cafe(12), Restaurant(13), Karaoke(14), Spa(15) | Table: business_categories

Value Objects:

  • ContactInfo: Phone, Email, Website
  • OperatingHours: OpenTime, CloseTime, OpenDays (DayOfWeek list, stored as comma-separated ints)
  • ShopFeatures: Stored as JSONB in features_config column

Child Entity: ShopBranch | Table: shop_branches

Field Type Column Required
Id Guid id Yes
ShopId Guid shop_id (FK) Yes
_name string name (max 100) Yes
_code string? code (max 20) No
_phone string? phone (max 20) No
_isActive bool is_active (default true) Yes
_address Address (owned) street, ward, district, city, province, postal_code, country_code Yes
_location GeoLocation (owned) latitude, longitude No
_operatingHours OperatingHours (owned) open_time, close_time, open_days No
_createdAt DateTime created_at Yes
_updatedAt DateTime? updated_at No

Value Objects (Branch):

  • Address: Street, Ward, District, City, Province, PostalCode, CountryCode (default "VN")
  • GeoLocation: Latitude, Longitude

Aggregate: MerchantStaff

Entity: MerchantStaff (Aggregate Root) | Table: merchant_staff

Fields:

Field Type Column Required
Id Guid id Yes
UserId Guid? user_id No
MerchantId Guid merchant_id Yes
EmployeeCode string? employee_code (max 20) No
RoleId int role_id Yes
StatusId int status_id Yes
Permissions StaffPermissions permissions (int) No
Phone string? phone (max 20) No
Email string? email (max 100) No
PinCodeHash string? pin_code_hash (max 100) No
FirstName string? first_name (max 100) No
LastName string? last_name (max 100) No
Address string? address (max 500) No
ProfilePhotoUrl string? profile_photo_url (max 500) No
DocumentFrontUrl string? document_front_url (max 500) No
DocumentBackUrl string? document_back_url (max 500) No
JoinedAt DateTime? joined_at No
TerminatedAt DateTime? terminated_at No
CreatedAt DateTime created_at Yes
UpdatedAt DateTime? updated_at No
DeviceTokens List<DeviceToken> FK: staff_id -> device_tokens Navigation
ShopAssignments List<ShopMember> FK: staff_id -> shop_members Navigation

Factory Methods: Invite (status=Invited), CreateActive (status=Active, joinedAt=now)

Behavior Methods: AcceptInvitation, Update, UpdateRole, UpdatePermissions, SetPinCode, VerifyPinCode, RegisterDevice, RemoveDevice, AssignToShop, RemoveFromShop, Deactivate, Reactivate, Terminate, HasPermission

Domain Events: StaffInvitedDomainEvent, StaffJoinedDomainEvent, StaffPinCodeSetDomainEvent, StaffDeviceRegisteredDomainEvent, StaffAssignedToShopDomainEvent, StaffTerminatedDomainEvent

Enumerations:

  • StaffRole: Cashier(1), Waiter(2), Manager(3), Admin(4), Kitchen(5), Barista(6) | Table: staff_roles (seeded: Cashier, Waiter, Manager, Admin)
  • StaffStatus: Invited(1), Active(2), Inactive(3), Terminated(4) | Table: staff_statuses
  • ShopRole: Cashier(1), Waiter(2), Manager(3), Owner(4), Kitchen(5), Barista(6) | Table: shop_roles (seeded: Cashier, Waiter, Manager, Owner)
  • StaffPermissions (Flags): None(0), ViewSales(1), ProcessPayment(2), RefundOrder(4), ManageInventory(8), ViewReports(16), ManageStaff(32), ManageSettings(64), All(MaxValue)

Child Entity: DeviceToken | Table: device_tokens

Field Type Column Required
Id Guid id Yes
StaffId Guid staff_id (FK) Yes
DeviceId string device_id (max 100) Yes
DeviceName string? device_name (max 100) No
FcmToken string? fcm_token (max 500) No
Platform string platform (max 20) Yes
LastUsedAt DateTime? last_used_at No
CreatedAt DateTime created_at Yes

Child Entity: ShopMember | Table: shop_members

Field Type Column Required
Id Guid id Yes
StaffId Guid staff_id (FK) Yes
ShopId Guid shop_id Yes
BranchId Guid? branch_id No
RoleId int role_id Yes
CustomPermissions StaffPermissions? custom_permissions (int?) No
IsPrimary bool is_primary (default false) Yes
AssignedAt DateTime assigned_at Yes

Aggregate: AttendanceRecord

Entity: AttendanceRecord (Aggregate Root) | Table: attendance_records

Fields:

Field Type Column Required
Id Guid id Yes
StaffId Guid staff_id Yes
ShopId Guid shop_id Yes
Date DateTime date Yes
CheckIn DateTime? check_in No
CheckOut DateTime? check_out No
HoursWorked decimal? hours_worked (precision 5,2) No
Status string status (max 20): Working/Completed/Late/Absent Yes
Notes string? notes (max 500) No
CreatedAt DateTime created_at Yes
_updatedAt DateTime? updated_at No

Factory: CheckInNow(staffId, shopId) — creates with status "Working"

Behavior Methods: DoCheckOut (calculates HoursWorked, sets status "Completed"), MarkAbsent, MarkLate

Indexes: ix_attendance_staff_date (StaffId + Date, UNIQUE), ix_attendance_shop_date (ShopId + Date)


Aggregate: LeaveRequest

Entity: LeaveRequest (Aggregate Root) | Table: leave_requests

Fields:

Field Type Column Required
Id Guid id Yes
StaffId Guid staff_id Yes
ShopId Guid shop_id Yes
LeaveType string leave_type (max 20): Annual/Sick/Personal/Maternity/Other Yes
StartDate DateTime start_date Yes
EndDate DateTime end_date Yes
Reason string? reason (max 500) No
Status string status (max 20): Pending/Approved/Rejected Yes
ApprovedBy Guid? approved_by No
ApprovedAt DateTime? approved_at No
RejectionReason string? rejection_reason (max 500) No
CreatedAt DateTime created_at Yes

Computed: Days = (EndDate - StartDate).Days + 1

Factory: Create(staffId, shopId, leaveType, startDate, endDate, reason) — validates endDate >= startDate, sets UTC dates

Behavior Methods: Approve(approvedBy), Reject(rejectedBy, reason?) — both require status == "Pending"

Indexes: ix_leave_requests_staff_id, ix_leave_requests_shop_id, ix_leave_requests_status


Database Schema

Tables (14 total)

Main Aggregate Tables:

Table Aggregate Type
merchants Merchant Aggregate Root
shops Shop Aggregate Root
shop_branches Shop Child entity (FK: shop_id)
merchant_staff MerchantStaff Aggregate Root
device_tokens MerchantStaff Child entity (FK: staff_id)
shop_members MerchantStaff Child entity (FK: staff_id)
attendance_records AttendanceRecord Aggregate Root
leave_requests LeaveRequest Aggregate Root

Enumeration Lookup Tables (seeded via migrations):

Table Values
merchant_types Individual(1), Company(2)
merchant_statuses PendingApproval(1), Active(2), Suspended(3), Banned(4)
verification_statuses Unverified(1), Pending(2), Verified(3), Rejected(4)
settlement_cycles Daily(1), Weekly(2), Monthly(3)
shop_types OnlineOnly(1), PhysicalOnly(2), Hybrid(3)
shop_statuses Draft(1), Active(2), Inactive(3), Closed(4)
business_categories FoodBeverage(1)...Spa(15) (15 values)
staff_roles Cashier(1), Waiter(2), Manager(3), Admin(4)
staff_statuses Invited(1), Active(2), Inactive(3), Terminated(4)
shop_roles Cashier(1), Waiter(2), Manager(3), Owner(4)

Indexes

Index Name Table Columns Unique
ix_merchants_user_id merchants user_id No
ix_merchants_status merchants status_id No
ix_shops_merchant_id shops merchant_id No
ix_shops_slug shops slug Yes
ix_shops_status shops status_id No
ix_shops_category shops category_id No
ix_shop_branches_shop_id shop_branches shop_id No
ix_merchant_staff_user_id merchant_staff user_id No
ix_merchant_staff_merchant_id merchant_staff merchant_id No
ix_merchant_staff_email merchant_staff email No
ix_device_tokens_staff_id device_tokens staff_id No
ix_device_tokens_device_id device_tokens device_id No
ix_shop_members_staff_id shop_members staff_id No
ix_shop_members_shop_id shop_members shop_id No
ix_attendance_staff_date attendance_records staff_id, date Yes
ix_attendance_shop_date attendance_records shop_id, date No
ix_leave_requests_staff_id leave_requests staff_id No
ix_leave_requests_shop_id leave_requests shop_id No
ix_leave_requests_status leave_requests status No

Owned Entities (flattened into parent table columns)

merchants table owns:

  • BusinessInfo -> tax_id, business_license_number, company_registration_number, established_date
  • SettlementConfig -> commission_rate, settlement_cycle_id, auto_settlement
    • BankAccount -> bank_code, bank_name, bank_account_number, bank_account_holder_name

shops table owns:

  • ContactInfo -> phone, email, website
  • OperatingHours -> open_time, close_time, open_days
  • ShopFeatures -> features_config (JSONB)

shop_branches table owns:

  • Address -> street, ward, district, city, province, postal_code, country_code
  • GeoLocation -> latitude, longitude
  • OperatingHours -> open_time, close_time, open_days

Domain Events

Merchant Events

Event Trigger
MerchantRegisteredDomainEvent(Merchant) Merchant.Register()
MerchantVerificationSubmittedDomainEvent(Merchant) Merchant.SubmitForVerification()
MerchantApprovedDomainEvent(Merchant, ApprovedBy) Merchant.Approve()
MerchantSuspendedDomainEvent(Merchant, Reason) Merchant.Suspend()
MerchantBannedDomainEvent(Merchant, Reason) Merchant.Ban()

Shop Events

Event Trigger
ShopCreatedDomainEvent(Shop) Shop constructor
ShopPublishedDomainEvent(Shop) Shop.Publish()
ShopClosedDomainEvent(Shop) Shop.Close()
ShopBranchAddedDomainEvent(Shop, Branch) Shop.AddBranch()
ShopSetAsDefaultDomainEvent(Shop) Shop.SetAsDefault()
ShopTransferredDomainEvent(Shop, PreviousMerchantId, NewMerchantId) Shop.TransferOwnership()

Staff Events

Event Trigger
StaffInvitedDomainEvent(Staff) MerchantStaff.Invite()
StaffJoinedDomainEvent(Staff) MerchantStaff.AcceptInvitation()
StaffPinCodeSetDomainEvent(Staff) MerchantStaff.SetPinCode()
StaffDeviceRegisteredDomainEvent(Staff, Device) MerchantStaff.RegisterDevice()
StaffAssignedToShopDomainEvent(Staff, Assignment) MerchantStaff.AssignToShop()
StaffTerminatedDomainEvent(Staff) MerchantStaff.Terminate()

DbContext

Class: MerchantServiceContext (implements DbContext + IUnitOfWork)

DbSets:

  • Merchants -> Merchant
  • Shops -> Shop
  • ShopBranches -> ShopBranch
  • MerchantStaff -> MerchantStaff
  • ShopMembers -> ShopMember
  • DeviceTokens -> DeviceToken
  • AttendanceRecords -> AttendanceRecord
  • LeaveRequests -> LeaveRequest

Key Behaviors:

  • SaveEntitiesAsync(): Dispatches domain events via MediatR before SaveChangesAsync
  • BeginTransactionAsync(): Starts ReadCommitted isolation transaction
  • CommitTransactionAsync(): Commits + disposes transaction
  • RollbackTransaction(): Rollback + disposes transaction
  • OnModelCreating(): ApplyConfigurationsFromAssembly + Ignores StaffRole/StaffStatus/ShopRole enumerations

Ignored Enumerations in ModelBuilder (resolved in-memory, not FK relationships):

  • StaffRole, StaffStatus, ShopRole

Repositories

IMerchantRepository / MerchantRepository

Method Return Description
GetByIdAsync(id) Merchant? Filters by _isDeleted=false
GetByUserIdAsync(userId) Merchant? Filters by _isDeleted=false
ExistsByUserIdAsync(userId) bool Check existence
GetAllAsync(pageNumber, pageSize) IReadOnlyList<Merchant> Paginated, ordered by createdAt desc
GetByStatusAsync(status) IReadOnlyList<Merchant> Filter by MerchantStatus
Add(merchant) Merchant Add new
Update(merchant) void Mark as Modified

IShopRepository / ShopRepository

Method Return Description
GetByIdAsync(id) Shop? Without branches
GetByIdWithBranchesAsync(id) Shop? Include branches
GetBySlugAsync(slug) Shop? Lowercase match
SlugExistsAsync(slug) bool Check slug uniqueness
GetByMerchantIdAsync(merchantId) IReadOnlyList<Shop> All shops for merchant
GetByCategoryAsync(category, page, size) IReadOnlyList<Shop> Active shops by category, paginated
GetActiveShopsWithBranchesAsync() IReadOnlyList<Shop> All active shops with branches
GetByMerchantIdPagedAsync(merchantId, statusId?, page, size) (Items, TotalCount) Paginated with optional status filter
GetDefaultByMerchantIdAsync(merchantId) Shop? Get default shop
Add(shop) Shop Add new
Update(shop) void Mark as Modified

IMerchantStaffRepository / MerchantStaffRepository

Method Return Description
GetByIdAsync(id) MerchantStaff? Include ShopAssignments + DeviceTokens
GetByUserIdAsync(userId) MerchantStaff? Include ShopAssignments, exclude Terminated
GetByUserIdAndMerchantIdAsync(userId, merchantId) MerchantStaff? Include ShopAssignments, exclude Terminated
GetByMerchantIdAsync(merchantId) IReadOnlyList<MerchantStaff> Include ShopAssignments, exclude Terminated
GetByShopIdAsync(shopId) IReadOnlyList<MerchantStaff> Via ShopAssignments join
ExistsByUserIdAndMerchantIdAsync(userId, merchantId) bool Exclude Terminated
GetByEmailAsync(email) MerchantStaff? Include ShopAssignments, exclude Terminated
GetByIdsAsync(ids) IReadOnlyList<MerchantStaff> Batch lookup by ID list
Add(staff) MerchantStaff Add new
Update(staff) void Mark as Modified

IAttendanceRepository / AttendanceRepository

Method Return Description
GetByIdAsync(id) AttendanceRecord? Simple lookup
GetTodayRecordAsync(staffId) AttendanceRecord? Today's record for staff
GetByStaffAndMonthAsync(staffId, month, year) List<AttendanceRecord> Monthly records, desc by date
GetByShopAndMonthAsync(shopId, month, year) List<AttendanceRecord> Monthly records for shop
Add(record) AttendanceRecord Add new
Update(record) void Mark as Modified

ILeaveRequestRepository / LeaveRequestRepository

Method Return Description
GetByIdAsync(id) LeaveRequest? Simple lookup
GetByStaffAsync(staffId) List<LeaveRequest> All for staff, desc by createdAt
GetByShopAsync(shopId) List<LeaveRequest> All for shop, desc by createdAt
Add(request) LeaveRequest Add new
Update(request) void Mark as Modified

Dependencies

NuGet Packages (from .csproj)

  • MediatR 12.4.1 — CQRS command/query dispatch
  • FluentValidation 11.11 — Input validation in pipeline
  • EF Core 10 (Microsoft.EntityFrameworkCore) — ORM
  • Npgsql.EntityFrameworkCore.PostgreSQL 10 — PostgreSQL provider
  • Serilog 8 — Structured logging
  • Hellang.Middleware.ProblemDetails — RFC 7807 error responses
  • Asp.Versioning 8.1 — API versioning
  • Swashbuckle 7.2 — Swagger/OpenAPI

Infrastructure Dependencies

  • PostgreSQL (Neon cloud) — Primary database
  • Redis 7 — Cache (configured but not actively used in this service)
  • IAM Service (port 5001) — JWT token authority

DI Registration (DependencyInjection.cs)

services.AddDbContext<MerchantServiceContext>(PostgreSQL + Npgsql retry 5x/30s)
services.AddScoped<IMerchantRepository, MerchantRepository>()
services.AddScoped<IShopRepository, ShopRepository>()
services.AddScoped<IMerchantStaffRepository, MerchantStaffRepository>()
services.AddScoped<IAttendanceRepository, AttendanceRepository>()
services.AddScoped<ILeaveRequestRepository, LeaveRequestRepository>()
services.AddScoped<IRequestManager, RequestManager>()

MediatR Pipeline Registration (Program.cs)

cfg.RegisterServicesFromAssemblyContaining<Program>()
cfg.AddOpenBehavior(typeof(LoggingBehavior<,>))
cfg.AddOpenBehavior(typeof(ValidatorBehavior<,>))
cfg.AddOpenBehavior(typeof(TransactionBehavior<,>))

Configuration

appsettings.json

{
    "ConnectionStrings": {
        "DefaultConnection": "Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Database=merchant_service;..."
    },
    "Redis": { "ConnectionString": "localhost:6379" },
    "Jwt": {
        "Authority": "http://localhost:5001",
        "Audience": "goodgo-api",
        "RequireHttpsMetadata": false
    }
}

appsettings.Development.json

  • Logging: Debug level
  • EF Core SQL logging enabled

appsettings.Production.json

  • Logging: Warning level only
  • ConnectionString: via environment variable (DATABASE_URL)

Environment Variables

Variable Purpose
DATABASE_URL Fallback connection string
ASPNETCORE_ENVIRONMENT Development enables sensitive data logging
Jwt:Authority JWT issuer authority URL

JWT Configuration

  • Authority: IAM IdentityServer (default http://localhost:5001)
  • Development mode: Skips signature validation (accepts any valid JWT structure)
  • Production mode: Full signature validation enabled

Health Checks

Endpoint Purpose
/health Full health (includes PostgreSQL probe)
/health/live Liveness probe (app running, no DB check)
/health/ready Readiness probe (includes PostgreSQL probe)

Subscription Plans

Plan ID Price
Starter 0 Free
Growth 1 299,000 VND/month
Pro 2 799,000 VND/month
Enterprise 3 1,999,000 VND/month

Idempotency

IRequestManager / RequestManager: Used for duplicate command detection via ClientRequest entity. Registered as scoped service.


File Structure

services/merchant-service-net/
  src/
    MerchantService.API/
      Application/
        Behaviors/
          LoggingBehavior.cs
          ValidatorBehavior.cs
          TransactionBehavior.cs
        Commands/
          Admin/           (Approve/Reject/Suspend/Ban/Reactivate Merchant)
          Attendance/       (CheckIn, CheckOut)
          LeaveRequests/    (Create/Approve/Reject LeaveRequest)
          Merchants/        (Register/Update/Verify Merchant)
          Pos/              (PinAuth, RegisterDevice)
          Shops/            (Create/Update/Publish/Close/Branch/Transfer Shop)
          Staff/            (Invite/CreateActive/Update/Delete/AcceptInvite Staff)
          Subscriptions/    (Subscribe)
        Queries/
          Admin/            (GetAllMerchants/Shops, GetMerchantDetail/Statistics)
          Attendance/       (GetAttendanceByStaff/Shop)
          LeaveRequests/    (GetLeaveRequestsByStaff/Shop)
          Merchants/        (GetMerchantProfile/ById)
          Pos/              (GetPosStaff, GetDevices)
          Shops/            (GetMyShops/ById/BySlug/Settings/Stats/Branches/Nearby)
          Staff/            (GetMyStaff, GetStaffRoles)
          Subscriptions/    (GetSubscription/Plans/Usage)
        Validations/        (18 validators)
      Controllers/
        MerchantsController.cs
        ShopsController.cs
        StaffController.cs        (StaffController + StaffPublicController)
        AttendanceController.cs
        LeaveRequestsController.cs
        PosController.cs
        DevicesController.cs
        SubscriptionsController.cs
        AdminMerchantsController.cs
        AdminShopsController.cs
      Program.cs
      appsettings.json / .Development.json / .Production.json

    MerchantService.Domain/
      AggregatesModel/
        MerchantAggregate/    (Merchant, IMerchantRepository, MerchantStatus, MerchantType, VerificationStatus, SettlementCycle, BusinessInfo, SettlementConfig, BankAccount)
        ShopAggregate/        (Shop, IShopRepository, ShopBranch, ShopStatus, ShopType, BusinessCategory, ContactInfo, OperatingHours, ShopFeatures, Address, GeoLocation)
        MerchantStaffAggregate/ (MerchantStaff, IMerchantStaffRepository, StaffRole, StaffStatus, ShopRole, StaffPermissions, DeviceToken, ShopMember)
        AttendanceAggregate/  (AttendanceRecord, IAttendanceRepository)
        LeaveRequestAggregate/ (LeaveRequest, ILeaveRequestRepository)
      Events/
        MerchantDomainEvents.cs  (5 events)
        ShopDomainEvents.cs      (6 events)
        StaffDomainEvents.cs     (6 events)
      SeedWork/               (Entity, IAggregateRoot, IRepository, IUnitOfWork, ValueObject, Enumeration)
      Exceptions/             (DomainException)

    MerchantService.Infrastructure/
      MerchantServiceContext.cs   (DbContext + IUnitOfWork)
      DependencyInjection.cs      (AddInfrastructure extension)
      EntityConfigurations/
        MerchantEntityTypeConfiguration.cs        (+ MerchantType, MerchantStatus, VerificationStatus, SettlementCycle configs)
        ShopEntityTypeConfiguration.cs            (+ ShopBranch, ShopType, ShopStatus, BusinessCategory configs)
        MerchantStaffEntityTypeConfiguration.cs   (+ DeviceToken, ShopMember, StaffRole, StaffStatus, ShopRole configs)
        AttendanceRecordEntityTypeConfiguration.cs
        LeaveRequestEntityTypeConfiguration.cs
      Repositories/
        MerchantRepository.cs
        ShopRepository.cs
        MerchantStaffRepository.cs
        AttendanceRepository.cs
        LeaveRequestRepository.cs
      Idempotency/
        IRequestManager.cs
        RequestManager.cs
        ClientRequest.cs
      Migrations/               (EF Core migrations)