# 08 — Service nghiệp vụ lõi

> File này mô tả các Service dùng chung/hạ tầng, phục vụ mọi domain khác: xác thực, gửi email, xuất PDF, upload/nhập file. `OrderService` (service lớn nhất, riêng cho domain đơn hàng) đã được mô tả sâu ở file `04-du-lieu-khach-hang-don-hang.md` mục 8 — không lặp lại ở đây.

## 1. Bản đồ các service trong `src/Service/`

Toàn bộ 38 file service tuân theo quy ước chung mô tả ở file 01 (`generateItem`/`list`/`find`/`add`/`update`/`delete`). File này đi sâu vào các service **hạ tầng/dùng chung**, không gắn riêng 1 entity nghiệp vụ:

| Service | Vai trò |
|---|---|
| `ApiService` | Parse request, xác thực token, dashboard tổng hợp, xuất Excel |
| `MailService` | Gửi email giao dịch qua Mailchimp Transactional (Mandrill) |
| `PdfService` | Sinh mọi PDF trong hệ thống (Dompdf) |
| `FileService` | Upload file/ảnh, và **toàn bộ luồng nhập đơn hàng qua (S)FTP** từ các đối tác |
| `GarpMondayService` | Tích hợp ERP "Garp" qua CSV — ⚠️ chưa hoàn thiện |
| `StatusListService` | CRUD cho bảng "enum" dùng chung (xem file 02 mục 4) |
| `ProductionService` | CRUD loại hình sản xuất (xem file 03 mục 3.3) |

---

## 2. ApiService — xác thực & tiện ích request

`src/Service/ApiService.php` — được inject vào **hầu hết mọi controller**, không theo quy ước CRUD entity thông thường.

### 2.1. `getRequestData(Request $request)`

Gộp dữ liệu từ 3 nguồn thành 1 mảng duy nhất: JSON body → POST form → GET query string (nguồn sau **ghi đè** nguồn trước nếu trùng key) — giúp controller đọc tham số mà không cần quan tâm client gửi bằng cách nào.

### 2.2. `getToken()` — cổng xác thực duy nhất của toàn API

Đây là hàm quan trọng nhất hệ thống về mặt bảo mật — **chi tiết đầy đủ được trình bày ở file `12-bao-mat-xac-thuc-cau-hinh.md` mục 1**. Tóm tắt nhanh: kiểm tra header `Bearer <token 32-hex>`, tra `User` theo `accessToken`, kiểm tra hạn dùng + cờ `lockForOnlyIntegration`, trả về token/`User`/`null`.

### 2.3. `dashboard($token, $requestData)`

Tính số liệu cho màn hình tổng quan backoffice: tổng đơn/tổng tiền (loại trừ đơn huỷ), số đơn "complete", tổng kickback (toàn bộ / tháng này / theo loại sản phẩm), biểu đồ doanh số theo tháng (mặc định 12 tháng gần nhất, hoặc theo 1 năm cụ thể), top 5 sản phẩm bán chạy, top 5 hoá đơn kickback gần nhất. Có trừ đi số liệu từ `ReturnOrder`/`ReturnOrderItem` liên quan. Tài khoản `ROLE_CHANNEL` bị **giới hạn chỉ xem đúng kênh của mình** (kiểm tra qua `UserMultiChannel`); nếu gắn `brandId` cố định, số liệu còn bị lọc tiếp theo brand.

### 2.4. Các hàm xuất Excel

Dùng chung 1 pattern: dựng `PhpSpreadsheet` → ghi header + data → lưu `.xlsx` vào `public/export_excel/...` hoặc `public/temp/...` → trả về đường dẫn tương đối cho client tải về. Gồm: `exportExcelOrder`, `exportExcelOrderProduct`, `exportExcelOrderProductKickback`, `exportExcelOrderProductSupplier`, `downPayment`, `exportExcelProducts`.

---

## 3. MailService — gửi email qua Mailchimp Transactional (Mandrill)

`src/Service/MailService.php` — **toàn bộ email giao dịch của hệ thống đi qua đây**, dùng SDK `mailchimp/transactional-php`. **Không dùng Symfony Mailer/SMTP** cho email nghiệp vụ (Symfony Mailer chỉ cấu hình DSN rỗng, xem file 12).

### 3.1. Cơ chế gửi (`sendMail`)

- Lấy API key từ tham số `email_mandrill_key` (`config/services.yaml`) — **đang hard-code giá trị thật trong file cấu hình** (xem cảnh báo ở file 12).
- Gọi API "send new message" của Mailchimp Transactional, gửi kèm file đính kèm dạng base64.
- Hỗ trợ BCC, `Reply-To` tuỳ chỉnh.

### 3.2. Mẫu email — lưu trong database, không phải file `.twig` cố định

Khác với cách làm thông thường, nội dung email **không** nằm trong file template cố định mà lưu trong bảng `EmailTemplate` (entity ở file 02), tra theo khoá ổn định `emailKey` (VD `invoice_mail`, `user_forgot_password`, `channel_user_get_login_otp`, `created_new_ecom_order`, `kickback_invoice_mail`...). Nội dung `content` được **biên dịch như Twig template ngay tại thời điểm gửi** (`$twig->createTemplate($body)->render($params)`) — nghĩa là **admin có thể sửa mẫu email trực tiếp trên UI mà không cần deploy code**, kể cả dùng biến Twig (`{{ name }}`, `{{ orderNr }}`...).

Một số mẫu "hệ thống" (nhắc nợ, kickback, OTP, huỷ đơn) sẽ **tự tạo `EmailTemplate` mặc định** nếu chưa tồn tại trong DB, để tránh vỡ luồng khi cài đặt mới.

### 3.3. Các email nghiệp vụ tiêu biểu

| Hàm | Khi nào gửi |
|---|---|
| `orderPrintOrderbekraftelse` / `orderPrintOffert` / `orderPrintFoljesedel` | Gửi PDF xác nhận đơn / báo giá / phiếu giao cho khách |
| `invoice`, `kickbackInvoice`, `invoiceReminder` | Gửi hoá đơn / hoá đơn hoa hồng / thư nhắc nợ |
| `orderKorrektur` | Gửi file duyệt mẫu (proof) cho khách xác nhận trước khi sản xuất |
| `createNewEcomOrder` | Xác nhận đơn hàng đặt qua webshop |
| `userForgotPassword` / `sendChannelUserLoginOtp` | Đặt lại mật khẩu / gửi mã OTP đăng nhập Channel |
| `shopifyDataRequest` | Phản hồi webhook GDPR "yêu cầu dữ liệu" từ Shopify |
| `prepaidSumPassedWatcherLevel` | Cảnh báo nội bộ khi số dư trả trước của kênh xuống dưới ngưỡng |
| `bulkOrderNotificationEmail` | Thông báo về 1 bulk order và các đơn con |
| `sendCancelConfirmationToCustomer` / `sendCancelAlertToAdmin` | Xác nhận huỷ đơn cho khách / cảnh báo nội bộ |

### 3.4. Nhật ký gửi email

Hầu hết các hàm gửi email nghiệp vụ (trừ `sendMail` thuần và vài hàm thông báo nội bộ) đều ghi 1 bản `ActivityLog` (`ACTION_SEND_MAIL`) lưu người gửi/người nhận/subject/body đã render/mã trạng thái phản hồi từ Mandrill — đây là **dấu vết audit duy nhất** để tra soát email đã gửi (không có bảng log email riêng).

---

## 4. PdfService — sinh PDF (Dompdf)

`src/Service/PdfService.php` — render template Twig → HTML → PDF bằng thư viện Dompdf.

### 4.1. Cơ chế render

`initDomPDF($html, $orientation, $size, $dpi)` khởi tạo Dompdf, set khổ giấy/hướng/DPI, bật `setIsRemoteEnabled(true)` (vì ảnh thường được nhúng dạng base64 qua `imageToBase64()` chứ không tải từ URL).

### 4.2. Danh sách tài liệu được sinh ra

| PDF | Hàm | Ghi chú |
|---|---|---|
| Phiếu sản xuất | `orderPrintProduktionLista` | Kèm ảnh thumbnail, vị trí in, file duyệt mẫu; set `printPackingSlip=true` |
| Xác nhận đơn | `orderPrintOrderbekraftelse` | Set `printOrder=true` |
| Báo giá | `orderPrintOffert` | **Có tác dụng phụ lên trạng thái đơn**: nếu đơn đang "complete", tạm khoá sửa và chuyển trạng thái sang "offert" |
| Phiếu giao hàng | `orderPrintFoljesedel` | Chuyển trạng thái sang "utskriven" (đã in), set `printDeliveryLabel=true` |
| Nhãn vận chuyển | `orderPrintDeliveryLabel` | Khổ tuỳ biến 102×76mm, 203 DPI — đúng chuẩn máy in nhãn nhiệt |
| Phiếu giao gộp | `bulkOrderPrintFoljesedel` | Cho `BulkOrder`, gộp toàn bộ đơn con |
| Hoá đơn | `invoice` | Tự tính hạn thanh toán theo điều khoản (xem file 05); rẽ nhánh riêng nếu hoá đơn tạo từ Credit |
| Thư nhắc nợ | `invoiceReminder` | |
| Lô hoá đơn | `invoiceJournal` | |
| Báo cáo công nợ chưa thu | `invoiceNotPaid1200` | Không gắn với entity file record nào, lưu tên file cố định theo ngày |
| Hoá đơn hoa hồng | `kickbackInvoice` | |
| File/lô đối soát | `downPaymentFile`, `downPaymentFileJournal` | |

Mọi PDF được lưu **trực tiếp bằng `file_put_contents`** vào `public/export_pdf/...` (phục vụ như file tĩnh, không stream qua controller) và ghi 1 bản ghi `*File`/`*Pdf` liên kết với entity cha.

---

## 5. FileService — Upload & nhập file qua (S)FTP

`src/Service/FileService.php` — kiêm 2 vai trò khác biệt: (1) xử lý upload file từ UI, (2) **toàn bộ luồng tải file đơn hàng từ đối tác qua (S)FTP**.

### 5.1. Upload

| Hàm | Giới hạn | Ghi chú |
|---|---|---|
| `uploadImage()` | `.png/.jpg/.gif/.jpeg/.ico`, tối đa 1MB | Lưu vào `public/uploads/{folder}/`; tham số resize ảnh hiện **không hoạt động** (code nén ảnh bị comment) |
| `uploadFile()` | Mọi định dạng, mặc định tối đa ~23MB | |
| `downloadProductImage()` | — | Tải ảnh từ URL ngoài về lưu local |

### 5.2. Nhập đơn hàng từ đối tác qua (S)FTP

Mỗi đối tác có 1 cặp hàm tải + đọc file riêng gần như trùng lặp cấu trúc, khác host/định dạng cột: `importOrderDownloadFile`/`...ReadFileContent` (mặc định), `importOrderNakataDownloadFile` (Nakata), `importOrderBolticDownloadFile` (Boltic), `importOneOrderDownloadFile` (377_sport), `importOrder1239BlavittPOD` (Blavitt — dùng FTP thuần, không phải SFTP). Tất cả cuối cùng gọi vào `OrderService::importCsvData()`/`importOneOrder()`/`importOrder1239BlavittPOD()` để lưu dữ liệu.

⚠️ **Lưu ý bảo mật:** thông tin đăng nhập (S)FTP của các luồng này (host, username, mật khẩu) **hard-code trực tiếp trong file service**, không đi qua `ParameterBagInterface`/`services.yaml` như phần Garp FTP — không nhất quán, nên cân nhắc refactor nếu có yêu cầu bảo mật chặt hơn (xem thêm file 12).

---

## 6. GarpMondayService — tích hợp ERP Garp (⚠️ chưa hoàn thiện)

`src/Service/GarpMondayService.php` — dùng cho lệnh console `garp:monday` (`src/Command/GarpMondayCommand.php`).

**Đính chính tên gọi:** đây là tích hợp với **Garp** (một hệ thống ERP/kế toán Thuỵ Điển), **không phải Monday.com** — "Monday" trong tên class/lệnh nhiều khả năng chỉ ám chỉ tần suất chạy dự kiến (thứ Hai hàng tuần), không phải sản phẩm Monday.com.

### Trạng thái hiện tại: **mã nguồn còn dang dở, không chạy được**

- `step01ListCSVFiles()`: chỉ liệt kê file **đã có sẵn cục bộ** trong `garp_ftp_local_csv_path` (`public/uploads/garp_csv`) — **không tự tải file qua FTP** dù tên gợi ý vậy; có thể một tiến trình khác đồng bộ file vào thư mục này, hoặc bước tải FTP chưa được viết.
- `step02ReadCSVContent()`: đọc xong 1 dòng CSV rồi gọi **`dd($itemData)`** (dump-and-die của Symfony) — **dừng chương trình ngay lập tức**, dòng gọi bước tiếp theo đã bị comment.
- `step03UpdateItemToDatabase()`: chỉ có code rỗng trong khối `try/catch`, chưa có logic cập nhật DB thật.

**Khuyến nghị:** không nên coi `garp:monday` là 1 cronjob đang hoạt động trong tài liệu vận hành — cần hoàn thiện code (bỏ `dd()`, viết bước 3) trước khi đưa vào lịch chạy định kỳ thật.

---

## 7. StatusListService & ProductionService

Cả 2 là CRUD chuẩn cho bảng "enum" dùng chung (`StatusList`, xem file 02 mục 4) và loại hình sản xuất (`Production`, xem file 03 mục 3.3) — không có logic nghiệp vụ đặc biệt ngoài:

- `StatusListService::generateItem()` dùng **PHP Reflection** để tự động dump toàn bộ property thay vì map tay từng field.
- `StatusListService::addCurrencyList()` — hàm seed dữ liệu khởi tạo (idempotent — kiểm tra tồn tại trước khi thêm): tiền tệ mặc định `SEK`/`NOK`/`DK`, và 2 trạng thái `ReturnOrder` mặc định (`New`, `Refunded`).
- `ProductionService::generateUpdate()` — khi đổi `typeId`, tự đồng bộ `typeText` từ `StatusList` tương ứng (giữ đúng pattern "copy tên hiển thị" mô tả ở file 01).

---

## 8. Ví dụ tình huống — vòng đời một email hoá đơn

> 1. Kế toán gọi `POST /invoices/{id}/send-mail`.
> 2. `InvoiceService::sendMail()` gọi `PdfService::invoice()` sinh PDF mới nhất (nếu cần), lưu `InvoiceFile`.
> 3. `MailService::invoice()` tra `EmailTemplate` theo `emailKey = invoice_mail` — nếu admin đã tuỳ biến nội dung trên UI, bản tuỳ biến này được dùng; nếu chưa từng tạo, hệ thống dùng mẫu mặc định tự sinh.
> 4. Nội dung mẫu được render như Twig thật (thay `{{ customerName }}`, `{{ invoiceNr }}`...), đính kèm PDF hoá đơn dạng base64, gửi qua Mandrill.
> 5. Kết quả (thành công/thất bại, mã phản hồi Mandrill) được ghi vào `ActivityLog` — nếu sau này khách báo "không nhận được hoá đơn", nhân viên tra `GET /activity-logs/?entityId={invoiceId}&entity=Invoice` để biết email đã thực sự được gửi đi hay chưa và gửi lúc nào.
