# 後台商家註冊 API（`POST /api/admin/merchant/register`）

超級管理員建立新商家帳號。**金流申請已改為獨立 API**（`POST /api/admin/merchant/cashflow/add`），register 僅接受 `Cashflow: 0`。  
驗證來源：[`MerchantAdminRegisterPost`](../src/Requests/Merchant/Admin/MerchantAdminRegisterPost.php)

---

## 基本資訊

| 項目 | 說明 |
|------|------|
| Method | `POST` |
| Path | `/api/admin/merchant/register` |
| Content-Type | `application/json` |
| 路由定義 | [`src/routes/admin/api.php`](../src/routes/admin/api.php) |
| 控制器 | [`MerchantAdminController::register`](../src/Controllers/Merchant/Admin/MerchantAdminController.php) |
| 服務 | [`MerchantAdminService::register`](../src/Services/Merchant/Admin/MerchantAdminService.php) |

### 中介層

| 中介層 | 說明 |
|--------|------|
| `SuperAdmin` | 需 Bearer Token；`isSuperUser = 1` 且 token 的 `merchant_id` 為空（不可為模擬商家身份） |
| `expired` | 檢查系統／訂閱狀態 |

### 回應格式

JJAJ `ResponseHelper`，結構為：

```json
{
  "code": 200,
  "status": "MERCHANT_ADMIN_REGISTER_SUCCESS",
  "message": "Merchant admin register success",
  "data": { }
}
```

- 成功 `status`：`MERCHANT_ADMIN_REGISTER_SUCCESS`
- `data`：新建 Merchant model 序列化結果（經 `MerchantAdminUserPageResource`）

---

## 請求參數總表

### A. 帳號／負責人（永遠必填）

| 欄位 | 型別 | 驗證 | 說明 |
|------|------|------|------|
| `email` | string | required, email | 商家擁有者 Email；自動轉小寫；金流時亦作 `ManagerEmail` |
| `firstName` | string | required | 負責人名 |
| `lastName` | string | required | 負責人姓 |
| `gender` | enum | required | 見 [gender](#gender) |

### B. 商家基本資料

| 欄位 | 型別 | 驗證 | 說明 |
|------|------|------|------|
| `merchantName` | string | required | 商家中文名稱 |
| `merchantNameEn` | string | required | 商家英文名稱（金流 `MerchantNameE`） |
| `merchantAlias` | string | required | 商家別名（子網域）；自動轉小寫；不可使用[保留字](#merchantalias-保留字) |
| `merchantTimezone` | string | nullable, timezone | 預設 `Asia/Taipei` |
| `merchantDescription` | string | nullable | 商家描述 |
| `merchantFreeMonths` | integer | required, gte:0 | **僅 Request 驗證必填，rulesInput 未使用此欄位** |
| `tax` | object | required | 發票／稅籍資訊 |
| `tax.type` | enum | required | 見 [tax.type](#taxtype) |
| `tax.title` | string | B2B 必填 | 公司名稱 |
| `tax.id` | string | B2B 必填 | 統一編號（admin 端無格式驗證，與前台不同） |

### C. 訂閱方案 `plan`（永遠必填）

| 欄位 | 型別 | 驗證 | 說明 |
|------|------|------|------|
| `plan` | object | required | 訂閱方案設定 |
| `plan.alias` | string | required | 須存在 `plans.alias`（DB 動態值，見 [plan.alias](#planalias)） |
| `plan.period` | enum | required | 見 [plan.period](#planperiod) |
| `plan.overwritePrice` | integer | nullable, gte:0 | 覆寫方案月費 |
| `plan.overwriteSMS` | integer | nullable | 覆寫可用簡訊數 |
| `plan.modules` | object | nullable | 模組選擇 |
| `plan.modules.freeOptional` | string[] | nullable | 免費可選模組，見 [plan.modules](#planmodules) |
| `plan.modules.additional` | string[] | nullable | 加購模組，不可與 `freeOptional` 重複 |
| `plan.components` | object[] | nullable | 加購 component |
| `plan.components.*.alias` | enum | required | 僅允許 `product` |
| `plan.components.*.unit` | integer | required, gt:0 | 加購數量 |

### D. Cashflow（僅允許 0）

| 欄位 | 型別 | 驗證 | 說明 |
|------|------|------|------|
| `Cashflow` | integer | required | **僅允許 `0`**。傳 `1` 時 validation 拒絕（`Cashflow.in` 錯誤訊息） |

> 金流欄位（MemberType、BankCode 等）**不再**於 register 接受。請先完成 register，再呼叫 [`POST /api/admin/merchant/cashflow/add`](../../src/routes/admin/api.php)（需 `hasMerchant` token）。  
> 開發工具已拆成兩個獨立頁面：[`tools/merchant-register/`](../tools/merchant-register/)（register）、[`tools/merchant-cashflow/`](../tools/merchant-cashflow/)（cashflow/add）。入口：[`tools/merchant/`](../tools/merchant/)。

金流欄位定義見 **cashflow/add**（[`MerchantCashflowAdminAddPost`](../src/Requests/Merchant/Admin/MerchantCashflowAdminAddPost.php)）。以下 enum 仍供工具／文件對照：

## Enum 對照表

### 硬編碼 enum（Request 驗證）

<a id="gender"></a>

#### gender

| 值 | 說明 |
|----|------|
| `male` | 男 |
| `female` | 女 |

<a id="taxtype"></a>

#### tax.type

| 值 | 說明 |
|----|------|
| `B2B` | 公司發票；`tax.title`、`tax.id` 必填 |
| `B2C` | 個人／二聯式 |

<a id="planperiod"></a>

#### plan.period

| 值 | 說明 |
|----|------|
| `month` | 月訂 |
| `year` | 年訂 |
| `twoyear` | 兩年訂（見[訂閱到期日](#訂閱到期日-subscription_end_at)注意事項） |

<a id="planmodules"></a>

#### plan.modules.*

| 值 | 模組名稱 |
|----|----------|
| `activity` | 活動 |
| `course` | 課程 |
| `service` | 服務 |
| `reservation` | 餐飲 |
| `guide` | 導覽 |
| `clinic` | 診所 |
| `hotel` | 訂房 |

陣列內不可重複（`distinct`）。

#### plan.components.*.alias

| 值 | 說明 |
|----|------|
| `product` | 分店 component |

#### Cashflow

| 值 | 說明 |
|----|------|
| `0` | 不申請金流 |
| `1` | 申請藍新金流 |

<a id="planalias"></a>

#### plan.alias（動態）

須為資料庫 `plans.alias` 中存在的值，非 hardcode enum。  
常見範例：`business-normal`、`normal`、`advanced`（依環境而異，請先查 DB 確認）。

---

### JSON seed 對照 enum（金流欄位）

請求時傳入 JSON **key**（中文），後端 [`cashflowInfoHandler`](../src/Requests/Merchant/Admin/MerchantAdminRegisterPost.php) 轉換為藍新代碼。

<a id="membertype"></a>

#### MemberType

來源：[`MemberType.json`](../src/database/seeds/cashflow/MemberType.json)

| Key（請求值） | 代碼 |
|---------------|------|
| `個人` | 0 |
| `企業` | 1 |

<a id="idpic"></a>

#### IDPic

來源：[`IDPic.json`](../src/database/seeds/cashflow/IDPic.json)

| Key（請求值） | 代碼 |
|---------------|------|
| `有` | 0 |
| `無` | 1 |

<a id="idfrom"></a>

#### IDFrom

來源：[`IDFrom.json`](../src/database/seeds/cashflow/IDFrom.json)

| Key（請求值） | 代碼 |
|---------------|------|
| `初發` | 1 |
| `補發` | 2 |
| `換發` | 3 |

<a id="merchanttype"></a>

#### MerchantType

來源：[`MerchantType.json`](../src/database/seeds/cashflow/MerchantType.json)

| Key（請求值） | 代碼 |
|---------------|------|
| `實體商品` | 1 |
| `服務` | 2 |
| `虛擬商品` | 3 |
| `票券` | 4 |

<a id="merchantaddrcity"></a>

#### MerchantAddrCity

來源：[`CityE.json`](../src/database/seeds/cashflow/CityE.json)  
請求須使用 **key**（中文縣市名），後端轉換為英文 `CityE`。

| Key（請求值） | 英文 |
|---------------|------|
| `臺北市` | Taipei City |
| `基隆市` | Keelung City |
| `新北市` | New Taipei City |
| `連江縣` | Lienchiang County |
| `宜蘭縣` | Yilan County |
| `釣魚臺` | Diauyutai |
| `新竹市` | Hsinchu City |
| `新竹縣` | Hsinchu County |
| `桃園市` | Taoyuan City |
| `苗栗縣` | Miaoli County |
| `臺中市` | Taichung City |
| `彰化縣` | Changhua County |
| `南投縣` | Nantou County |
| `嘉義市` | Chiayi City |
| `嘉義縣` | Chiayi County |
| `雲林縣` | Yunlin County |
| `臺南市` | Tainan City |
| `高雄市` | Kaohsiung City |
| `南海島` | Nanhai |
| `澎湖縣` | Penghu County |
| `金門縣` | Kinmen County |
| `屏東縣` | Pingtung County |
| `臺東縣` | Taitung County |
| `花蓮縣` | Hualien County |

<a id="businesstype"></a>

#### BusinessType

來源：[`BusinessType.json`](../src/database/seeds/cashflow/BusinessType.json)  
請求須使用 **key**（中文業別名稱），後端轉換為 MCC 代碼。

| Key（請求值） | MCC 代碼 |
|---------------|----------|
| `倉儲服務` | 4225 |
| `旅行社` | 4722 |
| `電話通訊設備及服務` | 4812 |
| `有線電視` | 4899 |
| `3C 商品` | 5045 |
| `寶石/黃金/珠寶貴重物` | 5094 |
| `書報雜誌` | 5192 |
| `園藝用品` | 5261 |
| `一般商品買賣` | 5399 |
| `冷凍食品` | 5422 |
| `西點麵包` | 5462 |
| `食品名特產` | 5499 |
| `服飾配件` | 5699 |
| `電器行` | 5732 |
| `餐廳` | 5812 |
| `運動商品` | 5941 |
| `攝影用品` | 5946 |
| `直銷` | 5963 |
| `化妝/美容保養產品` | 5977 |
| `花店` | 5992 |
| `寵物用品` | 5995 |
| `飯店/民宿` | 7011 |
| `喪葬服務及用品` | 7261 |
| `美容美體服務` | 7298 |
| `廣告服務` | 7311 |
| `網路資訊服務` | 7372 |
| `諮詢服務` | 7392 |
| `休閒交通工具租借` | 7519 |
| `樂區 / 博覽會` | 7996 |
| `娛樂休閒服務` | 7999 |
| `學校` | 8220 |
| `補習/教學服務` | 8299 |
| `社會福利團體` | 8398 |
| `政治團體` | 8651 |
| `宗教團體` | 8661 |
| `其他專業服務` | 8999 |

---

## 業務衍生規則

### 密碼自動產生

後端不接收 `password` 欄位，依條件自動設定初始密碼：

| 條件 | 密碼規則 |
|------|----------|
| `tax.type = B2B` | `{merchantAlias 小寫}{tax.id}` |
| `tax.type = B2C` | `{merchantAlias 小寫}{當日 Ymd}`（依 `merchantTimezone`） |

新帳號 `activation = 1`（已啟用）。

### 訂閱到期日 subscription_end_at

依 `plan.period` 與 `merchantTimezone` 計算，轉換為 app timezone 後寫入：

| plan.period | 計算方式 |
|-------------|----------|
| `month` | 當日 + 1 month，`endOfDay` |
| `year` / `twoyear` | 當日 + 1 year，`endOfDay` |

> **注意**：`twoyear` 在 Request 可傳入，但 `merchantInfoHandler` 目前與 `year` 相同，皆只加 1 年。

### 模組選擇業務規則

建立 MerchantPlan 時，[`MerchantHelper::checkAndGetFreeModules`](../src/Helpers/MerchantHelper.php) 會驗證：

1. `freeOptional` 數量須等於方案的 `freeOptionalModulesCount`
2. 每個 `freeOptional` 須在方案 `freeOptionalModules` 清單內
3. 不可選已含在方案 `freeModules` 的模組
4. `additional` 不可與 `freeOptional` 或 `freeModules` 重複

> **注意**：Admin register 觸發 `MerchantCreatePlanEvent` 時 `isAdmin = false`（預設值），故上述檢查仍會執行，與 `InstallMerchantPlanCommand` 的管理員模式不同。

### 金流申請（改走 cashflow/add）

register **不再**於同一請求建立金流。請於商家建立後呼叫 `POST /api/admin/merchant/cashflow/add`（[`MerchantCashflowAdminAddPost`](../src/Requests/Merchant/Admin/MerchantCashflowAdminAddPost.php)）。`AgreedFee` 未送時預設 `0.028`；`Withdraw`／`WithdrawMer`／`WithdrawSetting`／`AgreedDay` 由 Request 寫入固定值；`ManagerEmail` 取自登入者。`MerchantWebURL` 等欄位須由請求端帶入。

### 建立後自動初始化

在同一 DB transaction 內完成：

1. 建立 owner user（商店擁有者群組）
2. 建立 merchant 記錄
3. 將 owner 與當前 SuperAdmin attach 至 merchant
4. 觸發事件：Tutorial、MemberLevel、MerchantPlan／Modules
5. 建立 ROOT 商品分類
6. 建立預設會員 placeholder（`+886-##`）

> 金流：請另呼叫 `MerchantCashflowAdminService::add`（路由 `cashflow/add`）。

### merchantAlias 保留字

以下 alias 不可使用（[`MerchantService::$api_except_alias`](../src/Services/Merchant/MerchantService.php)）：

```
bindCardFailed, bindCardSuccess, cart, carts, index, login,
merchant, merchants, member, members, order, orders, plan, plans,
prepaid, prepaids, prepaidorder, prepaidorders, product, products,
register, register-form, register-done, subscription, summary,
superadmin, user, users
```

另須通過唯一性檢查（DB 中不可已存在相同 alias）。

---

## 請求範例

### 範例 1：無金流（Cashflow = 0，B2C）

```json
{
  "email": "owner@example.com",
  "firstName": "小明",
  "lastName": "王",
  "gender": "male",
  "merchantName": "好寵物美容",
  "merchantNameEn": "Good Pet Grooming",
  "merchantAlias": "goodpet",
  "merchantTimezone": "Asia/Taipei",
  "merchantDescription": "專業寵物美容服務",
  "merchantFreeMonths": 0,
  "tax": {
    "type": "B2C"
  },
  "plan": {
    "alias": "business-normal",
    "period": "month",
    "modules": {
      "freeOptional": ["activity", "service"],
      "additional": ["guide"]
    },
    "components": []
  },
  "Cashflow": 0
}
```

初始密碼：`goodpet20260702`（alias 小寫 + 當日 Ymd，依 timezone）。

### 範例 2：Cashflow = 1（已停用）

傳 `Cashflow: 1` 將回傳 Laravel validation error（422），`errors.Cashflow` 訊息：

> 註冊不可一併建立金流，請先完成商家註冊後，再呼叫 POST /api/admin/merchant/cashflow/add 申請金流

金流 B2B 企業 payload 範例請改參考 **cashflow/add** 與 [`tools/merchant-cashflow/`](../tools/merchant-cashflow/) 預覽。

<!--
歷史 register 合併範例（Cashflow=1，已停用）略。
-->

---

## 回應範例

### 成功

```json
{
  "code": 200,
  "status": "MERCHANT_ADMIN_REGISTER_SUCCESS",
  "message": "Merchant admin register success",
  "data": {
    "id": "uuid-string",
    "name": "好寵物美容",
    "alias": "goodpet",
    "owner_id": 123,
    "timezone": "Asia/Taipei",
    "tax_type": "B2C",
    "subscription_end_at": "2026-08-02 15:59:59"
  }
}
```

> `data` 欄位為 Merchant model 完整序列化，實際欄位依 model 設定而異。

### 常見錯誤

| status | code | 情境 |
|--------|------|------|
| `INPUT_INVALID` | 422 | Request 驗證失敗（含 `Cashflow: 1`、模組選擇不符等） |
| `USER_UNAUTHORIZED` | 401 | 未登入或 token 無效 |
| `USER_INSUFFICIENT_PERMISSION` | 403 | 非 SuperAdmin，或為模擬商家身份 |
| `MERCHANT_ADMIN_ALIAS_HAS_BEEN_USED` | 403 | alias 已被使用或為保留字 |

驗證失敗時回傳 Laravel 標準 validation error 結構（`errors` 物件）。

---

## 已知實作細節

| 項目 | 說明 |
|------|------|
| `merchantFreeMonths` | Request 必填，但 `rulesInput` 未讀取、未寫入任何欄位 |
| `twoyear` 訂閱 | Request 允許傳入，但 `subscription_end_at` 計算與 `year` 相同（+1 年） |
| 模組檢查 | Admin register 的 `MerchantCreatePlanEvent` 使用 `isAdmin = false` |
| `tax.id` 格式 | Admin 端無 `TaiwanUnifiedBusinessNumber` 驗證（前台 register 有） |
| `BankCode` / `SubBankCode` | 無 enum 驗證，自由字串 |

---

## 相關程式碼

| 檔案 | 說明 |
|------|------|
| [`src/routes/admin/api.php`](../src/routes/admin/api.php) | 路由定義 |
| [`src/Requests/Merchant/Admin/MerchantAdminRegisterPost.php`](../src/Requests/Merchant/Admin/MerchantAdminRegisterPost.php) | 驗證與 rulesInput |
| [`src/Controllers/Merchant/Admin/MerchantAdminController.php`](../src/Controllers/Merchant/Admin/MerchantAdminController.php) | HTTP 入口 |
| [`src/Services/Merchant/Admin/MerchantAdminService.php`](../src/Services/Merchant/Admin/MerchantAdminService.php) | 註冊業務邏輯 |
| [`src/Helpers/MerchantHelper.php`](../src/Helpers/MerchantHelper.php) | 模組／方案資料組裝 |
| [`src/database/seeds/cashflow/`](../src/database/seeds/cashflow/) | 金流 enum JSON |
| [`src/constants/merchant.php`](../src/constants/merchant.php) | 回應 status 定義 |
