# PCE AP Write API — 介接手冊（外部廠商）

> **對象**：與 NetSuite 介接的外部系統／廠商開發人員  
> **目的**：說明如何透過 RESTlet 寫入台灣進項電子發票資料與相關 AP 交易  
> **相關**：[技術與測試文件](./PCE_GUI_MODULE_ap-write-api-technical.md) · [English](./PCE_GUI_MODULE_ap-write-api-integration-guide.en.md) · [Postman](./postman/PCE-AP-Write.postman_collection.json)

---

## 1. 快速開始

1. 向客戶 NetSuite 管理員取得：
   - Account ID（如 `TD1234567`）
   - RESTlet **External URL**
   - OAuth 2.0 Integration：**Client ID**、（M2M 時）Certificate ID／私鑰，或 Authorization Code 用的 Client Secret／Redirect URI
2. 先取得 **OAuth 2.0 access token**（Bearer）。
3. 以 **POST** + `options.validateOnly: true` 試跑連線與驗證（**不支援 GET**）。
4. 再以 **POST** + JSON 依業務情境選擇 `action` 寫入。

### 1.1 連線方式

| 項目 | 說明 |
|------|------|
| 協定 | HTTPS |
| 驗證 | **OAuth 2.0**（`Authorization: Bearer {access_token}`） |
| Content-Type | `application/json`（寫入）；取 token 時為 `application/x-www-form-urlencoded` |
| 方法 | **僅 POST**（GET 回 `PCE_METHOD_NOT_ALLOWED`） |

> **不使用** OAuth 1.0a／TBA 逐筆簽名。

External URL 範例（數字 id 以客戶環境為準）：

```text
https://{accountId}.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=xxx&deploy=yyy
```

Token endpoint：

```text
https://{accountId}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token
```

> 請勿將 Client Secret／私鑰／access token 寫死在公開程式庫。

### 1.2 OAuth 2.0 取得 Token（廠商建議：M2M）

**Client Credentials（Machine-to-Machine）** — 適用系統對系統、無使用者登入：

1. Integration 勾選 **Client Credentials (Machine to Machine)**，Scope 含 **RESTlets**。
2. 產生憑證（例 ES256），於 **Setup → Integration → OAuth 2.0 Client Credentials (M2M) Setup** 上傳公鑰並綁定 Entity／Role／Application，記下 **Certificate ID**。
3. 以私鑰簽署 JWT（`iss`=Client ID，`aud`=token URL，`scope`=`restlets`，header `kid`=Certificate ID）。
4. POST token URL：

```text
grant_type=client_credentials
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion={signed_jwt}
```

5. 回應之 `access_token` 放入後續 RESTlet 請求標頭：`Authorization: Bearer {access_token}`。

輔助腳本：[`docs/postman/generate-client-assertion.js`](./postman/generate-client-assertion.js)。

**Authorization Code Grant** — 適用需使用者登入的互動測試／應用；取得 token 後同樣使用 Bearer。

**Postman**：匯入 [`docs/postman/PCE-AP-Write.postman_collection.json`](./postman/PCE-AP-Write.postman_collection.json) → 先跑 **0. Auth** 取 token → 再打 RESTlet（Collection Auth = Bearer `{{accessToken}}`）。

---

## 2. 業務情境（action）

每次 POST **必須**帶 `action`，且只能擇一：

| action | 用途 | 必填 | 不可帶 |
|--------|------|------|--------|
| `apInvoiceOnly` | 只建立進項發票（可不掛交易） | `apInvoices`（至少 1 筆） | 用來「新建」的 `transaction`（type／fields／行） |
| `transactionOnly` | 只建立或引用交易單 | `transaction`（物件或陣列；每筆需有 `id` **或** `type`，最多 10 筆） | `apInvoices` |
| `transactionWithApInvoices` | 交易＋一至多筆進項（1:N） | 每筆 `transaction`（物件或陣列，最多 10 筆）內嵌非空 `apInvoices[]` | top-level `apInvoices`／`invoices`／`apInvoicesByTransaction` |

### 2.1 支援的交易類型

建立新交易時，`transaction.type` 限下列字串（大小寫不敏感）：

| type | NetSuite 交易 |
|------|----------------|
| `vendorbill` | Vendor Bill |
| `expensereport` | Expense Report |
| `vendorcredit` | Vendor Credit |
| `vendorprepayment` | Vendor Prepayment |

引用既有交易：只傳 `transaction.id`（內部 ID）即可；可省略 `type`／`fields`。

---

## 3. GET — 不允許

本 API **不支援 GET**。對 External URL 發 GET 時，應收到：

```json
{
  "success": false,
  "returnCode": "1014",
  "returnMessage": "PCE_METHOD_NOT_ALLOWED",
  "error": {
    "code": "PCE_METHOD_NOT_ALLOWED",
    "message": "GET is not allowed on PCE AP Write. Use POST with action=apInvoiceOnly|transactionOnly|transactionWithApInvoices.",
    "details": null
  }
}
```

契約與範例請見本文件與 Postman collection；連線試跑請用 `options.validateOnly: true` 的 POST。

---

## 4. POST — 寫入

### 4.1 共用結構

```json
{
  "requestId": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "action": "apInvoiceOnly | transactionOnly | transactionWithApInvoices",
  "apInvoices": [ { } ],
  "transaction": { },
  "options": {
    "validateOnly": false
  }
}
```

| 欄位 | 必填 | 說明 |
|------|------|------|
| `action` | **必填** | 三種之一：`apInvoiceOnly`／`transactionOnly`／`transactionWithApInvoices`（見 §2） |
| `requestId` | **必填** | 冪等鍵。建議 UUID v4；8–64 字元，僅 `A–Z` `a–z` `0–9` `-` `_`。每一次邏輯寫入用新 key；網路重試必須重用同一把。 |
| `apInvoices` | 依 action | **僅** `apInvoiceOnly` 必填（至少 1 筆）；亦接受別名 `invoices`。其他 action 不可帶 top-level |
| `transaction` | 依 action | `transactionOnly`／`transactionWithApInvoices` 必填（物件或陣列，最多 10 筆）。後者每筆須內嵌非空 `apInvoices[]` |
| `options.validateOnly` | 選填 | `true` 時只驗證進項（及 shape），**不寫入**資料（見各 action 差異）。失敗或 `validateOnly` 回應會附 `requestSummary`（精簡：index／clientRef／invoiceNo／type／id） |
| `options.echoRequest` | 選填 | 預設不回傳。`true` 時在任何回應附上完整 `request`（除錯用；正式環境建議關閉） |
| `clientRef` | 選填 | 放在每個 `transaction` 與／或每張 `apInvoices[]` 項目上（字串 1–64），成功回應會原樣回傳以便對帳 |

> **冪等**：若相同 `requestId` 先前已成功寫入（非 `validateOnly`），再次呼叫會回傳上次結果並帶 `idempotentReplay: true`，不會再建資料。同 key 但 payload 不同 → `REQUEST_ID_PAYLOAD_MISMATCH`。

> **破壞性契約**：`transactionWithApInvoices` 的進項必須放在 `transaction.apInvoices`（或多筆時每筆各自的 `apInvoices`）。不再接受 top-level `apInvoices`／`apInvoicesByTransaction`。
### 4.2 成功／失敗回應格式

每次回應（含 GET 被拒）頂層皆含 **`returnCode`**（4 位字串）與 **`returnMessage`**（固定英文短句），供外部系統程式判斷；`success` 與 `error.code` 仍保留。

**成功碼（`000x`）**

| returnCode | 條件 | returnMessage |
|------------|------|---------------|
| `0000` | 一般寫入成功（全部項目成功） | `OK` |
| `0001` | `validateOnly: true` 且全部通過 | `VALIDATED` |
| `0002` | `idempotentReplay: true`（冪等重播，先前全成功） | `IDEMPOTENT_REPLAY` |
| `0003` | 全部成功但有 `warnings`（稅額容差等） | `OK_WITH_WARNINGS` |
| `0004` | **部分成功**：至少一筆已寫入／通過，且至少一筆失敗 | `PARTIAL_SUCCESS` |

> **Partial success**：`success` 為 `false`，`partialSuccess` 為 `true`，`returnCode` 為 `0004`，但仍回完整 `results[]`（成功項有 `id`／`transactionId`，失敗項有 `error`）。同一 `requestId` 重送相同 payload 會冪等回傳上次部分結果，不會重複建單；補送失敗項請用**新 requestId** 且只帶未成功資料。

成功（以 `results[]` 對應 request；每筆含 `returnCode`；可選 `clientRef` 原樣回傳）：

```json
{
  "success": true,
  "returnCode": "0000",
  "returnMessage": "OK",
  "action": "apInvoiceOnly",
  "requestId": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "results": [
    {
      "index": 0,
      "returnCode": "0000",
      "returnMessage": "OK",
      "id": "1001",
      "invoiceNo": "CA12345678",
      "clientRef": "inv-001"
    }
  ]
}
```

部分成功（`transactionWithApInvoices` 多筆發票）：

```json
{
  "success": false,
  "partialSuccess": true,
  "returnCode": "0004",
  "returnMessage": "PARTIAL_SUCCESS",
  "action": "transactionWithApInvoices",
  "requestId": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "error": {
    "code": "PARTIAL_SUCCESS",
    "message": "Partial success: 1 succeeded, 1 failed. Succeeded: [transaction[0].apInvoices[0] clientRef=… invoiceNo=CA12345678 id=1001 returnCode=0000]. Failed: [transaction[0].apInvoices[1] clientRef=… invoiceNo=AA99999999 returnCode=2001 error=PCE_AP_INVOICE_VALIDATION_ERROR].",
    "details": {
      "succeeded": 1,
      "failed": 1,
      "succeededItems": [{ "path": "transaction[0].apInvoices[0]", "clientRef": "…", "invoiceNo": "CA12345678", "id": "1001", "returnCode": "0000" }],
      "failedItems": [{ "path": "transaction[0].apInvoices[1]", "clientRef": "…", "invoiceNo": "AA99999999", "returnCode": "2001", "errorCode": "PCE_AP_INVOICE_VALIDATION_ERROR" }]
    }
  },
  "results": [
    {
      "index": 0,
      "returnCode": "0004",
      "returnMessage": "PARTIAL_SUCCESS",
      "transactionId": "789",
      "transactionType": "vendorbill",
      "transactionCreated": true,
      "apInvoices": [
        {
          "index": 0,
          "returnCode": "0000",
          "returnMessage": "OK",
          "id": "1001",
          "invoiceNo": "CA12345678"
        },
        {
          "index": 1,
          "returnCode": "2001",
          "returnMessage": "AP_INVOICE_VALIDATION_FAILED",
          "id": null,
          "invoiceNo": "AA99999999",
          "error": {
            "code": "PCE_AP_INVOICE_VALIDATION_ERROR",
            "message": "…",
            "details": {}
          }
        }
      ]
    }
  ],
  "requestSummary": { }
}
```

失敗（單筆驗證失敗仍回 `results[]`；多筆全失敗頂層常為 `2100`）：

```json
{
  "success": false,
  "returnCode": "2001",
  "returnMessage": "AP_INVOICE_VALIDATION_FAILED",
  "error": {
    "code": "PCE_AP_INVOICE_VALIDATION_ERROR",
    "message": "人類可讀說明",
    "details": null
  },
  "results": [
    {
      "index": 0,
      "returnCode": "2001",
      "returnMessage": "AP_INVOICE_VALIDATION_FAILED",
      "id": null,
      "invoiceNo": "CA12345678",
      "error": {
        "code": "PCE_AP_INVOICE_VALIDATION_ERROR",
        "message": "人類可讀說明",
        "details": null
      }
    }
  ]
}
```

> **HTTP 與 returnCode**：NetSuite RESTlet 業務失敗時 HTTP 仍常為 200；請以 body 的 `returnCode`／`success` 為準。OAuth 401／403 屬 token／角色問題，body 可能無 JSON。

進項驗證失敗時，`error.code` 常為 `PCE_AP_INVOICE_VALIDATION_ERROR`，`details` 為依欄位分組的錯誤明細。

`warnings`：仍判定成功，但有提示（例如稅額與 5% 計算差在容許範圍內）。請記錄並視需要人工覆核。

---

## 5. 進項發票欄位（apInvoices[]）

建議使用 **友善別名**（camelCase）。亦可直接傳 NetSuite field id（`custrecord_pce_ap_*`）。

### 5.1 建議別名對照

| 建議別名 | 中文 | 必填 | 備註 |
|----------|------|------|------|
| `docFormatCode` | 憑證格式代碼 | **必填** | 如 `"21"`～`"29"`；亦可用 `docType`／`formatCode` |
| `invCatCode` | 發票類別代號 | 條件必填 | 格式 **21**：`01`／`05`；格式 **25**：`06`／`07`；亦可用 `invoiceCategoryCode` |
| `docFormatName` | 憑證格式名稱 | 選填 | 完整 Name（如 `21-三聯式統一發票`）；可取代 `invCatCode` 消歧 |
| `docDate` | 憑證開立日期 | **必填** | `YYYY/MM/DD` 或 `YYYY-MM-DD`，如 `2026/08/01` |
| `docPeriod` | 資料所屬年月 | **必填** | 民國五碼 `YYYMM`，如 `11508`（可自 `docDate` 推算） |
| `filingPeriod` | 申報期別 | **必填** | 民國五碼 `YYYMM` |
| `deductionCode` | 進項扣抵代號 | **必填** | `1` 進項可扣抵之進貨及費用／`2` 進項可扣抵之固定資產／`3` 進項不可扣抵之進貨及費用／`4` 進項不可扣抵之固定資產 |
| `invoiceNo` | 發票號碼 | 條件必填 | 依格式／路徑；亦可用 `invoiceNumber` |
| `otherDocNo` | 其他憑證號碼 | 條件必填 | 依格式／彙加路徑 |
| `customsAuthCode` | 海關代徵認證碼 | 條件必填 | 格式 28／29 |
| `utilityCarrierSn` | 公用事業載具流水號 | 條件必填 | 格式 25 |
| `buyerTaxId` | 買受人統一編號 | **必填** | 8 碼，含檢查碼 |
| `sellerTaxId` | 銷售人統編 | 條件必填 | 依格式／彙加可能必填或禁填 |
| `taxType` | 課稅別 | **必填** | `1` 應稅／`2` 零稅率／`3` 免稅／`F` 作廢／`D` 空白未使用 |
| `salesAmount` | 銷售金額 | 條件必填 | 亦可用 `salesAmt`；部分規則必填 |
| `taxAmount` | 稅額 | 條件必填 | 亦可用 `taxAmt`；依課稅別／格式檢核 |
| `totalAmount` | 總計 | 選填 | 亦可用 `totalAmt` |
| `summaryMark` | 彙加／分攤註記 | 條件必填 | `A` 彙加／`B` 分攤；部分格式必填 |
| `summaryQty` | 彙總數量 | 條件必填 | 彙加等情境 |
| `fileBizTax` | 申報營業稅 | 選填 | `true`／`false` 或 `T`／`F` |
| `clientRef` | 客戶對帳鍵 | 選填 | 字串 1–64；回應原樣回傳 |
| `transaction` | 來源交易 | 選填 | 既有交易 internal id（掛 parent；CSV 亦可用 `transactionId`） |

Lookup 類欄位可傳 **業務代碼**（建議）或 NetSuite **internal id**。

#### 5.1.1 格式 21／25 與 ERP 欄位映射（invCatCode）

當 ERP 只傳營業稅代號 `docFormatCode: "21"` 或 `"25"` 時，須再帶 **`invCatCode`**（或 **`docFormatName`**／lookup internal id）消歧：

| docFormatCode | invCatCode | NetSuite Name（docFormatName） | 常見 ERP 憑證類型關鍵字 |
|---------------|------------|--------------------------------|-------------------------|
| 21 | 01 | 21-三聯式統一發票 | 三聯式、手開三聯式 |
| 21 | 05 | 21-電子計算機統一發票 | 電子計算機、電算機 |
| 25 | 06 | 25-進項三聯式收銀機統一發票（…） | 三聯式收銀機、POS 三聯式 |
| 25 | 07 | 25-進項一般稅額計算之電子發票（…） | 一般稅額電子發票、B2B 電子發票 |

完整映射表（含備註）：[docs/samples/ap-invCatCode-erp-mapping.csv](./samples/ap-invCatCode-erp-mapping.csv)

**虛擬碼（ERP 對照邏輯）：**

```
若 docFormatCode = "21":
  若 憑證類型 含「電子計算機」或「電算機」→ invCatCode = "05"
  否若 含「三聯式」→ invCatCode = "01"
若 docFormatCode = "25":
  若 憑證類型 含「三聯式收銀機」或「收銀機（三聯）」→ invCatCode = "06"
  否若 含「電子發票」或「一般稅額」→ invCatCode = "07"
否則 → 改傳 docFormatName 或 lookup internal id
```

**JSON 範例（格式 25）：**

```json
{
  "docFormatCode": "25",
  "invCatCode": "07",
  "docDate": "2026/08/01",
  "docPeriod": "11508",
  "filingPeriod": "11508",
  "deductionCode": "1",
  "invoiceNo": "CA12345680",
  "buyerTaxId": "24536806",
  "sellerTaxId": "24536806",
  "taxType": "1",
  "salesAmount": 1000,
  "taxAmount": 50,
  "totalAmount": 1050,
  "fileBizTax": true
}
```

> **條件必填**：實際是否必填／禁填依財政部進項規則與憑證格式而異（例如字軌、統編、稅額容差）。請用 `options.validateOnly: true` 先試跑。

### 5.2 範例：只寫進項

```json
{
  "requestId": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "action": "apInvoiceOnly",
  "apInvoices": [
    {
      "docFormatCode": "21",
      "invCatCode": "01",
      "docDate": "2026/08/01",
      "docPeriod": "11508",
      "filingPeriod": "11508",
      "deductionCode": "1",
      "invoiceNo": "CA12345678",
      "buyerTaxId": "24536806",
      "sellerTaxId": "24536806",
      "taxType": "1",
      "salesAmount": 1000,
      "taxAmount": 50,
      "totalAmount": 1050,
      "fileBizTax": true
    }
  ]
}
```

成功回應示例：

```json
{
  "success": true,
  "returnCode": "0000",
  "returnMessage": "OK",
  "action": "apInvoiceOnly",
  "validateOnly": false,
  "requestId": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "results": [
    {
      "index": 0,
      "id": "1001",
      "invoiceNo": "CA12345678",
      "clientRef": "inv-001"
    }
  ]
}
```

---

## 6. 交易物件（transaction）

```json
{
  "id": "456",
  "type": "vendorbill",
  "fields": {
    "entity": "123",
    "subsidiary": "1",
    "account": "220",
    "memo": "from external system"
  },
  "expenseLines": [
    { "account": "610", "amount": 1050 }
  ],
  "itemLines": []
}
```

| 欄位 | 必填 | 說明 |
|------|------|------|
| `id` | 選填 | 既有交易 internal id；有值則不新建（與 `purchaseOrderId` 互斥） |
| `type` | 條件必填 | 新建時必填（見 §2.1）；僅引用既有 `id` 時可省略 |
| `purchaseOrderId` | 選填 | **僅 `vendorbill`**：從既有 Purchase Order transform 建立 VB（見 §6.2）；不可與 `id` 同時出現 |
| `fields` | 選填 | 表頭欄位 → NetSuite field id → value（**無白名單**，見 §6.0）。實際 NetSuite 表單必填依客戶設定 |
| `expenseLines` | 選填 | `expense` 子清單列（欄位同樣透傳） |
| `itemLines` | 條件必填 | 一般選填；有 `purchaseOrderId` 時**必填**非空（見 §6.2） |
| `apInvoices` | 條件必填 | **僅** `transactionWithApInvoices`：每筆交易內必填非空陣列 |
| `clientRef` | 選填 | 字串 1–64；回應原樣回傳 |

`fields`／行項目在 NetSuite 端的必填與合法值依客戶設定而定（廠商／科目／子公司等請向客戶索取對照表）。

### 6.0 客製化欄位（允許透傳）

現階段設計**允許**透過 API 寫入 Vendor Bill／Expense Report／Vendor Credit／Vendor Prepayment 的客製欄位。

- `transaction.fields` 的 key 會直接作為 NetSuite 表頭 `fieldId` 呼叫 `setValue`（例如 `custbody_xxx`）。
- `expenseLines`／`itemLines` 的 key 會直接作為子清單 `fieldId` 呼叫 `setCurrentSublistValue`（例如 `custcol_yyy`）。
- Payload 契約**沒有**欄位白名單；PCE **不會**驗證這些交易／客製欄內容，成敗由 NetSuite 欄位設定、表單、權限與值型別決定。

```json
{
  "requestId": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "action": "transactionOnly",
  "transaction": {
    "type": "vendorbill",
    "fields": {
      "entity": "123",
      "subsidiary": "1",
      "memo": "AP Write",
      "custbody_xxx": "自訂表頭值"
    },
    "expenseLines": [
      {
        "account": "610",
        "amount": 1050,
        "custcol_yyy": "自訂行值"
      }
    ]
  }
}
```

注意：

1. Field ID 必須正確，且該交易類型／表單上確實有該欄。
2. 值型別須符合欄位（select 傳 internal id、日期格式等依客戶環境）。
3. Integration／Role 需具備寫入該欄的權限。
4. 標準欄有寫入順序偏好（`entity`／`subsidiary` 等較早；`account`／`payment` 最後）。客製欄通常排在多數標準欄之後；若客製欄依賴 sourcing，請與客戶一併驗證。

### 6.1 範例：只建交易

```json
{
  "requestId": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "action": "transactionOnly",
  "transaction": {
    "type": "vendorbill",
    "fields": {
      "entity": "123",
      "subsidiary": "1",
      "account": "220",
      "memo": "AP Write"
    },
    "expenseLines": [
      { "account": "610", "amount": 1050 }
    ]
  }
}
```

### 6.2 從 Purchase Order 建立 Vendor Bill

當 `type` 為 `vendorbill` 且帶 **`purchaseOrderId`**（PO 內部 id）時，API 會以 NetSuite **transform**（PO → Vendor Bill）建立帳單，並依 request 的 `itemLines` 覆寫數量（支援部分開帳）。

規則：

| 項目 | 說明 |
|------|------|
| 適用 | 僅 `vendorbill`；其他 type 帶 `purchaseOrderId` → `PURCHASE_ORDER_NOT_ALLOWED` |
| 與 `id` | 不可同時出現 → `PURCHASE_ORDER_WITH_EXISTING_ID` |
| `itemLines` | **必填**非空陣列；每筆必有 `orderline`（或別名 `line`）與 `quantity`（≥0 數字） |
| 數量 | 以 request 的 `quantity` 為準；**未出現在 request 的 PO 行**會設為 `quantity=0`（不開帳） |
| 找不到行 | request 的 `orderline` 在 transform 後的 VB 上不存在 → `PO_ORDERLINE_NOT_FOUND` |
| 其他欄 | `fields`／`itemLines` 其餘 key 仍可透傳（如 `rate`、`custcol_*`）；可另帶 `expenseLines` 追加費用列 |

`itemLines[]`（有 `purchaseOrderId` 時）：

| 欄位 | 必填 | 說明 |
|------|------|------|
| `orderline`（或 `line`） | **必填** | 對應 PO item 子清單行號 |
| `quantity` | **必填** | ≥0 數字；以此覆寫開帳數量 |
| 其餘 key（如 `rate`、`custcol_*`） | 選填 | 透傳覆寫；`quantity` 仍以 request 為準 |

```json
{
  "requestId": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "action": "transactionOnly",
  "transaction": {
    "type": "vendorbill",
    "purchaseOrderId": "12345",
    "fields": {
      "memo": "bill from PO"
    },
    "itemLines": [
      { "orderline": "1", "quantity": 2 },
      { "orderline": "3", "quantity": 1, "rate": 100 }
    ]
  }
}
```

成功時 `results[]` 會含 `transactionId`、`transactionCreated: true`，以及原樣回傳的 `purchaseOrderId`。

### 6.3 範例：交易＋多筆進項

```json
{
  "requestId": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "action": "transactionWithApInvoices",
  "transaction": {
    "clientRef": "vb-001",
    "type": "vendorbill",
    "fields": {
      "entity": "123",
      "subsidiary": "1",
      "account": "220"
    },
    "expenseLines": [
      { "account": "610", "amount": 2100 }
    ],
    "apInvoices": [
      {
        "clientRef": "inv-001",
        "docFormatCode": "21",
        "invCatCode": "01",
        "docDate": "2026/08/01",
        "docPeriod": "11508",
        "filingPeriod": "11508",
        "deductionCode": "1",
        "invoiceNo": "CA12345678",
        "buyerTaxId": "24536806",
        "sellerTaxId": "24536806",
        "taxType": "1",
        "salesAmount": 1000,
        "taxAmount": 50,
        "totalAmount": 1050,
        "fileBizTax": true
      },
      {
        "clientRef": "inv-002",
        "docFormatCode": "21",
        "invCatCode": "05",
        "docDate": "2026/08/02",
        "docPeriod": "11508",
        "filingPeriod": "11508",
        "deductionCode": "1",
        "invoiceNo": "CA12345679",
        "buyerTaxId": "24536806",
        "sellerTaxId": "24536806",
        "taxType": "1",
        "salesAmount": 1000,
        "taxAmount": 50,
        "totalAmount": 1050,
        "fileBizTax": true
      }
    ]
  },
  "options": {
    "validateOnly": false
  }
}
```

成功回應示例：

```json
{
  "success": true,
  "action": "transactionWithApInvoices",
  "requestId": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "results": [
    {
      "index": 0,
      "clientRef": "vb-001",
      "transactionId": "789",
      "transactionType": "vendorbill",
      "transactionCreated": true,
      "apInvoices": [
        {
          "index": 0,
          "id": "1001",
          "invoiceNo": "CA12345678",
          "clientRef": "inv-001"
        },
        {
          "index": 1,
          "id": "1002",
          "invoiceNo": "CA12345679",
          "clientRef": "inv-002"
        }
      ]
    }
  ]
}
```

掛到既有交易：

```json
{
  "requestId": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "action": "transactionWithApInvoices",
  "transaction": {
    "id": "789",
    "apInvoices": [ { "...": "..." } ]
  }
}
```

---

## 7. CSV 匯入進項（Suitelet）

除 RESTlet 外，亦可在 NetSuite 內以 **Suitelet 上傳 CSV** 建立進項（內部仍呼叫同一支 AP Write：`action=apInvoiceOnly`）。

| 項目 | 說明 |
|------|------|
| Script | `customscript_pce_sl_ap_csv_import`／`customdeploy_pce_sl_ap_csv_import` |
| 開啟 | Customization → Scripting → Script Deployments → **PCE SL AP CSV Import** |
| 編碼 | Excel 另存 CSV（建議「CSV UTF-8」）；若中文亂碼請確認為 UTF-8 |
| 上限 | 單次 **100** 列資料（不含表頭） |
| 欄位標頭 | 與 §5.1 API 別名相同（大小寫不敏感）；亦可 `custrecord_pce_ap_*` |
| 掛交易 | 可選欄 `transaction` 或 `transactionId`＝既有交易 internal id |
| 驗證 | 勾選「只驗證不寫入」＝`options.validateOnly: true` |
| 對帳 | 每次匯入產生 `requestId`（`csv-…`），並寫入 **PCE AP Write 呼叫紀錄** |
| POST 後導向 | **PRG**：POST 完成後 `https.redirect` 至 GET `?action=result&writeLogId={id}`；重新整理不會重送 CSV／API |
| 結果還原 | GET 結果頁自 **PCE AP Write 呼叫紀錄** 讀取 response JSON，並自 `_csvUi` 快照還原表頭／validateOnly／列數（`PCE_LIB_AP_CsvImportReplay.js`） |
| 結果畫面 | 本頁 **Bootstrap** 自訂介面：中文總結（成功／失敗筆數）；**失敗列優先**並列出中文原因；成功列可開進項紀錄 |
| 失敗重試 | 有失敗時可 **下載失敗列 CSV**（表頭與原檔相同），修正後再上傳 |
| 範本下載 | 頁面提供 **下載 CSV 範本**（`action=template`）與 **下載欄位說明**（`action=guide`） |

範本：[docs/samples/ap-invoice-import-sample.csv](./samples/ap-invoice-import-sample.csv)（含 21／25 各 invCatCode 範例列）／欄位說明：[docs/samples/ap-invoice-import-field-guide.csv](./samples/ap-invoice-import-field-guide.csv)／ERP 映射：[docs/samples/ap-invCatCode-erp-mapping.csv](./samples/ap-invCatCode-erp-mapping.csv)

> 與原生 NetSuite CSV Import 不同：此 Suitelet 支援 API 別名／業務代碼、逐列結果、呼叫紀錄與 partial success，契約與 RESTlet 一致。技術欄位（`returnCode`、完整 JSON）摺在「進階資訊」。

---

## 8. 僅驗證（validateOnly）

```json
{
  "requestId": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
  "action": "apInvoiceOnly",
  "options": { "validateOnly": true },
  "apInvoices": [ { "...": "..." } ]
}
```

| action | validateOnly 行為 |
|--------|-------------------|
| `apInvoiceOnly` | 跑進項驗證，不 create |
| `transactionWithApInvoices` | 驗證進項（若有 `transaction.id` 會帶入關聯）；不 create 交易／發票 |
| `transactionOnly` | **只**確認 JSON 結構合法，不驗證交易內容、不存檔 |

上線前請務必先用 `validateOnly: true` 對真實樣本資料試跑。

---

## 9. 錯誤碼一覽（介接端）

### 8.1 returnCode（建議程式判斷用）

| returnCode | 語意 | success |
|------------|------|---------|
| `0000`–`0099` | 成功／部分成功（見 §4.2；`0004`=partial） | `true`（`0004` 時為 `false`） |
| `1000`–`1999` | Request／Payload shape | `false` |
| `2000`–`2999` | 進項業務驗證 | `false` |
| `3000`–`3999` | Lookup／主檔解析 | `false` |
| `4000`–`4999` | NetSuite 執行期（權限、save 失敗等） | `false` |
| `9000`–`9999` | 未預期錯誤 | `false` |

### 8.2 error.code 對照

| code | returnCode | 意涵 | 建議處理 |
|------|------------|------|----------|
| `INVALID_BODY` | `1001` | body 不是 JSON 物件 | 檢查序列化 |
| `INVALID_ACTION` | `1002` | action 拼錯 | 使用三種合法值之一 |
| `REQUEST_ID_REQUIRED` | `1003` | 缺少 `requestId` | 補上冪等鍵（建議 UUID v4） |
| `REQUEST_ID_INVALID` | `1004` | `requestId` 格式不合 | 8–64 字元，僅 `A–Z a–z 0–9 - _` |
| `REQUEST_ID_PAYLOAD_MISMATCH` | `1005` | 同 `requestId` 但 payload 不同 | 換新 `requestId`，或重送完全相同的 body |
| `AP_INVOICES_REQUIRED` | `1006` | 缺少進項陣列 | `apInvoiceOnly` 補 top-level `apInvoices`；`transactionWithApInvoices` 補每筆 `transaction.apInvoices` |
| `AP_INVOICES_TOP_LEVEL_NOT_ALLOWED` | `1008` | `transactionWithApInvoices` 不可用 top-level 發票欄位 | 改為巢狀在每個 `transaction` 內 |
| `CLIENT_REF_INVALID` | `1013` | `clientRef` 格式不合 | 使用 1–64 字元字串 |
| `AP_INVOICES_NOT_ALLOWED` | `1007` | `transactionOnly` 不應帶發票（top-level 或巢狀） | 改 action 或拿掉陣列 |
| `TRANSACTION_REQUIRED` | `1009` | 缺少交易 | 補 `id` 或 `type` |
| `TRANSACTION_NOT_ALLOWED` | `1010` | `apInvoiceOnly` 不可新建交易 | 改用 `transactionWithApInvoices` |
| `INVALID_TRANSACTION_TYPE` | `1011` | type 不支援 | 改為 §2.1 列表 |
| `TRANSACTION_LIMIT_EXCEEDED` | `1012` | 一次超過 10 筆交易 | 分批呼叫 |
| `PCE_METHOD_NOT_ALLOWED` | `1014` | 不支援的 HTTP 方法（如 GET） | 改用 POST |
| `PURCHASE_ORDER_NOT_ALLOWED` | `1015` | 非 vendorbill 卻帶 `purchaseOrderId` | 改 type 或拿掉欄位 |
| `PURCHASE_ORDER_WITH_EXISTING_ID` | `1016` | 同時帶 `id` 與 `purchaseOrderId` | 擇一：引用既有 VB，或從 PO 新建 |
| `PO_ITEM_LINES_REQUIRED` | `1017` | 從 PO 建立時缺 `itemLines` | 補上含 orderline／quantity 的列 |
| `PO_ORDERLINE_REQUIRED` | `1018` | item 列缺 `orderline`／`line` | 對應 PO 行號 |
| `PO_QUANTITY_REQUIRED` | `1019` | item 列缺合法 `quantity` | 填 ≥0 數字 |
| `PCE_AP_INVOICE_VALIDATION_ERROR` | `2001` | 進項業務規則失敗 | 讀 `results[i].error`／`message`／`details` 修正欄位 |
| `BATCH_FAILED` | `2100` | 批次內全部項目失敗 | 逐筆看 `results[]` |
| `PARTIAL_SUCCESS` | `0004` | 部分成功 | 依 `results[]` 保留成功項，失敗項換新 `requestId` 重送 |
| `PCE_LOOKUP_RESOLVE_ERROR` | `3001` | 代碼無法對應主檔 | 向客戶確認 lookup／代碼表 |
| `PCE_INVALID_TRANSACTION_TYPE` | `3002` | 執行期 type 無法對應 record | 改為 §2.1 列表 |
| `SKIPPED_PARENT_FAILED` | `4001` | 父交易失敗，巢狀發票未處理 | 先修好交易再重送 |
| `PO_ORDERLINE_NOT_FOUND` | `4002` | request 的 orderline 不在 PO／transform 結果 | 核對 PO 行號 |
| `PO_TRANSFORM_FAILED` | `4003` | PO→VB transform 失敗 | 查 PO 狀態／權限／可開帳量 |
| 其他 NetSuite 錯誤 | `4000` | 權限、必填、系統錯誤 | 聯絡客戶 NetSuite 管理員 |
| `UNEXPECTED_ERROR` | `9999` | 未預期例外 | 附完整 response 聯絡技術窗口 |

HTTP 層面的 401／403 屬 OAuth 2.0 token／角色問題，與 JSON `returnCode`／`success: false` 不同。

---

## 10. 呼叫範例（概念）

```bash
# 1) 取得 access token（M2M；client_assertion 為已簽署 JWT）
curl -X POST "$TOKEN_URL" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
  -d "client_assertion=$CLIENT_ASSERTION"

# 2) GET is not allowed (expect PCE_METHOD_NOT_ALLOWED)
curl -X GET "$RESTLET_URL" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json"

# 3) POST write
curl -X POST "$RESTLET_URL" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d @payload.json
```

JWT 產生可參考 [`docs/postman/generate-client-assertion.js`](./postman/generate-client-assertion.js)。access token 有時效，過期需重新向 token endpoint 換發。

---

## 11. 介接注意事項

1. **一筆交易可對多筆進項**；請用 `transactionWithApInvoices` 一次送齊，或先建交易再用 `transaction.id` 分批掛發票。  
2. **每次邏輯寫入必須帶唯一 `requestId`（建議 UUID v4）**；網路逾時重試請重用同一把，避免重複建單。  
3. **發票號碼重複**會被驗證擋下；請避免重送同一張已成功寫入的發票。  
4. **目前僅支援新建進項**，不支援透過本 API 更新／刪除既有進項。  
5. **統編、字軌、申報期**等規則以客戶環境主檔與財政部邏輯為準；測試帳與正式帳資料可能不同。  
6. 成功回應以 `results[]` 對應 request（含 `index`／可選 `clientRef`／`invoiceNo`／`transactionId`／`apInvoices[].id`）；請由貴系統保存，作為對帳與除錯依據。  
7. 若同時需要 UI 與 API 寫入，兩邊共用同一套驗證規則，行為應一致。

---

## 12. 聯絡與變更

- 介面變更（action、必填、錯誤碼）應由客戶專案組提前書面通知。  
- 技術爭議或驗證規則疑問，請透過客戶指定之顧問／技術窗口，並附上：Account ID、request（可遮罩敏感資料）、完整 `error` JSON、時間（含時區）。

---

*文件版本：對應 SuiteApp `com.netsuite.pceguitw` — PCE RL AP Write（`customscript_pce_rl_ap_write`）。*
