# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Start here for any new task

Before starting any non-trivial task (new feature, bug investigation, data migration, question about how something works), **read the relevant file(s) in `/docs/spec/` first** — it's a 12-file Vietnamese spec covering the entire codebase in depth (entity/relationship tables, business-scenario examples, every API route, every cronjob/maintenance script). This file (`CLAUDE.md`) is only a short primer; `/docs/spec/01-tong-quan-kien-truc.md` is the real starting point and links to the other 11 files by topic (data domains, API bundle, webshop bundle, core services, integrations, cronjobs, maintenance scripts, security/config).

**In particular, always check `/docs/spec/11-script-bao-tri-mot-lan.md` before running or suggesting anything in `src/Command/Update/`** — three of those commands destroy data irreversibly (`remove:order`, `remove_soft_delete:order`, `update:product-order-production`) despite unremarkable-looking names.

## Commands

```bash
php bin/console                                  # list all commands
php bin/console doctrine:migrations:generate     # create an empty migration to write by hand (do NOT use migrations:diff, see below)
php bin/console doctrine:migrations:status       # show executed / pending migrations
php bin/console doctrine:migrations:migrate      # run pending migrations
php bin/console cache:clear                      # clear Symfony cache
php bin/console cronjob:<name>                   # run a cronjob manually
```

## Architecture

**Symfony 7.1, PHP 8.2+, PostgreSQL**

The system is a B2B order management platform (TshirtOrder) with two bundles:
- `ApplicationApiBundle` — REST API consumed by the React frontend (`/api/v1/`)
- `ApplicationWebshopBundle` — server-side rendered webshop (`/ecom/{channelEcomId}/`)

### Request flow (API)

```
Frontend → POST /api/v1/channels/ (Bearer token)
→ ChannelController::add()
→ ChannelService::add($data)
→ EntityManager persist/flush
```

Auth is handled in every controller by calling `ApiService::getToken($authorization)`. Returns `null` on failure → controller returns 401.

Services always return `['data' => ..., 'status_code' => int]`.

### Directory structure

| Path | Purpose |
|------|---------|
| `src/Entity/` | Doctrine ORM entities → PostgreSQL. IDs use `SEQUENCE` strategy. |
| `src/Repository/` | Custom query methods via `->query([...])` pattern |
| `src/Service/` | All business logic. Each entity has a matching Service. |
| `src/Application/ApiBundle/Controller/` | Thin controllers — delegate everything to Services |
| `src/Application/ApiBundle/Resources/config/route/` | One YAML file per resource, included in `routing.yaml` |
| `src/Command/Cronjob/` | Scheduled jobs (`#[AsCommand(name: 'cronjob:...')]`) |
| `src/Command/Update/` | One-off data update scripts — ⚠️ some are destructive, see `docs/spec/11-script-bao-tri-mot-lan.md` before running any of them |
| `migrations/` | Doctrine migrations — **written by hand**, see [Migrations](#migrations) |

### Key entities

- **Channel** (`src/Entity/Channel.php`) — central config entity. Each channel has its own settings for invoicing, ecom, Shopify, prepaid, Svea, etc. All product/order data belongs to a channel.
- **Product** (`src/Entity/Product.php`) — has `sku`, `idWp` (WooCommerce ID), `stock` fields. Belongs to many channels.
- **Order / OrderProduct** — order headers and line items.

### Service conventions

Each service has:
- `generateItem(Entity $entity): array` — maps entity to the array format returned to frontend
- `list($data)`, `find($id)`, `add($data)`, `update($id, $data)`, `delete($id)` — standard CRUD

### Cronjob pattern

Cronjobs are Symfony Console Commands in `src/Command/Cronjob/`. They query channels with specific flags enabled, then call a service method per channel. Logging goes to a flat file (e.g. `shopify_logs/clone_product_stock_{date}.txt`). See `ShopifyCloneProductStockCommand` as the reference implementation.

### Adding new Channel settings

1. Add column(s) to `src/Entity/Channel.php` with getter/setter
2. Write the migration by hand (see [Migrations](#migrations)) → `migrate`
3. Add the field to `ChannelService::generateItem()` (read) and `ChannelService::update()` (write)

### Migrations

**Never use `doctrine:migrations:diff` (and never run a migration it generated).** The DB schema has drifted from the entity mapping, so the diff always pulls in unrelated, destructive SQL:
- `DROP TABLE activity_log_yYYYYmMM` for every monthly partition of `activity_log` (partitioned tables managed by the DB, which Doctrine does not understand) → **log data loss**
- `DROP INDEX` for indexes that were added by hand in earlier migrations (e.g. `idx_op_tryck_parent_order_product_id`, `idx_ptc_*`)

Instead:
1. Update the entity mapping (columns, `#[ORM\Index]`, ...) so it stays in sync with the DB
2. `php bin/console doctrine:migrations:generate` → write only the SQL for your change in `up()` and its reverse in `down()`
3. Fill in `getDescription()` and a docblock with the ticket number + "Written by hand, not via doctrine:migrations:diff (see Version20260924090615 for why)."
4. `doctrine:migrations:migrate`

Reference examples: `Version20260924090615` (add column), `Version20260925130000` (column + index), `Version20260925065840` (index only).

### Integrations

| Integration | Where |
|------------|-------|
| WooCommerce / WordPress | `synId` on Channel, `idWp` on Product; existing stock sync in `ShopifyCloneProductStockCommand` |
| Shopify | `src/Service/ShopifyService.php`, Channel fields `shopifyApiKey/Url/AccessToken` |
| FTP | `phpseclib/phpseclib` library; FTP params in `config/services.yaml` under `garp_ftp_*` |
| PostNord | `src/Service/PostNordService.php` |
| Svea (checkout) | `src/Service/Payment/SveaService.php`, Channel fields `svea*` |
| Swish | `src/Service/Payment/SwishService.php` |
| Mailchimp Transactional | `src/Service/MailService.php` |

### FTP params (services.yaml)

```yaml
garp_ftp_host: 195.67.131.74
garp_ftp_username: garpout
garp_ftp_password: ...
garp_ftp_local_csv_path: public/uploads/garp_csv
```

For WOO stock export FTP, new params will be added here and injected via `ParameterBagInterface`.

## Issue docs

Whenever asked to create an issue doc (e.g. "tạo doc issue", "TSHIRTORDER-XXXX ... tạo file doc"), always follow `docs/issues/_TEMPLATE.md` — read it first, then follow its structure and its own instructions on flat file vs. folder-with-`files/` layout (folder when mockups/images are involved). Before writing the TODO list, check whether the relevant entity/service/migration already exists in the codebase — several past tickets turned out to be already implemented on the backend.