Files
hqland-app/WORKFLOW.md

464 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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** | 🔴 Nghiêm trọng | Tất cả model dùng delete cứng. Xóa nhầm → mất vĩnh viễn |
| 2 | **Phân quyền** | 🟡 Trung bình | Chỉ 1 loại user. Ai cũng xóa/sửa được mọi thứ |
| 3 | **Payment.collected_by** | 🟡 Trung bình | Không ghi nhận ai thu tiền. Khó truy trách nhiệm |
| 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 |
---
*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ụ.*