diff --git a/Multi-vertical.md b/Multi-vertical.md deleted file mode 100644 index 48c2b77e..00000000 --- a/Multi-vertical.md +++ /dev/null @@ -1,408 +0,0 @@ -Multi-vertical Architecture Implementation Plan -Problem Statement / Vấn Đề -Hệ thống hiện tại có Merchant Service đang làm tốt vai trò quản trị/định danh, nhưng cần mở rộng để hỗ trợ đa ngành hàng (Multi-vertical) bao gồm: - -Retail: Bán lẻ hàng hóa vật lý (Áo, Mũ, Điện thoại...) -F&B: Food & Beverage (Quán cafe, Nhà hàng, Quán ăn...) -Services: Dịch vụ (Spa, Salon, Phòng khám...) -Hybrid: Kết hợp nhiều ngành (Quán cafe bán kèm merchandise) -Nguyên tắc thiết kế: - -Merchant Service chỉ quản lý "Ai" (Identity) và "Luật chơi" (Configuration), còn "Chơi thế nào" (Execution) phải đẩy ra các Service vệ tinh. - -Architecture Overview / Tổng Quan Kiến Trúc -🔵 Vertical Engines - Logic Ngành -🔴 Orchestration Layer -🟡 Core Services - Configuration & Identity -Client -Retail Item -Service Item -F&B Item -Ref -Get Config -⚡ Traefik Gateway -💳 Smart POS / Web -🔐 IAM Service(User/Role) -🏪 Merchant Service(Shop DNA, Settings, Staff) -💰 Wallet Service(Payment) -📦 Catalog Service(Polymorphic Products) -📝 Order Service(The Brain - Strategy Pattern) -🏭 Inventory Service(Retail Logic) -📅 Booking Service(Spa/Service Logic) -🍳 F&B Engine(Table/Session/KDS) -User Review Required -IMPORTANT - -Quyết định Scope: Plan này bao gồm 4 phases. User cần xác nhận muốn triển khai tất cả hay chỉ một số phases cụ thể. - -WARNING - -Breaking Changes trong Phase 1: Thay đổi database schema của Shop entity sẽ yêu cầu migration và có thể ảnh hưởng đến các service đang sử dụng Merchant Service API. - -Phase 1: Foundation Refactoring -Mục tiêu -Chuẩn bị Merchant Service làm nền tảng cho multi-vertical, không triển khai logic nghiệp vụ ngành. - -1.1 Database Changes -[MODIFY] Add JSONB columns to shops table -Migration SQL: - --- Migration: Add multi-vertical support columns -ALTER TABLE shops -ADD COLUMN features_config JSONB DEFAULT '{}', -ADD COLUMN vertical_settings JSONB DEFAULT '{}', -ADD COLUMN business_category VARCHAR(50) DEFAULT 'Other'; --- Index for performance -CREATE INDEX idx_shops_business_category ON shops(business_category); -CREATE INDEX idx_shops_features_config ON shops USING GIN (features_config); --- Comment documentation -COMMENT ON COLUMN shops.features_config IS 'Feature flags: HasInventory, HasBooking, HasTables, HasKitchen...'; -COMMENT ON COLUMN shops.vertical_settings IS 'Business-specific settings as JSON document'; -COMMENT ON COLUMN shops.business_category IS 'Primary business type: Retail, FoodBeverage, Services, Other'; -1.2 Domain Model Changes -[MODIFY] - -Shop.cs -New Properties: - -public class Shop : Entity, IAggregateRoot -{ - // ... existing properties ... - - // NEW: Business category enum - public BusinessCategory Category { get; private set; } - - // NEW: Feature configuration (maps to JSONB) - public ShopFeatures Features { get; private set; } - - // NEW: Dynamic settings (maps to JSONB) - public JsonDocument? Settings { get; private set; } - - // Factory method with category-based defaults - public static Shop Create(Guid merchantId, string name, BusinessCategory category) - { - var shop = new Shop(merchantId, name, category); - shop.Features = GetDefaultFeatures(category); - return shop; - } - - private static ShopFeatures GetDefaultFeatures(BusinessCategory category) => category switch - { - BusinessCategory.Retail => new ShopFeatures(HasInventory: true, HasShipping: true), - BusinessCategory.FoodBeverage => new ShopFeatures(HasTables: true, HasKitchen: true, HasInventory: true), - BusinessCategory.Services => new ShopFeatures(HasBooking: true), - _ => new ShopFeatures(HasInventory: true, HasBooking: true) // Hybrid - }; -} -[NEW] BusinessCategory.cs -// Path: MerchantService.Domain/AggregatesModel/ShopAggregate/BusinessCategory.cs -public enum BusinessCategory -{ - Retail = 1, // Bán lẻ - FoodBeverage = 2, // F&B - Services = 3, // Dịch vụ (Spa, Salon) - Other = 99 // Hybrid/Other -} -[NEW] ShopFeatures.cs (Value Object) -// Path: MerchantService.Domain/AggregatesModel/ShopAggregate/ShopFeatures.cs -public record ShopFeatures( - bool HasInventory = false, - bool HasBooking = false, - bool HasTables = false, - bool HasKitchen = false, - bool HasShipping = false, - bool HasDelivery = false -); -1.3 API Changes -[MODIFY] Shop DTOs -CreateShopRequest: - -public record CreateShopRequest( - string Name, - string? Description, -+ BusinessCategory Category = BusinessCategory.Other, -+ ShopFeaturesDto? Features = null -); -ShopResponse: - -public record ShopResponse( - Guid Id, - string Name, - string? Description, -+ BusinessCategory Category, -+ ShopFeaturesDto Features, - ... -); -[NEW] ShopFeaturesDto -public record ShopFeaturesDto( - bool HasInventory = false, - bool HasBooking = false, - bool HasTables = false, - bool HasKitchen = false, - bool HasShipping = false, - bool HasDelivery = false -); -Phase 2: Retail MVP -Mục tiêu -Xây dựng luồng bán hàng cơ bản cho ngành Retail. - -2.1 Catalog Service (NEW) -Trách nhiệm: Quản lý sản phẩm đa hình (Polymorphic Products) - -Database Schema -CREATE TABLE products ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - shop_id UUID NOT NULL REFERENCES shops(id), - name VARCHAR(255) NOT NULL, - description TEXT, - price DECIMAL(18,2) NOT NULL, - product_type VARCHAR(50) NOT NULL, -- Physical, Service, PreparedFood - attributes JSONB DEFAULT '{}', -- Type-specific attributes - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); -CREATE INDEX idx_products_shop ON products(shop_id); -CREATE INDEX idx_products_type ON products(product_type); -Core Entities -Product (Aggregate Root) -ProductType (Enum: Physical, Service, PreparedFood) -ProductVariant (Size, Color...) -ProductCategory -2.2 Inventory Service (NEW) -Trách nhiệm: Quản lý tồn kho cho Retail - -Database Schema -CREATE TABLE inventory_items ( - id UUID PRIMARY KEY, - product_id UUID NOT NULL, - shop_id UUID NOT NULL, - quantity INT NOT NULL DEFAULT 0, - reserved_quantity INT NOT NULL DEFAULT 0, - reorder_level INT DEFAULT 10, - updated_at TIMESTAMPTZ DEFAULT NOW() -); -CREATE TABLE inventory_transactions ( - id UUID PRIMARY KEY, - inventory_item_id UUID NOT NULL, - transaction_type VARCHAR(50) NOT NULL, -- IN, OUT, ADJUSTMENT - quantity INT NOT NULL, - reference_id UUID, -- Order ID, PO ID... - notes TEXT, - created_at TIMESTAMPTZ DEFAULT NOW() -); -Core Features -Stock in/out operations -Stock reservation for orders -Low stock alerts -Inventory audit trail -2.3 Order Service (NEW) - The Brain -Trách nhiệm: Điều phối đơn hàng, sử dụng Strategy Pattern - -Strategy Pattern Design -// Interface for line item processing -public interface ILineItemStrategy -{ - ProductType SupportedType { get; } - Task ValidateAsync(OrderItem item, Guid shopId); - Task ExecuteAsync(OrderItem item, Guid shopId); -} -// Strategy implementations -public class RetailStrategy : ILineItemStrategy -{ - public ProductType SupportedType => ProductType.Physical; - - public async Task ValidateAsync(OrderItem item, Guid shopId) - { - // Call Inventory Service: Check stock availability - } - - public async Task ExecuteAsync(OrderItem item, Guid shopId) - { - // Call Inventory Service: Reserve/Deduct stock - } -} -Order Flow -WalletService -InventoryService -MerchantService -OrderService -POS -WalletService -InventoryService -MerchantService -OrderService -POS -loop -[For each LineItem] -loop -[For each LineItem] -Create Order -Get Shop Features -ShopFeatures -Select Strategy by ProductType -Validate (Check Stock) -OK/Error -Process Payment -Payment OK -Execute (Deduct Stock) -Order Completed -Phase 3: F&B Vertical -Mục tiêu -Mở rộng hỗ trợ ngành F&B (Food & Beverage) - -3.1 F&B Engine (NEW) -Trách nhiệm: Quản lý phiên ăn uống và bếp - -Core Entities -Table - Sơ đồ bàn -Session - Phiên ăn (khách ngồi tại bàn) -KitchenTicket - Phiếu bếp -Recipe - Định lượng nguyên liệu -Database Schema -CREATE TABLE tables ( - id UUID PRIMARY KEY, - shop_id UUID NOT NULL, - table_number VARCHAR(50) NOT NULL, - capacity INT DEFAULT 4, - zone VARCHAR(100), - status VARCHAR(50) DEFAULT 'Available' -); -CREATE TABLE sessions ( - id UUID PRIMARY KEY, - table_id UUID NOT NULL, - started_at TIMESTAMPTZ DEFAULT NOW(), - closed_at TIMESTAMPTZ, - guest_count INT DEFAULT 1, - status VARCHAR(50) DEFAULT 'Active' -); -CREATE TABLE kitchen_tickets ( - id UUID PRIMARY KEY, - session_id UUID NOT NULL, - order_item_id UUID NOT NULL, - station VARCHAR(50), -- Bar, Kitchen, Grill... - priority INT DEFAULT 0, - status VARCHAR(50) DEFAULT 'Pending', - created_at TIMESTAMPTZ DEFAULT NOW() -); -3.2 F&B Strategy -public class FnbStrategy : ILineItemStrategy -{ - public ProductType SupportedType => ProductType.PreparedFood; - - public async Task ExecuteAsync(OrderItem item, Guid shopId) - { - // 1. Publish KitchenTicketCreated event - // 2. F&B Engine receives and displays on KDS - // 3. Optionally: Deduct ingredients from Inventory - } -} -Phase 4: Services Vertical (Spa/Salon) -Mục tiêu -Mở rộng hỗ trợ ngành Dịch vụ (Spa, Salon, Clinic) - -4.1 Booking Service (NEW) -Trách nhiệm: Quản lý "tồn kho thời gian" - -Core Entities -StaffSchedule - Lịch làm việc nhân viên -Resource - Tài nguyên (Giường, Phòng) -Appointment - Cuộc hẹn -TimeSlot - Slot thời gian khả dụng -Database Schema -CREATE TABLE staff_schedules ( - id UUID PRIMARY KEY, - staff_id UUID NOT NULL, - shop_id UUID NOT NULL, - day_of_week INT NOT NULL, - start_time TIME NOT NULL, - end_time TIME NOT NULL -); -CREATE TABLE resources ( - id UUID PRIMARY KEY, - shop_id UUID NOT NULL, - name VARCHAR(255) NOT NULL, - resource_type VARCHAR(50) NOT NULL, -- Room, Bed, Equipment - capacity INT DEFAULT 1 -); -CREATE TABLE appointments ( - id UUID PRIMARY KEY, - shop_id UUID NOT NULL, - customer_id UUID, - staff_id UUID, - resource_id UUID, - service_id UUID NOT NULL, - start_time TIMESTAMPTZ NOT NULL, - end_time TIMESTAMPTZ NOT NULL, - status VARCHAR(50) DEFAULT 'Scheduled' -); -4.2 Booking Strategy -public class ServiceStrategy : ILineItemStrategy -{ - public ProductType SupportedType => ProductType.Service; - - public async Task ValidateAsync(OrderItem item, Guid shopId) - { - // Call Booking Service: Check slot availability - // Check staff + resource availability intersection - } - - public async Task ExecuteAsync(OrderItem item, Guid shopId) - { - // Call Booking Service: Create Appointment - } -} -New Microservices Summary -Service Trách nhiệm Phase -Catalog Service Quản lý sản phẩm đa hình Phase 2 -Inventory Service Quản lý tồn kho Retail + F&B ingredients Phase 2 -Order Service Orchestrator với Strategy Pattern Phase 2 -F&B Engine Table management, KDS, Sessions Phase 3 -Booking Service Appointment scheduling Phase 4 -File Changes Summary -Phase 1 (Merchant Service Updates) -Action File -MODIFY MerchantService.Domain/AggregatesModel/ShopAggregate/Shop.cs -NEW MerchantService.Domain/AggregatesModel/ShopAggregate/BusinessCategory.cs -NEW MerchantService.Domain/AggregatesModel/ShopAggregate/ShopFeatures.cs -MODIFY MerchantService.Infrastructure/EntityConfigurations/ShopConfiguration.cs -NEW Migration file for JSONB columns -MODIFY API DTOs (CreateShopRequest, ShopResponse) -Phase 2-4 (New Services) -Action Project -NEW catalog-service-net/ -NEW inventory-service-net/ -NEW order-service-net/ -NEW fnb-engine-net/ -NEW booking-service-net/ -Verification Plan -Documentation Review - Technical Documentation reviewed by team - Architecture diagrams validated - API specifications approved -Phase 1 Verification - Migration runs successfully on test database - Existing Merchant Service APIs unchanged (backward compatible) - New ShopFeatures returned in API responses - Unit tests for new Value Objects pass -Phase 2+ Verification - Integration tests between services pass - End-to-end order flow works for Retail - Strategy Pattern correctly routes line items -Notes -NOTE - -Scope: User yêu cầu chỉ tạo documentation và planning, KHÔNG triển khai code trong phase này. - -TIP - -Phụ thuộc Skills: Khi triển khai code, tham khảo các skills: - - -domain-driven-design - -repository-pattern - -cqrs-mediatr - -inter-service-communication \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs index 3cceb315..8b856fb3 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs @@ -891,12 +891,17 @@ public class PosDataService public record StorageFolderInfo(Guid Id, Guid? ParentId, string Name, string? Path, DateTime CreatedAt); public record CreateFolderRequest(string Name, Guid? ParentId); + // Storage API wraps responses in { success, data, error } + private record StorageApiResponse(bool Success, T? Data, string? Error); + private record UserFilesResult(List Files, int TotalCount); + public async Task> GetStorageFilesAsync(int skip = 0, int take = 50, string? search = null) { AttachToken(); var qs = $"?skip={skip}&take={take}"; if (!string.IsNullOrEmpty(search)) qs += $"&search={Uri.EscapeDataString(search)}"; - return await _http.GetFromJsonAsync>($"api/bff/files{qs}", _jsonOptions) ?? new(); + var wrapper = await _http.GetFromJsonAsync>($"api/bff/files{qs}", _jsonOptions); + return wrapper?.Data?.Files ?? new(); } public async Task DeleteStorageFileAsync(Guid fileId) @@ -907,15 +912,22 @@ public class PosDataService AttachToken(); var resp = await _http.GetAsync($"api/bff/files/{fileId}/download-url"); if (!resp.IsSuccessStatusCode) return null; - var json = await resp.Content.ReadFromJsonAsync(); - return json.TryGetProperty("url", out var url) ? url.GetString() : null; + var json = await resp.Content.ReadFromJsonAsync(_jsonOptions); + if (json.TryGetProperty("data", out var data)) + { + if (data.TryGetProperty("url", out var url)) return url.GetString(); + if (data.TryGetProperty("downloadUrl", out var dl)) return dl.GetString(); + } + if (json.TryGetProperty("url", out var directUrl)) return directUrl.GetString(); + return null; } public async Task> GetFoldersAsync(Guid? parentId = null) { AttachToken(); var qs = parentId.HasValue ? $"?parentId={parentId}" : ""; - return await _http.GetFromJsonAsync>($"api/bff/folders{qs}", _jsonOptions) ?? new(); + var wrapper = await _http.GetFromJsonAsync>>($"api/bff/folders{qs}", _jsonOptions); + return wrapper?.Data?.ToList() ?? new(); } public async Task<(bool Ok, string? Error)> CreateFolderAsync(CreateFolderRequest req) diff --git a/deployments/local/docker-compose.yml b/deployments/local/docker-compose.yml index bf605be0..b429bb06 100644 --- a/deployments/local/docker-compose.yml +++ b/deployments/local/docker-compose.yml @@ -1304,6 +1304,7 @@ services: - PromotionService__BaseUrl=http://promotion-service-net:8080 - BookingService__BaseUrl=http://booking-service-net:8080 - FnbEngine__BaseUrl=http://fnb-engine-net:8080 + - StorageService__BaseUrl=http://storage-service:8080 ports: - "3001:8080" depends_on: