# TSHIRTORDER-1648 — LIVE: Invoice missing in kundreskontra export

## Yêu cầu gốc

> TSHIRTORDER-1648 LIVE - Invoice missing in kundreskontra export
> we see that some invoice is misisng in kundreskontra lkista and we think the issue is that cuustomer make settings wrong , they set invoice period it as both , can you see if you can make the changes in Database

Kèm ảnh mockup (xem `files/channel-invoice-settings.png`, `files/reskontralista-export.png`).

Danh sách invoice number bị thiếu (do user cung cấp, chưa có xác nhận trực tiếp từ khách):

```
302205
302204
302408
302348
302347
```

---

## Tổng quan

Export **Reskontralista** (kundreskontra = sổ công nợ phải thu: danh sách hoá đơn chưa thanh toán đủ tại 1 ngày chốt) bị thiếu một số hoá đơn. Thuộc phần API/Invoice, xử lý bằng **sửa dữ liệu DB** (theo yêu cầu ticket), không đổi code.

### Nguyên nhân (đã xác nhận trong code + DB local)

1. Channel có 2 checkbox loại hoá đơn (màn "Fakturainställningar"): "Att fakturera periodvis" = `invoiceOncePerMonth`, "Att fakturera löpande" = `invoiceIndividual`. Hai cờ này lẽ ra loại trừ nhau nhưng UI/`ChannelService::update()` không chặn tick cả hai.
2. Export Reskontralista (`InvoiceService::exportPdfNotPaid1200()` → `InvoiceRepository` filter `export_not_paid_date`, `InvoiceRepository.php:214-221`) **chỉ lấy hoá đơn `createdFrom` = `individual` hoặc `credit`**. Hoá đơn `monthly` (tạo từ `addFromMultiOrders`) và `bulk_order` bị loại.
3. Channel tick cả hai cờ → một số hoá đơn được tạo theo kiểu gộp tháng (`monthly`) → không xuất hiện trong kundreskontra.

> ⚠️ **Phát hiện quan trọng:** sửa cờ trên channel **chỉ ảnh hưởng hoá đơn tạo từ sau đó**. Hoá đơn `monthly` đã tạo vẫn bị thiếu trong export, phải sửa riêng `invoices.created_from`.

### Kết quả tra trên DB local (đối chiếu lại trên LIVE trước khi chạy)

Channel đang tick cả hai cờ (`COALESCE(invoice_once_per_month, true) = true AND invoice_individual = true`; lưu ý `Channel::isInvoiceOncePerMonth()` coi `NULL` là `true`):

| channel id | name | monthly_inv | individual_inv | last_invoice |
|---|---|---|---|---|
| 14 | AlltidMff | 4 | 72 | 2026-09-16 |
| 88 | NKT3 Sport | 5 | 20 | 2026-10-07 |
| 103 | Individuell Människohjälp | 0 | 2 | 2026-10-07 |

5 hoá đơn của ticket (tất cả `created_from = 'monthly'`):

| invoice_nr | invoice id | channel | cờ channel (once/indiv) |
|---|---|---|---|
| 302204 | 2229 | 88 NKT3 Sport | 1 / 1 |
| 302205 | 2231 | 88 NKT3 Sport | 1 / 1 |
| 302347 | 2378 | 88 NKT3 Sport | 1 / 1 |
| 302348 | 2379 | 88 NKT3 Sport | 1 / 1 |
| 302408 | 2439 | 23 IK Sävehof Webbshop / tshirt.se | 1 / (không set) |

> ⚠️ 302408 thuộc channel 23 chỉ có periodvis (không bị tick cả hai) → **không do lỗi cấu hình**. Hoá đơn monthly của channel như vậy luôn bị loại khỏi export; cần xác nhận với PO đó có chủ ý không.

---

## Luồng hoạt động / Chi tiết theo mockup

- `files/channel-invoice-settings.png`: màn cấu hình Channel, mục **Fakturainställningar** với 2 checkbox "Att fakturera periodvis" / "Att fakturera löpande" (mockup đang chỉ vào việc 2 checkbox này không được cùng bật).
- `files/reskontralista-export.png`: màn danh sách hoá đơn, ô **Reskontralista** (chọn ngày) + nút **Exportera** → gọi `exportPdfNotPaid1200` → `Reskontralista_<ngày>.pdf`.

---

## Questions / Pending Clarification

| # | Câu hỏi | Trả lời | Ghi chú kỹ thuật |
|---|---------|---------|-------------------|
| 1 | 3 channel (14, 88, 103) giữ cờ nào? | *Chưa xác nhận.* Đề xuất giữ **löpande** (invoice_individual) vì phần lớn hoá đơn là `individual` | Cần xác nhận với support/khách, nhất là 14 và 88 (từng dùng cả hai kiểu) |
| 2 | 5 hoá đơn đã tạo: đổi `created_from` thành `individual`, hay nới filter export để lấy cả `monthly`? | *Chưa quyết định.* Ticket yêu cầu sửa DB → phương án đổi `created_from` | `created_from = 'monthly'` còn dùng ở `InvoiceRepository.php:47,72` và `InvoiceReminderRepository.php:38` → đổi có thể đổi luồng nhắc nợ/danh sách hoá đơn monthly. **Chưa kiểm tra kỹ** |
| 3 | Có thêm validation chặn tick cả hai cờ (BE `ChannelService::update()` + UI)? | **Đã quyết định làm phần BE** (callback trong `Channel::validate()`); phần UI (radio/bỏ tick cái kia) thuộc repo FE | `add()`/`update()` đều gọi `validateData()` → trả 400, `propertyPath = invoiceIndividual`. ⚠️ Phải chạy SQL sửa data (mục 3) **trước khi deploy**, nếu không channel 14/88/103 (và channel LIVE khác đang tick cả hai) sẽ bị 400 ở mọi lần update. Chưa kiểm tra trường hợp cả hai cờ `false` (`InvoiceService` xử lý ra sao) nên rule hiện cho phép |

---

## Thiết kế kỹ thuật (SQL sửa DB)

Chạy theo thứ tự, trong transaction, kiểm tra số dòng trước khi `COMMIT`. **Chạy lại SELECT trên LIVE trước** vì danh sách channel có thể khác local.

### 1. SELECT liệt kê channel bị "cả hai"

```sql
SELECT c.id, c.name, c.invoice_once_per_month, c.invoice_individual,
       COUNT(i.id) FILTER (WHERE i.created_from = 'monthly')    AS monthly_invoices,
       COUNT(i.id) FILTER (WHERE i.created_from = 'individual') AS individual_invoices,
       MAX(i.date_invoice)                                      AS last_invoice
FROM channels c
LEFT JOIN invoices i ON i.channel_id = c.id
WHERE COALESCE(c.invoice_once_per_month, true) = true
  AND c.invoice_individual = true
GROUP BY c.id, c.name, c.invoice_once_per_month, c.invoice_individual
ORDER BY c.id;
```

### 2. Backup

```sql
CREATE TABLE channels_backup_tshirtorder_1648 AS
SELECT id, invoice_once_per_month, invoice_individual
FROM channels
WHERE COALESCE(invoice_once_per_month, true) = true
  AND invoice_individual = true;

CREATE TABLE invoices_backup_tshirtorder_1648 AS
SELECT id, invoice_nr, created_from FROM invoices
WHERE invoice_nr::text IN ('302205','302204','302408','302348','302347');
```

### 3. UPDATE channel (giữ löpande cho các channel đã chốt — điều chỉnh danh sách id theo kết quả xác nhận)

```sql
UPDATE channels
SET invoice_once_per_month = false
WHERE id IN (14, 88, 103)
  AND invoice_individual = true;
```

Muốn giữ periodvis cho channel nào thì thay bằng:

```sql
UPDATE channels
SET invoice_individual = false, invoice_once_per_month = true
WHERE id IN (/* id */);
```

### 4. UPDATE 5 hoá đơn đã tạo (kỳ vọng `UPDATE 5`)

```sql
UPDATE invoices
SET created_from = 'individual'
WHERE invoice_nr::text IN ('302205','302204','302408','302348','302347')
  AND created_from = 'monthly';
```

### 5. Kiểm tra lại

```sql
-- kỳ vọng 0 dòng
SELECT id, name FROM channels
WHERE COALESCE(invoice_once_per_month, true) = true AND invoice_individual = true;
```

Sau đó export lại Reskontralista (ngày ≥ ngày hoá đơn) và kiểm tra 5 hoá đơn đã hiện (nếu còn nợ — export chỉ liệt kê hoá đơn chưa trả đủ).

### Rollback

```sql
UPDATE channels c
SET invoice_once_per_month = b.invoice_once_per_month,
    invoice_individual     = b.invoice_individual
FROM channels_backup_tshirtorder_1648 b WHERE c.id = b.id;

UPDATE invoices i SET created_from = b.created_from
FROM invoices_backup_tshirtorder_1648 b WHERE i.id = b.id;
```

#### ⚠️ Lưu ý bẫy quan trọng

- Đổi `created_from` có thể đổi luồng nhắc nợ (`InvoiceReminderRepository`) và danh sách hoá đơn monthly (`InvoiceRepository.php:47,72`) — nên thử trên local, đối chiếu với hoá đơn đã gửi khách/invoice summary.
- Chỉ dùng `WHERE id IN (...)` cụ thể, không update hàng loạt.
- Export chỉ lấy hoá đơn còn nợ (> 0 sau khi trừ `DownPaymentRow`), nên hoá đơn đã thanh toán đủ sẽ không hiện dù đã sửa.

---

## TODO List

### Dữ liệu (DB)
- [ ] Chạy SELECT (mục 1) trên LIVE, đối chiếu với danh sách local (14, 88, 103)
- [ ] Xác nhận với support/PO: giữ cờ nào cho từng channel (Q1)
- [ ] Xác nhận 5 invoice number đúng là danh sách khách báo thiếu
- [ ] Backup 2 bảng (mục 2)
- [ ] UPDATE `channels` (mục 3)
- [ ] UPDATE 5 `invoices` (mục 4), sau khi kiểm tra tác động lên reminder/monthly list (Q2)
- [ ] Chạy kiểm tra lại (mục 5)

### Test / kiểm tra
- [ ] Export Reskontralista trên LIVE, xác nhận 5 hoá đơn hiện (nếu còn nợ)
- [ ] Kiểm tra màn hoá đơn monthly và luồng Faktura påminnelser của channel 88 và 23 không bị đổi bất thường

### Code (BE)
- [x] Thêm rule vào `Channel::validate()` (`src/Entity/Channel.php`): chặn `isInvoiceOncePerMonth() && isInvoiceIndividual()`, lỗi gắn vào `invoiceIndividual` (Q3)
- [ ] Test: PUT channel với cả hai cờ = true → 400; chỉ một cờ → 200; channel có `invoice_once_per_month = NULL` + `invoice_individual = true` → 400
- [ ] Deploy code **sau** khi đã chạy SQL sửa data trên LIVE (mục 3)
- [ ] Xác nhận hành vi khi cả hai cờ `false` trong `InvoiceService` (có cần bắt buộc chọn một không)

### Yêu cầu cho FE
Đổi 2 checkbox trong "Fakturainställningar" thành **radio**, bắt buộc chọn 1 trong 2. Khi POST/PUT channel, luôn gửi **cả hai field**, giá trị bù nhau:

| Radio chọn | `invoiceOncePerMonth` | `invoiceIndividual` |
|---|---|---|
| Att fakturera periodvis | `true` | `false` |
| Att fakturera löpande | `false` | `true` |

- Tên field giữ nguyên, không thêm field mới.
- Không chỉ gửi một field: BE trả 400 nếu cả hai cùng `true` (channel mới mặc định `invoiceOncePerMonth = true`).
- Khi load channel: `invoiceIndividual === true` thì chọn löpande, còn lại chọn periodvis.
- BE hiện chưa chặn cả hai `false` (khi đó đơn không hiện ở màn tạo hoá đơn nào) — FE đảm bảo bằng radio bắt buộc.

### Không làm trong ticket này
- Phần code FE — thuộc repo FE, theo yêu cầu ở mục trên

---

## Các file/files liên quan

| File | Mục đích |
|------|----------|
| `src/Service/InvoiceService.php` (`exportPdfNotPaid1200`, `addFromMultiOrders`, `addFromMultiOrdersIndividual`) | Export Reskontralista; tạo hoá đơn monthly/individual |
| `src/Repository/InvoiceRepository.php` (`export_not_paid_date`, dòng 214-221; dòng 47, 72) | Filter chỉ lấy `individual`/`credit`; chỗ khác dùng `monthly` |
| `src/Repository/InvoiceReminderRepository.php` (dòng 38) | Dùng `monthly` cho nhắc nợ |
| `src/Service/PdfService.php` (`invoiceNotPaid1200`) | Sinh PDF `Reskontralista_<ngày>.pdf` |
| `src/Entity/Invoice.php` | Hằng số `CREATED_FROM_*` |
| `src/Entity/Channel.php` (`invoiceOncePerMonth`, `invoiceIndividual`, `validate()`) | Hai cờ loại hoá đơn; callback chặn tick cả hai |
| `src/Service/ChannelService.php` | `generateItem()` / `update()` cho 2 cờ |
| `migrations/Version20250515082136.php` | Thêm cột `invoice_once_per_month`, `invoice_individual` |
