# PCE AP Write API — 技術與測試文件（內部）

> **對象**：內部技術人員、實作顧問、QA  
> **範圍**：架構、部署、驗證鏈、除錯、單元／整合測試  
> **相關**：[介接手冊（廠商）](./PCE_GUI_MODULE_ap-write-api-integration-guide.md) · [English](./PCE_GUI_MODULE_ap-write-api-technical.en.md) · [Postman](./postman/PCE-AP-Write.postman_collection.json)

---

## 1. 服務概要

| 項目 | 內容 |
|------|------|
| 用途 | 外部系統透過 RESTlet 寫入進項發票（`customrecord_pce_ap_invoice`）與／或 AP 交易 |
| Script | `customscript_pce_rl_ap_write` |
| Deployment | `customdeploy_pce_rl_ap_write` |
| Entry | `ap/restlets/PCE_RL_AP_Write.js` |
| Payload 契約 | `ap/libraries/PCE_LIB_AP_WritePayload.js` |
| 驗證 | `ap/libraries/PCE_LIB_AP_Validate.js`（寫入前 + UE `beforeSubmit` 再驗） |
| 呼叫紀錄 | `customrecord_pce_ap_write_log`（每次呼叫自動寫入 request／response；含被拒的 GET） |
| HTTP | **僅 POST**（GET 回 `PCE_METHOD_NOT_ALLOWED`） |

### 1.1 三種 action

| action | 行為 |
|--------|------|
| `apInvoiceOnly` | 只建進項發票；不可帶「要新建的」transaction |
| `transactionOnly` | 只建／引用交易；不可帶 `apInvoices` |
| `transactionWithApInvoices` | 先解析／建立交易，再寫一至多筆進項並掛 parent |

支援交易類型：`vendorbill`｜`expensereport`｜`vendorcredit`｜`vendorprepayment`。

---

## 2. 程式結構

```
ap/
  restlets/
    PCE_RL_AP_Write.js          # RESTlet 入口（每次呼叫寫入 API Log）
  suitelets/
    PCE_SL_ApWriteSmoke.js      # AP Write smoke Suitelet
    PCE_SL_AP_CsvImport.js      # CSV 匯入 Suitelet（POST→GET PRG）
  libraries/
    PCE_LIB_AP_CsvImportReplay.js  # CSV 結果頁自 write log 還原 UI
    PCE_LIB_AP_WriteReturnCode.js  # returnCode 對照與 applyReturnCode
    PCE_LIB_AP_WritePayload.js  # action／shape／alias 正規化
    PCE_LIB_AP_Validate.js      # 進項業務驗證（MOF 對齊）
    PCE_LIB_AP_Lookup.js        # 格式／稅別／扣抵／字軌 code↔id
    PCE_LIB_AP_Period.js        # 民國年月／申報期
    PCE_LIB_AP_WriteLog.js      # 寫入 customrecord_pce_ap_write_log
    PCE_AP_Fields.js            # 欄位 script id 常數
  clientscripts/                # UI（與本 API 分離）
  userevents/
    PCE_UE_APInvoice_DefaultValues.js  # beforeSubmit 再呼叫 Validate
  common/
    PCE_LIB_TaxId.js            # 統編檢查（由 Validate 引用）
```

### 2.0 API 呼叫紀錄（`customrecord_pce_ap_write_log`）

每次 POST（以及被拒的 GET）結束後由 RESTlet 自動建立一筆紀錄。寫入失敗只記 Execution Log，**不影響** API 回應。

| 欄位 | Script ID | 說明 |
|------|-----------|------|
| Method | `custrecord_pce_apwl_method` | `GET`（應被拒）／`POST` |
| Action | `custrecord_pce_apwl_action` | POST 的 `action` |
| Request ID | `custrecord_pce_apwl_request_id` | 冪等鍵 `requestId` |
| Success | `custrecord_pce_apwl_success` | 回應 `success` |
| Error Code | `custrecord_pce_apwl_error_code` | 失敗時 `error.code` |
| Duration (ms) | `custrecord_pce_apwl_duration_ms` | 處理耗時 |
| Governance (units) | `custrecord_pce_apwl_governance` | 本次 API 消耗的 SuiteScript governance units（寫入本紀錄前計算） |
| User | `custrecord_pce_apwl_user` | 執行使用者 |
| Request | `custrecord_pce_apwl_request` | request JSON（過長截斷） |
| Response | `custrecord_pce_apwl_response` | response JSON（過長截斷） |

UI：Lists → Custom → **PCE AP Write 呼叫紀錄**。  
部署後請確認 Integration／Role 對此 custom record 有 **Create** 權限。

### 2.1 POST 處理流程

```mermaid
flowchart TD
  A[POST body] --> B[validatePayloadShape]
  B -->|invalid| F[fail shape error]
  B -->|valid| C{action}
  C -->|apInvoiceOnly| D[normalize + validate + create invoices]
  C -->|transactionOnly| E[resolveOrCreateTransaction]
  C -->|transactionWithApInvoices| G[resolveOrCreateTransaction]
  G --> D2[normalize + validate + create invoices with parent]
  D --> H[ok + warnings]
  E --> H
  D2 --> H
  D -->|validate error| X[fail PCE_AP_INVOICE_VALIDATION_ERROR]
  D2 -->|validate error| X
```

`resolveOrCreateTransaction`：有 `transaction.id` 則引用既有交易；否則若 `type=vendorbill` 且帶 `purchaseOrderId`，則 `record.transform`（purchaseorder → vendorbill），再依 `itemLines[].orderline`／`quantity` 覆寫（未列之 PO 行 quantity=0）；其餘情況走既有 `record.create` + fields／lines。

### 2.2 驗證雙關卡

1. **RESTlet**：`buildValidatedInvoiceData` → `validateRecord`；失敗不 save。  
2. **UE**：實際 `record.save` 時 `beforeSubmit` 再驗一次（CSV／WS／UI／RESTLET 皆會跑，視部署 context）。

Select 欄位（格式／課稅別／扣抵／彙加註記）可傳 **代碼**（如 `"21"`）或 **internal id**；由 `resolveSelectValue` + Lookup 解析。進項格式 **21／25** 已拆 2 筆，僅傳代碼時須加 **`invCatCode`** 或 **`docFormatName`**。

---

## 3. 部署與環境

### 3.1 SDF / SuiteCloud

- Script 物件：`src/Objects/Scripts/customscript_pce_rl_ap_write.xml`
- FileCabinet：`ap/restlets/*`、`ap/suitelets/*`、`ap/libraries/*`（見 `deploy.xml`）
- Deploy SuiteApp 後確認 Script Deployment **Released**、`isdeployed = T`

### 3.2 權限與 Integration

| 檢查項 | 說明 |
|--------|------|
| Integration Application | **OAuth 2.0**（建議 M2M Client Credentials；測試可用 Authorization Code） |
| Scope | 至少勾選 **RESTlets** |
| Role | 需有 RESTlet 執行權、進項 custom record 建立／編輯、目標交易類型 create／edit；M2M mapping 綁定此 Role |
| Audience | 目前 deployment 設 `allroles = T`；正式環境建議收斂角色 |
| Log level | 預設 DEBUG；正式可改 ERROR／AUDIT |

### 3.3 取得 External URL

Customization → Scripting → Script Deployments → **PCE RL AP Write** → 複製 External URL。  
格式大致為：

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

帳號 ID、script／deploy 內部 id 以環境為準（scriptid 字串不等於 URL 上的數字 id）。

---

## 4. 錯誤碼（技術對照）

實作：`ap/libraries/PCE_LIB_AP_WriteReturnCode.js`；RESTlet `ok()`／`fail()` 與 `enrichResponseWithRequest()` 結尾皆呼叫 `applyReturnCode()`。

### 4.0 returnCode 區間

| 區間 | 語意 |
|------|------|
| `0000`–`0099` | 成功（`0000` OK、`0001` validateOnly、`0002` 冪等重播、`0003` 有 warnings、`0004` partial） |
| `1000`–`1999` | Payload shape |
| `2000`–`2999` | 進項驗證 |
| `3000`–`3999` | Lookup |
| `4000`–`4999` | NetSuite 執行期 |
| `9000`–`9999` | 未預期 |

### 4.1 Payload shape（寫入前、不進 try 內業務）

| code | returnCode | 時機 |
|------|------------|------|
| `INVALID_BODY` | `1001` | body 非物件 |
| `INVALID_ACTION` | `1002` | action 非法 |
| `REQUEST_ID_REQUIRED` | `1003` | 缺 `requestId` |
| `REQUEST_ID_INVALID` | `1004` | `requestId` 格式不合（需 8–64 字元 `A–Z a–z 0–9 - _`） |
| `REQUEST_ID_PAYLOAD_MISMATCH` | `1005` | 同 `requestId` 已成功寫入，但本次 payload 指紋不同 |
| `AP_INVOICES_REQUIRED` | `1006` | 需要發票陣列卻空 |
| `AP_INVOICES_NOT_ALLOWED` | `1007` | `transactionOnly` 帶了發票 |
| `AP_INVOICES_TOP_LEVEL_NOT_ALLOWED` | `1008` | `transactionWithApInvoices` 仍使用 top-level 發票欄位 |
| `TRANSACTION_REQUIRED` | `1009` | 缺 `transaction.id`／`type` |
| `TRANSACTION_NOT_ALLOWED` | `1010` | `apInvoiceOnly` 卻帶新建交易內容 |
| `INVALID_TRANSACTION_TYPE` | `1011` | type 不在白名單 |
| `TRANSACTION_LIMIT_EXCEEDED` | `1012` | 一次交易筆數超過上限（10） |
| `CLIENT_REF_INVALID` | `1013` | `clientRef` 非 1–64 字元字串 |
| `PCE_METHOD_NOT_ALLOWED` | `1014` | 不支援的 HTTP 方法 |
| `PURCHASE_ORDER_NOT_ALLOWED` | `1015` | 非 vendorbill 卻帶 `purchaseOrderId` |
| `PURCHASE_ORDER_WITH_EXISTING_ID` | `1016` | 同時帶 `id` 與 `purchaseOrderId` |
| `PO_ITEM_LINES_REQUIRED` | `1017` | 從 PO 建立時缺非空 `itemLines` |
| `PO_ORDERLINE_REQUIRED` | `1018` | item 列缺 `orderline`／`line` |
| `PO_QUANTITY_REQUIRED` | `1019` | item 列缺合法 `quantity` |

### 4.2 執行期（catch → fail）

| name / code | returnCode | 說明 |
|-------------|------------|------|
| `PCE_AP_INVOICE_VALIDATION_ERROR` | `2001` | Validate 失敗；`error.details` = 欄位錯誤物件（亦出現在 `results[i].error`） |
| `BATCH_FAILED` | `2100` | 批次內全部 leaf 失敗（多筆） |
| `PARTIAL_SUCCESS` | `0004` | 部分成功；`partialSuccess: true`，仍回 `results[]` |
| `PCE_LOOKUP_RESOLVE_ERROR` | `3001` | code／id 無法對到 lookup |
| `PCE_INVALID_TRANSACTION_TYPE` | `3002` | create 時 type 對不到 record.Type |
| `SKIPPED_PARENT_FAILED` | `4001` | 父交易失敗，巢狀發票未處理 |
| `PO_ORDERLINE_NOT_FOUND` | `4002` | request orderline 不在 transform 後 VB |
| `PO_TRANSFORM_FAILED` | `4003` | PO→VB transform／save 失敗 |
| NetSuite 原生錯誤 | `4000` | 權限、必填、record.save 失敗等 |
| `UNEXPECTED_ERROR` | `9999` | 無 name 的例外 |

**Partial success 行為**：`processApInvoices`／多筆 transaction 逐筆 try/catch，不因單筆失敗中斷整包。`findSuccessfulWriteByRequestId` 亦會把 `returnCode=0004`／`partialSuccess` 視為已提交，避免同 `requestId` 重試重複建單。

回應型態一律含 `returnCode`／`returnMessage`：

```json
{
  "success": false,
  "returnCode": "2001",
  "returnMessage": "AP_INVOICE_VALIDATION_FAILED",
  "error": {
    "code": "PCE_AP_INVOICE_VALIDATION_ERROR",
    "message": "...",
    "details": null
  }
}
```

---

## 5. 測試策略

### 5.1 單元測試（本機，不需 NetSuite）

```bash
npm test
# 或分開跑
npm run test:ap-validate
npm run test:ap-write-payload
npm run test:ap-write-return-code
npm run test:ap-csv
```

| 檔案 | 覆蓋 |
|------|------|
| `test/ap-write-payload.test.js` | action／tx type／alias／shape |
| `test/ap-write-return-code.test.js` | returnCode 對照與成功優先序 |
| `test/ap-csv.test.js` | CSV 解析／表頭／100 列上限 |
| `test/ap-validate.test.js` | 進項驗證規則（MOF） |
| `test/helpers/amd-loader.js` | SuiteScript AMD → Node |

**變更契約時**：先改 `PCE_LIB_AP_WritePayload.js` + 對應單元測試，再改 RESTlet 與文件。

### 5.2 帳號內冒煙測試（顧問／QA）

建議順序：

1. **GET** → 應回 `success: false`、`error.code: PCE_METHOD_NOT_ALLOWED`（本 API **不允許** GET）。  
2. **POST** `options.validateOnly: true` + `apInvoiceOnly` 合法 payload → 不應產生 record。  
3. **POST** 故意錯統編／字軌 → `PCE_AP_INVOICE_VALIDATION_ERROR` + `details`。  
4. **POST** 真實寫入一筆 `apInvoiceOnly`（測試統編／字軌表需齊）→ UI 開進項確認。  
5. **POST** `transactionWithApInvoices` → 確認 VB／ER／VC／Prepayment parent 與子清單。  
6. 用既有 `transaction.id` 再掛第二張進項 → 驗證 1:N。  
7. （可選）有可用 PO 時：`vendorbill` + `purchaseOrderId` + `itemLines[].orderline`／`quantity` → 確認部分開帳與未列行 quantity=0。

### 5.3 Postman / curl 注意

- 匯入 [`docs/postman/PCE-AP-Write.postman_collection.json`](./postman/PCE-AP-Write.postman_collection.json)（可選環境檔同目錄）
- 先跑 **0. Auth → Get Access Token**；Collection Auth = Bearer `{{accessToken}}`
- M2M JWT：[`docs/postman/generate-client-assertion.js`](./postman/generate-client-assertion.js)
- Content-Type: `application/json`
- `validateOnly` 對 `transactionOnly`：只檢查 shape，**不**模擬 NetSuite 交易必填

### 5.4 建議測試資料檢查清單

- [x] Lookup：憑證格式 21–29、課稅別、扣抵、彙加註記（Round C validate／寫入通過）
- [x] 字軌表：對應申報期／格式有字軌（11508 格式 21：CA、CB）
- [x] Vendor／Subsidiary／Account（建 VB 時；Round C fixtures）
- [x] UE 部署含 RESTLET execution context（寫入路徑驗證通過）
- [x] Client Script 不影響 RESTlet 路徑（預期；RESTlet 寫入成功）

### 5.5 帳號內實測紀錄（`transactionOnly`）

帳號：**TD2905095**（Chesley's Dev Environment）。  
RESTlet：`customscript_pce_rl_ap_write`／`customdeploy_pce_rl_ap_write`。  
Action：`transactionOnly`（實際建立交易）。  
主要測試資料：Vendor `944`（Lancome）、Subsidiary `8`（US East）、Employee `1086`、Expense account `415`、Bank `422`、Currency `1`、Location `11`。

#### Round A — SDF Installation smoke（2026-08-04）

| 類型 | 結果 | Internal ID |
|------|------|-------------|
| Vendor Bill | ✅ Pass | 25647 |
| Expense Report | ✅ Pass | 25648 |
| Vendor Credit | ✅ Pass | 25649 |
| Vendor Prepayment | ✅ Pass | 25551 |

原始 JSON：[`test/results/ap-write-smoke-result.json`](../test/results/ap-write-smoke-result.json)

#### Round B — Browser MCP + Suitelet smoke（2026-08-04）

入口：`/app/site/hosting/scriptlet.nl?script=customscript_pce_sl_ap_write_smoke&deploy=customdeploy_pce_sl_ap_write_smoke&action=run`  
（Suitelet 以登入 session 呼叫 `https.requestRestlet` → 正式 AP Write RESTlet。）

| 類型 | 結果 | Internal ID | UI 確認 |
|------|------|-------------|---------|
| Vendor Bill | ✅ Pass | 25747 | Bill #188，Lancome，$1,050，Pending Approval |
| Expense Report | ✅ Pass | 25847 | Employee Abigail Khan |
| Vendor Credit | ✅ Pass | 25848 | Bill Credit #15，memo 含 browser smoke |
| Vendor Prepayment | ✅ Pass | 25947 | VP #26，PAID，$1,050，Bank US East Checking |

原始 JSON：[`test/results/ap-write-browser-smoke-result.json`](../test/results/ap-write-browser-smoke-result.json)

#### Round C — Browser MCP full checklist §5.2（2026-08-04）

入口：`…&action=checklist`  
本機單元測試：`npm test` → **49/49 Pass**。

| Step | 項目 | 結果 | 備註 |
|------|------|------|------|
| 5.4 | 測試資料 fixtures | ✅ | vendor 944 / emp 1086 / bank 422 |
| 1 | GET 不允許 | ✅／⚠️ | 預期 `PCE_METHOD_NOT_ALLOWED` |
| 2 | `apInvoiceOnly` + `validateOnly` | ✅ | 不建檔 |
| 3a | 錯誤統編 | ✅ | `PCE_AP_INVOICE_VALIDATION_ERROR` |
| 3b | 錯誤字軌 AA | ✅ | `GuiTrackNotAllowed`；可用 CA、CB |
| 4 | `apInvoiceOnly` 真實寫入 | ✅ | apInvoiceId **27**（CA26632189） |
| 5.1 | `transactionWithApInvoices` VB | ✅ | tx **26149** + inv 127 |
| 5.2 | ER | ✅ | tx **26150** + inv 128 |
| 5.3 | VC | ✅ | tx **26151** + inv 129 |
| 5.4 | VP | ✅ | tx **26152** + inv 130 |
| 6 | 既有 `transaction.id` 掛第 2 張（1:N） | ✅ | tx 26149 + inv **131**；UI 子清單可見 CA26632688、CA26642463 |

JSON：[`test/results/ap-write-checklist-result.json`](../test/results/ap-write-checklist-result.json)

測試中一併修正：`fileBizTax` 改以 boolean `setValue`；`docDate` 以 `N/format` 轉成 `Date` 再寫入。

#### 備註

- GET **不允許**；應回 `PCE_METHOD_NOT_ALLOWED`（契約／健康檢查改以文件與 `validateOnly` POST 為準）。
- Vendor Prepayment 需設 **Bank account**（非 AP），且 `account`／`payment` 應在 `subsidiary`／`entity` 之後寫入（sourcing 會清掉過早設值）。
- Smoke Suitelet 必須略過 internal id ≤ 0 的 vendor（例如 `-3`），否則會 `INVALID_FLD_VALUE`。

---

## 6. 除錯指南

| 現象 | 可能原因 | 處理 |
|------|----------|------|
| 401 Unauthorized | access token 無效／過期／缺 Bearer | 重新打 token endpoint；檢查 Scope=restlets |
| 403 / INSUFFICIENT_PERMISSION | Role 缺權限 | 加 custom record／交易權限；檢查 M2M Role mapping |
| `PCE_LOOKUP_RESOLVE_ERROR` | 代碼未建 lookup 或傳錯 | 查 `PCE_LIB_AP_Lookup` 與安裝 lookup |
| Validate 過但 save 失敗 | UE／NS 必填／ sourcing | Script Execution Log；比對 UI 同資料 |
| 有 warnings 但仍 success | 稅額 ±5 等「可過提示」 | 屬預期；見 Validate |
| shape OK 但交易沒建 | `validateOnly: true` | 拿掉或設 false |
| `apInvoiceOnly` 帶 transaction | shape 擋 `TRANSACTION_NOT_ALLOWED` | 改用 `transactionWithApInvoices` |
| 無呼叫紀錄 | Role 缺 custom record Create | 開 `customrecord_pce_ap_write_log` 權限；查 `PCE AP Write log failed` |

Execution Log 關鍵字：`PCE AP Write RESTlet failed`、`PCE AP Write log failed`。  
除錯時優先開 **PCE AP Write 呼叫紀錄** 看 Request／Response。

---

## 7. 已知限制與設計取捨

1. **交易欄位不經 PCE Validate（含客製欄）**：`transaction.fields`／`expenseLines`／`itemLines` 的 key 直接 `setValue`／`setCurrentSublistValue`，**無白名單**。標準欄與客製欄（`custbody_*`／`custcol_*` 等）皆可透傳；必填、型別與權限由 NetSuite／客戶表單決定。詳見介接手冊 §6.0。  
2. **`transactionOnly` + `validateOnly`**：不驗證交易內容，只確認 payload shape。  
3. **不更新既有進項**：目前只 create；payload 的 `id`／`externalId` 會被忽略。  
4. **不刪除／作廢**：需另開流程。  
5. **並發重複發票**：Validate 有重複檢核；極端並發仍可能 race，正式環境宜由上游控管。  
6. **requestId 冪等**：靠呼叫紀錄查詢成功寫入；custom record 無真正 unique constraint，極端並發同 `requestId` 仍可能 race（實務上串列重試＋UUID 可接受）。  
7. **Script ID 更名**：若帳號曾部署舊 BPM 命名 script，需停用舊 deployment，改用 `customscript_pce_rl_ap_write`。

---

## 8. 變更影響評估（給顧問）

| 變更類型 | 影響 |
|----------|------|
| 新增 alias | 低；更新 payload lib + 單元測試 + 介接手冊 |
| 改 action 語意 | 高；破壞性，需版控與通知廠商 |
| Validate 規則加嚴 | 中高；既有整合可能大量失敗，需先 `validateOnly` 試跑 |
| Lookup 資料缺漏 | 高；立即 `PCE_LOOKUP_RESOLVE_ERROR` |

---

## 9. 相關內部資產

| 資產 | 路徑 |
|------|------|
| UI 測試 Runbook | `test/UI-TEST-RUNBOOK.md` |
| UI Checklist | `test/UI-TEST-CHECKLIST.md` |
| 進項驗證單元測試 | `test/ap-validate.test.js` |
| Payload 單元測試 | `test/ap-write-payload.test.js` |
| AP Write smoke 結果（SDF） | `test/results/ap-write-smoke-result.json` |
| AP Write smoke 結果（Browser） | `test/results/ap-write-browser-smoke-result.json` |
| AP Write smoke Suitelet | `ap/suitelets/PCE_SL_ApWriteSmoke.js` |
| AP CSV 匯入 Suitelet | `ap/suitelets/PCE_SL_AP_CsvImport.js` + `ap/libraries/PCE_LIB_AP_Csv.js` |
| AP CSV 結果還原 | `ap/libraries/PCE_LIB_AP_CsvImportReplay.js`（write log `_csvUi` 快照） |
| AP CSV 結果還原測試 | `test/ap-csv-import-replay.test.js` |
| AP Write 呼叫紀錄 | `customrecord_pce_ap_write_log`／`PCE_LIB_AP_WriteLog.js` |
| 廠商介接手冊 | `docs/PCE_GUI_MODULE_ap-write-api-integration-guide.md` |
| Technical (EN) | `docs/PCE_GUI_MODULE_ap-write-api-technical.en.md` |
| Integration (EN) | `docs/PCE_GUI_MODULE_ap-write-api-integration-guide.en.md` |
| Postman collection | `docs/postman/PCE-AP-Write.postman_collection.json` |

---

*文件版本：與 SuiteApp `com.netsuite.pceguitw` AP Write RESTlet 現行實作同步。*
