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>
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_typesMerchantStatus: PendingApproval(1), Active(2), Suspended(3), Banned(4) | Table:merchant_statusesVerificationStatus: Unverified(1), Pending(2), Verified(3), Rejected(4) | Table:verification_statusesSettlementCycle: Daily(1), Weekly(2), Monthly(3) | Table:settlement_cycles
Value Objects:
BusinessInfo: TaxId, BusinessLicenseNumber, CompanyRegistrationNumber, EstablishedDateSettlementConfig: CommissionRate, SettlementCycleId, AutoSettlement, BankAccountBankAccount: 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_typesShopStatus: Draft(1), Active(2), Inactive(3), Closed(4) | Table:shop_statusesBusinessCategory: 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, WebsiteOperatingHours: OpenTime, CloseTime, OpenDays (DayOfWeek list, stored as comma-separated ints)ShopFeatures: Stored as JSONB infeatures_configcolumn
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 |
| 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_statusesShopRole: 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 | 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_dateSettlementConfig-> commission_rate, settlement_cycle_id, auto_settlementBankAccount-> bank_code, bank_name, bank_account_number, bank_account_holder_name
shops table owns:
ContactInfo-> phone, email, websiteOperatingHours-> open_time, close_time, open_daysShopFeatures-> features_config (JSONB)
shop_branches table owns:
Address-> street, ward, district, city, province, postal_code, country_codeGeoLocation-> latitude, longitudeOperatingHours-> 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-> MerchantShops-> ShopShopBranches-> ShopBranchMerchantStaff-> MerchantStaffShopMembers-> ShopMemberDeviceTokens-> DeviceTokenAttendanceRecords-> AttendanceRecordLeaveRequests-> LeaveRequest
Key Behaviors:
SaveEntitiesAsync(): Dispatches domain events via MediatR before SaveChangesAsyncBeginTransactionAsync(): Starts ReadCommitted isolation transactionCommitTransactionAsync(): Commits + disposes transactionRollbackTransaction(): Rollback + disposes transactionOnModelCreating(): 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)