Files
hqland-app/WORKFLOW.md

26 KiB
Raw Permalink Blame History

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:

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)

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

// 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ụ.