# HQLAND - WORKFLOW TOÀN HỆ THỐNG > Tài liệu này mô tả chi tiết luồng dữ liệu, nghiệp vụ và tương tác giữa các module trong HQLand. > **Cập nhật:** 29/04/2026 --- ## I. TỔNG QUAN KIẾN TRÚC ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ FILAMENT ADMIN PANEL v5.5 │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ Warehouse │ │ CRM │ │ Contracts │ │ Finance │ │ │ │ (Kho hàng) │ │ (Khách hàng)│ │ (Hợp đồng) │ │ (Thu tiền/Báo │ │ │ │ │ │ │ │ │ │ cáo) │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │ │ │ │ │ │ │ │ ┌──────┴───────┐ ┌──────┴───────┐ ┌──────┴───────┐ ┌───────┴─────────┐ │ │ │ Project │ │ Customer │ │ Contract │ │ Payment │ │ │ │ Product │ │ (INDIVIDUAL│ │ Appendix │ │ PaymentFine │ │ │ │ │ │ /COMPANY) │ │ Settlement │ │ Settlement │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ └─────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────────┐│ │ │ SUPPORT MODULES ││ │ │ PaymentTemplate → PaymentSchedule → PaymentScheduleItem ││ │ │ SalesPhase → SalesPhaseProduct (Pivot) ││ │ │ FormTemplate → FormField → FormPrintLog ││ │ │ MailMergeService | DiscountEngine | PriceCalculationService ││ │ └─────────────────────────────────────────────────────────────────────────┘│ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ POSTGRESQL (UUID Primary Keys) │ │ 120 Customers | 45 Products | 139 Contracts | Notifications Table │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ### Tech Stack - **Framework:** Laravel 13.x + PHP 8.3 - **Admin Panel:** Filament v5.5 (Schemas Architecture) - **Database:** PostgreSQL + UUID (100% tables) - **Excel:** PhpSpreadsheet 5.7 - **Testing:** Pest PHP 4.6 --- ## II. LUỒNG DỮ LIỆU TỔNG THỂ (END-TO-END) ### 1. Khởi tạo Dự án & Kho hàng ``` Project (Hà Quang 1) ├── PaymentTemplate (Mẫu lịch thanh toán mặc định) │ └── PaymentScheduleItem x N đợt (% + ngày đến hạn) └── Product x N (Lô đất/Căn hộ) ├── Giá gốc (qsdd_value, foundation_temp_value, total_price) ├── Hạ tầng (infrastructure_status - JSONB) └── Custom data (block, building_density, legal_status) ``` ### 2. Khởi tạo Khách hàng ``` Customer (INDIVIDUAL hoặc COMPANY) ├── INDIVIDUAL: full_name, cmnd_cccd, phone, addresses... └── COMPANY: full_name (tên cty), tax_code, representative_id └── representative → Customer (INDIVIDUAL) ``` ### 3. Luồng Bán hàng chuẩn ``` Bước 1: Tạo Đợt mở bán (SalesPhase) ├── Chọn Project ├── Chọn PaymentTemplate (mẫu lịch thanh toán) ├── Định chính sách chiết khấu mặc định (discount_policy JSONB) └── Thêm sản phẩm vào đợt (SalesPhaseProduct pivot) ├── Giá riêng cho đợt (sale_price/land_value/foundation_value) ├── Chiết khấu riêng (discount_details JSONB) └── Status: Còn hàng | Đã giữ | Đã bán | Khóa Bước 2: Tạo Hợp đồng (Contract) ├── Chọn Product (Lô đất) ├── [Optional] Chọn SalesPhase → Auto populate giá từ SalesPhaseProduct ├── Nhập giá trị tài chính: │ land_value + foundation_value = total_value (auto) ├── Chiết khấu (discount_details JSONB) ├── Chọn Khách hàng (belongsToMany qua contract_customers) ├── Chọn PaymentTemplate (để tạo lịch thanh toán sau khi lưu) └── [Auto] Khi save: ├── Booted: total_value = land_value + foundation_value ├── Saved: PriceCalculationService tạo calculation_log └── AfterCreate: ContractScheduleService tạo PaymentSchedule Bước 3: PaymentSchedule tự động tạo ├── Contract → PaymentSchedule (1-1) └── PaymentSchedule → PaymentScheduleItem x N ├── Mỗi item = 1 đợt thanh toán ├── amount = total_value × (percentage / 100) ├── due_date tính từ signing_date + days_after_signing └── Sắp xếp theo installment_no Bước 4: Thu tiền (Payment) ├── Chọn Contract ├── [Optional] Chọn PaymentScheduleItem (đợt thanh toán) │ Nếu không chọn = Tạm ứng / không đối soát đợt ├── Nhập amount, paid_date, method, receipt_number ├── [Validation] amount không vượt quá công nợ đợt / công nợ HĐ └── [Auto - PaymentObserver] Khi Payment created/updated/deleted: ├── recalculateContract: SUM payments → paid_amount │ remaining_amount = total_value - paid_amount │ excess_amount = paid_amount - total_value (nếu > 0) └── applySurplusToNextInstallment: Nếu excess_amount > 0 → Auto tạo Payment mới cho đợt tiếp theo ``` ### 4. In ấn (Form Templates) ``` FormTemplate (Admin tạo mẫu) ├── HTML content với placeholder {{ma_truong}} └── FormField x N: ├── db_column: Lấy trực tiếp cột từ Model ├── db_relation: Lấy qua relation (customers.0.full_name) ├── formula: Tính toán (dùng Shunting Yard + bcmath) ├── input: Nhập tay khi in └── static: Giá trị cố định Khi in: MailMergeService::render(template, record) ├── evaluateFields() → Tính giá trị tất cả fields ├── Thay placeholder → rendered HTML └── savePrintLog() → Lưu snapshot + rendered HTML + user + timestamp ``` --- ## III. CHI TIẾT MODULE ### 3.1. WAREHOUSE (Kho hàng) #### Models | Model | Key Fields | Quan hệ | |-------|-----------|---------| | **Project** | code, name, type, address, payment_template_id | hasMany Products, hasMany SalesPhases, hasMany PaymentTemplates | | **Product** | code, project_id, product_type (LAND/APARTMENT), area, price_per_unit, total_price, qsdd_value, foundation_temp_value, contract_temp_value, infrastructure_status (JSONB), custom_data (JSONB), status | belongsTo Project, hasMany Contracts, belongsToMany SalesPhases | #### Nghiệp vụ 1. **Import từ Excel** (`import:products-excel`): - File: `sanpham.xlsx` - Parse hạ tầng từ chuỗi "Key: Value - Key2: Value2" → `infrastructure_status` JSONB - Parse custom_data: block, building_density, legal_status_raw - `updateOrCreate` theo `code + project_id` 2. **Tìm đợt mở bán đang active** (`Product::activeSalesPhase()`): - SalesPhase.status = "Đang mở bán" - SalesPhaseProduct.status = "Còn hàng" - Ngày hiện tại nằm trong [start_date, end_date] --- ### 3.2. CRM (Khách hàng) #### Models | Model | Key Fields | Quan hệ | |-------|-----------|---------| | **Customer** | type (INDIVIDUAL/COMPANY), full_name, cmnd_cccd, tax_code, title, phone, secondary_phones (JSONB), email, dob, permanent_address, contact_address, id_issue_date, id_issue_place, representative_id | belongsToMany Contracts (via contract_customers), representative → Customer, representedCompanies → Customer[] | #### Nghiệp vụ 1. **Import từ Excel** (`import:customers-excel`): - File: `khachhang.xlsx` - Tách nhiều số điện thoại (dấu phẩy, gạch chéo, xuống dòng) → `phone` + `secondary_phones` - Parse ngày Excel serial number → Carbon - Tạo mẫu: Công ty TNHH BĐS Thịnh Vượng + Ngườ đại diện 2. **Form tạo/sửa** (CustomerForm): - Chuyển đổi động INDIVIDUAL/COMPANY (live) - INDIVIDUAL: Hiện dob, id_issue_date, id_issue_place - COMPANY: Hiện tax_code, representative_id (chọn từ danh sách cá nhân) - Copy địa chỉ thường trú → liên hệ (suffixAction) --- ### 3.3. CONTRACTS (Hợp đồng) #### Models | Model | Key Fields | Quan hệ | |-------|-----------|---------| | **Contract** | contract_number, contract_type (HĐMB/HĐGV/HĐDC), product_id, status, signing_date, sale_date, hql_confirmation_date, land_value, foundation_value, total_value, total_value_with_foundation, paid_amount, remaining_amount, excess_amount, discount_details (JSONB), calculation_log (JSONB), brokerage_name, stored_contract_count, filing_note, transfer_order, payment_template_id, sales_phase_id | belongsTo Product, belongsToMany Customers (via contract_customers + pivot role/transfer_order), belongsTo PaymentTemplate, belongsTo SalesPhase, hasOne PaymentSchedule, hasManyThrough PaymentScheduleItem, hasMany Payments, hasMany PaymentFines, hasMany Appendices | #### Nghiệp vụ **A. Tạo hợp đồng mới (CreateContract)** ``` 1. Chọn Product → Auto điền: - total_value = product.total_price - land_value = product.qsdd_value - foundation_value = product.foundation_temp_value 2. [Optional] Chọn SalesPhase → Auto điền từ SalesPhaseProduct: - land_value, foundation_value, sale_price - Nếu có sale_price → total_value = sale_price - discount_details từ pivot 3. Nhập tay land_value, foundation_value → total_value auto cập nhật 4. Nhập discount_details (Key-Value) + xem tổng quan chiết khấu 5. Chọn Customers (multiple), nhập các thông tin quản lý 6. Chọn payment_template_id → [Auto] Sau khi lưu tạo PaymentSchedule ``` **B. Booted Logic (Model Contract)** ``` static::saving: land_value + foundation_value → total_value Nếu tạo mới và chưa có giá → fallback lấy product.total_price remaining_amount = total_value - paid_amount static::saved: Nếu có land_value/foundation_value: PriceCalculationService::calculateForContract() → calculation_log Cập nhật calculation_log (steps, final_values, price_sheet) ``` **C. Calculation Pipeline** ``` Input: land_value, foundation_value, discount_details, vat_rate Step 1: Giá trị QSDĐ → land_value Step 2: Giá trị Móng → foundation_value Step 3: Subtotal = land_value + foundation_value Step 4: Chiết khấu = total_amount HOẶC total_percentage × subtotal Step 5: Net value = subtotal - discount Step 6: VAT = net_value × vat_rate% Step 7: Total payment = net_value + VAT Output: calculation_log lưu snapshot tất cả bước ``` **D. Lịch thanh toán (ContractScheduleService)** ``` generateFromTemplate(contract, template): DB::transaction: Xóa lịch cũ (nếu có) Tạo PaymentSchedule Duyệt template.items theo installment_no: due_date = signing_date + days_after_signing amount = contract.total_value × (percentage / 100) Tạo PaymentScheduleItem ``` **E. Các module phụ** - **Appendix (Phụ lục):** belongsTo Contract + Product, custom_data JSONB - **Settlement (Quyết toán):** belongsTo Product, temp_value, final_value, difference - **PaymentFine (Tiền phạt):** belongsTo Contract, amount, due_date, paid_date --- ### 3.4. FINANCE (Tài chính & Thu tiền) #### Models | Model | Key Fields | Quan hệ | |-------|-----------|---------| | **Payment** | contract_id, schedule_item_id, amount, paid_date, method, receipt_number, metadata (JSONB) | belongsTo Contract, belongsTo PaymentScheduleItem | | **PaymentSchedule** | contract_id, template_id | belongsTo Contract, belongsTo PaymentTemplate, hasMany Items | | **PaymentScheduleItem** | schedule_id, installment_no, type (PaymentType enum), percentage, amount, due_date, days_after_signing, days_after_previous | belongsTo PaymentSchedule, hasMany Payments | #### PaymentType Enum | Value | Label | |-------|-------| | QSDD | Tiền QSDĐ | | MONG | Tiền Móng | | THAN | Tiền Thân | | CHI_PHI_TC | Chi phí thi công | | CK | Chiết khấu | | PHAT | Tiền phạt | | OTHER | Khác | #### Nghiệp vụ **A. Thu tiền (PaymentForm)** ``` 1. Chọn Contract → Cascade: Đợt thanh toán lọc theo contract 2. Chọn Đợt TT (optional) - Để trống = Tạm ứng / Không đối soát đợt 3. Nhập amount - Helper text hiển thị công nợ đợt / công nợ HĐ - Validation: Không cho phép thu quá công nợ 4. Nhập paid_date, method, receipt_number 5. Lưu ``` **B. PaymentObserver (Tự động)** ``` created/updated/deleted: ├── recalculateContract(contract): │ totalPaid = SUM(payments.amount) │ paid_amount = totalPaid │ Nếu totalPaid > total_value: │ remaining_amount = 0 │ excess_amount = totalPaid - total_value │ Ngược lại: │ remaining_amount = total_value - totalPaid │ excess_amount = 0 │ saveQuietly() └── applySurplusToNextInstallment(contract): Nếu excess_amount > 0: Tìm đợt tiếp theo chưa thanh toán đủ (theo installment_no) applyAmount = min(excess, remaining_for_item) Tạo Payment auto: method = "Tự động khấu trừ" receipt_number = "AUTO-SURPLUS-..." metadata = {auto_surplus: true} ``` **C. Bảng thu tiền (PaymentsTable)** - Cột: Hợp đồng, Số tiền, Ngày thu, Phương thức, Số phiếu thu - Cột mở rộng: Loại đợt, Đợt TT, Trạng thái đối soát (Đủ/Thiếu/Thừa/Tạm ứng), Còn thiếu - Filter: Phương thức, Ngày thu (range) - Eager load: `scheduleItem.payments` --- ### 3.5. SALES PHASES (Đợt mở bán) #### Models | Model | Key Fields | Quan hệ | |-------|-----------|---------| | **SalesPhase** | project_id, name, code, status (Chuẩn bị/Đang mở bán/Tạm dừng/Đã đóng), start_date, end_date, description, payment_template_id, discount_policy (JSONB) | belongsTo Project, belongsTo PaymentTemplate, hasMany phaseProducts, belongsToMany Products | | **SalesPhaseProduct** (Pivot) | sales_phase_id, product_id, sale_price, land_value, foundation_value, discount_details (JSONB), status (Còn hàng/Đã giữ/Đã bán/Khóa) | belongsTo SalesPhase, belongsTo Product | #### Nghiệp vụ 1. **Tạo đợt mở bán:** - Thông tin cơ bản (name, code, status, dates) - Chính sách & Mẫu thanh toán - Danh sách sản phẩm (Repeater) với giá riêng và chiết khấu riêng 2. **Tích hợp vào ContractForm:** - Khi chọn `sales_phase_id` + `product_id` → Auto populate giá từ pivot - CreateContract: Nếu không chọn payment_template_id trực tiếp → fallback lấy từ `salesPhase->paymentTemplate` --- ### 3.6. FORM TEMPLATES (Biểu mẫu in ấn) #### Models | Model | Key Fields | Quan hệ | |-------|-----------|---------| | **FormTemplate** | name, code, target_model (Contract/Product/Customer), html_template, paper_size (A4/A5/Letter), is_active | hasMany fields, hasMany printLogs | | **FormField** | template_id, code, label, source_type (db_column/db_relation/formula/input/static), source_config (JSONB), format (text/number/currency/date/percent), decimal_places, display_order | belongsTo template | | **FormPrintLog** | template_id, target_model, target_id, target_number, snapshot_data (JSONB), rendered_html, printed_by, printed_at | belongsTo template, belongsTo printedBy (User) | #### MailMergeService Workflow ``` evaluateFields(template, record): foreach field: db_column → record->{column} db_relation → record->{relation}[index]->{column} formula → Shunting Yard evaluate (bcmath) input → default value static → fixed value render(template, record): evaluateFields() → raw_values formatValue() theo format/currency/date/percent str_replace placeholder {{code}} → value trong html_template savePrintLog(): Lưu snapshot_data + rendered_html + user + timestamp ``` --- ## IV. DASHBOARD & BÁO CÁO ### Widgets | Widget | Dữ liệu | |--------|---------| | **RecentNotifications** | Thông báo chưa đọc của user hiện tại (database notifications) | | **ContractStatsOverview** | Tổng doanh thu, Đã thu, Công nợ phải thu, HĐ hiệu lực, Đợt TT sắp đến hạn | | **UpcomingPaymentsTable** | PaymentScheduleItem trong 30 ngày tới (có đủ công nợ) | ### Pages | Page | Chức năng | |------|-----------| | **ProjectReport** | Bảng thống kê theo dự án: Tổng SP, Đã bán, Số HĐ, Tổng giá trị, Đã thu, Công nợ | ### Commands | Command | Chức năng | |---------|-----------| | `export:debt-report` | Xuất Excel 2 sheet: Tổng hợp công nợ + Chi tiết đợt TT | | `notifications:send-due-payments` | Gửi cảnh báo đợt TT sắp đến hạn cho tất cả users (database notification) | | `contracts:generate-schedules` | Tạo lịch thanh toán hàng loạt cho HĐ chưa có lịch | --- ## V. CÁC COMMAND IMPORT DỮ LIỆU ### `import:products-excel {file=sanpham.xlsx}` - Input: File Excel sản phẩm - Output: Product records trong Project "Hà Quang 1" - Logic: `updateOrCreate` theo code, parse infrastructure_status, parse custom_data ### `import:customers-excel {file=khachhang.xlsx}` - Input: File Excel khách hàng - Output: Customer records (INDIVIDUAL) + Mẫu Công ty - Logic: Tách nhiều phone, parse Excel date serial ### `import:contracts-complex {hopdong=hopdong.xlsx} {hdkh=Hd_kh.xlsx}` - Input: 2 file (tài chính + liên kết KH) - Logic "Bắc cầu": 1. Đọc hopdong.xlsx → Map `contract_number → finance_data` 2. Đọc Hd_kh.xlsx → Tìm product theo plotCode, customer theo cmnd_cccd 3. Tìm mapping: `str_contains(contract_number, plotCode)` 4. `updateOrCreate` Contract + `syncWithoutDetaching` customers qua pivot - Transaction bọc toàn bộ ### `contracts:generate-schedules {--force}` - Input: HĐ chưa có PaymentSchedule (hoặc --force để tạo lại) - Logic: Ưu tiên `contract.paymentTemplate`, fallback `product.project.paymentTemplate` --- ## VI. MÔ HÌNH QUAN HỆ DATABASE (ER Tóm tắt) ``` Project 1───* Product 1───* Contract *───* Customer │ │ │ │ │ 1───1 PaymentSchedule 1───* PaymentScheduleItem │ │ │ │ │ │ *───* Payment *───────────┘ │ │ │ │ │ *───* Appendix │ │ *───* PaymentFine │ │ *───* Settlement (qua Product) │ │ │ *───* SalesPhase *───* SalesPhaseProduct *───1 Product │ │ │ 1───1 PaymentTemplate 1───* PaymentScheduleItem (template) │ *───* PaymentTemplate FormTemplate 1───* FormField 1───* FormPrintLog User 1───* Notification (MorphMany) ``` --- ## VII. ĐIỂM CẦN XEM XÉT (TỪ ASSESSMENT) | # | Vấn đề | Mức độ | Ghi chú | |---|--------|--------|---------| | 1 | **~~Soft Delete~~ ✅** | 🟢 Đã xong | Contract, Payment, Customer có SoftDeletes + Restore/ForceDelete UI | | 2 | **Phân quyền** | 🟡 Đang thiết kế | Kiến trúc Hướng B (Tự viết, không Spatie). Xem chi tiết Phần VIII | | 3 | **~~Payment.collected_by~~ ✅** | 🟢 Đã xong | Đã thêm collected_by (FK → users) + hiển thị Form/Table | | 4 | **Sổ quỹ** | 🟡 Trung bình | Thu tiền nhưng không ghi vào quỹ TM/NH. Không đối soát được | | 5 | **CRM Pipeline** | 🟢 Thấp | Chưa quản lý Lead/Khách hàng tiềm năng | | 6 | **Báo cáo BCTC** | 🟢 Thấp | Chưa có báo cáo theo chuẩn kế toán VN | --- --- ## VIII. PERMISSION SYSTEM DESIGN (Hướng B - Tự viết, không Spatie) > **Quyết định kiến trúc:** Không dùng Spatie Permission. Tự viết module phân quyền đơn giản, đủ dùng, kiểm soát 100%. > **Lý do:** Tránh config phức tạp, tránh xung đột UUID/BIGINT, phù hợp 5-10 role, không cần advanced features. ### Kiến trúc 3 lớp ``` LAYER 1: ROLE TEMPLATE (Mẫu nhóm) role_templates.id(UUID) | name | description | permissions(JSONB) | is_active permissions lưu dạng: { "contracts": ["view","create","update","delete","restore","forceDelete","export"], "payments": ["view","create","update","delete"], "customers": ["view","create","update","delete"] } LAYER 2: USER (Kế thừa + Override) users.role_template_id(UUID) → nullable users.extra_permissions(JSONB) → thêm quyền vượt cấp users.excluded_permissions(JSONB) → bớt quyền so với mẫu LAYER 3: EFFECTIVE PERMISSIONS (Tính toán động, cache trong Session) effective = template_permissions + extra_permissions - excluded_permissions Tính 1 lần khi user login → lưu vào session()->get('user.{id}.permissions') Tự động xóa khi logout Có thể xóa thủ công: auth()->user()->clearPermissionCache() ``` ### Quy tắc khai báo Permission trong Resource Mỗi Resource khai báo: ```php class ContractResource extends Resource { protected static array $permissionActions = [ 'view', 'create', 'update', 'delete', 'restore', 'forceDelete', 'export' ]; protected static string $permissionLabel = 'Hợp đồng'; } ``` Naming: `{snake_case_resource_name}.{action}` (ví dụ: `contracts.create`, `payments.delete`) ### Command đồng bộ (THỦ CÔNG - KHÔNG TỰ ĐỘNG) ```bash php artisan permissions:sync ``` **Khi nào chạy:** - Khi thêm Resource mới - Khi thêm/bớt `$permissionActions` trong Resource - KHÔNG chạy tự động trong composer post-autoload-dump **Command làm gì:** 1. Quét tất cả Filament Resources, lấy `$permissionActions` + `$permissionLabel` 2. Lưu vào bảng `permission_modules` (module, label, actions) 3. Action mới mặc định **TẮT** cho tất cả role hiện tại (an toàn) 4. Admin phải vào bật thủ công sau ### Luồng thêm module/action mới **Step 1: Dev code** - Tạo Resource mới / thêm action vào Resource - Thêm `$permissionActions` vào Resource class **Step 2: Dev chạy command** - `php artisan permissions:sync` **Step 3: Admin cấu hình** - Vào Role Template UI → thấy module/action mới (badge "Mới") - Tick bật quyền cho các nhóm cần dùng ### Các hàm kiểm tra quyền ```php // User Model public function getEffectivePermissions(): array; public function hasEffectivePermission(string $permission): bool; public function clearPermissionCache(): void; // Trong Resource public static function canCreate(): bool { return auth()->user()->can('contracts.create'); } ``` ### UI quản lý (Filament Resources) | Resource | Chức năng | |----------|-----------| | **RoleTemplateResource** | CRUD mẫu nhóm. Form chọn module → tick actions. CheckboxList per module | | **UserPermissionPage** | Chọn user → hiện role template đang dùng → thêm/bớt quyền chi tiết → preview effective permissions | --- *File này cần được cập nhật mỗi khi có thay đổi lớn trong kiến trúc hoặc nghiệp vụ.*