# CLAUDE.md

## Project Overview

Long-running Laravel app (console + HTTP API) for Italy EPR: polls SQL Server OR accepts HTTP API intake, processes data, generates Word documents, uploads to Tencent COS. Authorization letter (授权书) stage is done by external RPA via `python/rpa_get.py` / `python/rpa_save.py`.

## Development Commands

- 常驻进程: `php artisan epr:process-italy`（源轮询）/ `epr:process-api`（API 兜底）
- 本地 API 监听: `php artisan serve --host=0.0.0.0 --port=8010`
- 测试 / 格式化: `vendor/bin/phpunit [--coverage-text]` / `vendor/bin/pint`

## Architecture

Service-repository 模式 + DI。完整数据流/状态机/字段映射/Word 占位符: `docs/architecture.md`；API 线契约+状态机: `docs/api.md`（样例 JSON 在 `docs/api/`）；运维: `docs/runbook.md`。

### Source Flow（轮询模式）
- `EprProcessorService` 编排全管线；`SourceDbRepository` 轮询源库（`PushTaxBureauStatus=5, PushType=301, Country='IT'`；target 库排除子查询只考虑 `data_source='source'` 行）
- 必填校验（除 `F_Mobile`/`LegalSignedFile`）；失败置 `-1` + 企业微信
- 成功状态（`PushTaxBureauStatus=6/7`）由 RPA（`rpa_save.py`）回写，PHP 侧更新代码有意注释掉，勿恢复

### API Intake Mode（`data_source='api'`）
- `POST /api/epr/italy` — Bearer 鉴权（`EPR_API_TOKEN_ENABLED`/`EPR_API_TOKEN`）、统一信封 `{code, msg, ProcessMode:"async", data, bizParam}`
- `EprApiIntakeService`: 校验（聚合 400，格式规则对齐 es_haiya）→ 防重 → 落库 `desc_status=1`（处理中，排除 worker 竞争）+ 业务列（`buildApiColumns`，与源流程同映射，含 doc_day/month/year）
- **说明书在受理请求内同步生成**: `processApiRecord`（转换→翻译→docx→COS）→ `desc_status=2` + `desc_file_url`；失败 `desc_status=3` + 企业微信 + **信封 `code=500` + 分级原因（HTTP 恒 200，不发回调）**；授权书由 RPA 异步生成回写
- **受理路径不发任何回调**: `EPR_RESULT_CALLBACK_URL` 写入行内是**给 RPA 读**的；回调只有两个来源 —— `epr:process-api` 兜底路径与 RPA（均为真异步，无调用方在等）
- `epr:process-api` 仅兜底: 回收历史遗留 `desc_status=0` 与陈旧 `desc_status=1` 行；`EPR_API_DESC_MAX_RETRIES` 只作用于该路径
- API 行不碰源库: 翻译用 `Data.CountryEn` 直传，签名文件 URL 透传；RPA 脚本按 `data_source` 分流，API 行跳过 vat_db/attachment/EPRRegInfo 操作
- **两份文件**: PHP 生成 EPR 注册文件（说明书，API 桶 `EPR_API_COS_BUCKET`，键 `{EPR_API_OSS_PREFIX}{年}/epr_italia/{流水号}/`）+ RPA 生成授权书（同目录）→ RPA 完成后经 `rpa_save.py` 发统一回调
- **API 行库内与回调统一存 OSS 相对路径**（不带域名，落在 `desc_file_url` / `auth_file_url`；source 行仍存完整 URL）—— 换桶/换域名不脏数据，与 es_haiya API 流程一致
- **API 桶是私有桶**: 下载 API 桶对象必须带签名 —— `rpa_get.py` 取数时按 RPA 程序传入的凭证生成 600s 预签名 URL（`sign_api_file_url`，未传凭证则原样兜底；source 流程的 file.usaeu.com 不签名）；签名 URL 含凭证信息，勿写日志/入库
- 回调（对齐 UNIFIED_API_DESIGN §7）: `POST` 行内 `callback_url`（受理时从 `EPR_RESULT_CALLBACK_URL` 写入）；成功 `data={task_id,status,files:[{url,name,type}]}`（`url` 为 OSS 相对路径，`type`=`EPR注册文件`/`授权书`），失败 `data=null` 原因放 `msg`；超时 10s、2xx 即送达、失败重试 3 次/间隔 5 秒（`EPR_RESULT_CALLBACK_*`，**只作用于兜底路径与 RPA**），**重试会产生重复 POST**（接收端幂等：成功按 `bizParam`+`data.task_id`，失败 `data=null` 按 `bizParam`）；投递明细落 `notify_*` 列（列未迁移时静默跳过）
- **重投语义**: desc 失败后调用方重投不会被防重拦（`desc_status=3` 不算在途）；但唯一索引 `UX_italia_epr_file_api_code` 只按 `code` 过滤，重投会**复用同一行**并刷新 `request_data`/`biz_param`/`callback_url` 后置回 `desc_status=1`（`TargetDbRepository::buildRetryResetColumns`）—— 否则 RPA 回调会读出行内首次提交的 `biz_param`
- **三份常量**: `.env` 的 `EPR_API_COS_BUCKET` / `EPR_API_OSS_PREFIX` / `EPR_RESULT_CALLBACK_URL` 必须分别与 `python/rpa_save.py` 的 `API_FLOW_BUCKET_DEFAULT` / `API_FLOW_OSS_PREFIX` / `DEFAULT_CALLBACK_URL` 一致（RPA 进程不读 `.env`）

### 硬约束
- `python/` RPA 脚本保持向后兼容签名: 新参数追加默认值，勿改旧签名
- API 行 schema 变更走 `database/italia_epr_file_api_columns.sql`（幂等）

### Database Connections
- `source` — 读 `EPRBusinessRecord` 待处理记录
- `target` — 写 `italia_epr_file`（`data_source` 为 `'source'` 或 `'api'`）

## Key Field Rules

`vat_number`/`italy_vat_number` 由纯函数 `DataTransformService::resolveRegNumber()` / `resolveItalyVatNumber()` 解析（源流程在 `getNextPendingRecord()`，API 流程在受理时），按 `CountryTwoCode` 分:

| Target field | 中国大陆/香港公司 | 其他国公司 |
|--------------|-------------------|-----------|
| `vat_number` | `RegNumber`（营业执照号，原样） | 优先 `VATNumber`（税号），空则回退 `RegNumber`；不加国家前缀 |
| `italy_vat_number` | `null`（可为空） | `VATNumber` 以字母开头则去前 2 位（`IT12345678901`→`12345678901`）；可为空 |

## Configuration / Logging

- Env vars 见 `.env.example`（`SOURCE_DB_*`/`TARGET_DB_*`/`DEEPL_*`/`TENCENT_COS_*`；API 模式 `EPR_API_TOKEN*`/`EPR_RESULT_CALLBACK_URL`/`EPR_API_*`；`WECHAT_WEBHOOK_URL` 可选覆盖遗留硬编码 webhook）
- 日志: 专用 channel `epr` → `storage/logs/epr-processor.log`，按天轮转保留 30 天
