PCE AP Write API — Technical & Testing Guide (Internal)
Scope: Architecture, deployment, validation chain, troubleshooting, unit/integration testing
Related: Integration guide (vendors) · 中文版 · Postman collection
1. Service overview
| Item | Value |
|---|---|
| Purpose | External systems write Taiwan AP e-invoice records (customrecord_pce_ap_invoice) and/or AP transactions via RESTlet |
| Script | customscript_pce_rl_ap_write |
| Deployment | customdeploy_pce_rl_ap_write |
| Entry | ap/restlets/PCE_RL_AP_Write.js |
| Payload contract | ap/libraries/PCE_LIB_AP_WritePayload.js |
| Validation | ap/libraries/PCE_LIB_AP_Validate.js (before write + UE beforeSubmit) |
| Call log | customrecord_pce_ap_write_log (auto-saves request/response; includes rejected GETs) |
| HTTP | POST only (GET returns PCE_METHOD_NOT_ALLOWED) |
1.1 Actions
| action | Behavior |
|---|---|
apInvoiceOnly | Create AP invoice(s) only; must not include a transaction to create |
transactionOnly | Create or reference a transaction; must not include apInvoices |
transactionWithApInvoices | Resolve/create transaction, then create one or more AP invoices linked as children |
This package accepts only the three actions above. stageApIntake, updateApIntake, createPurchaseReq, and updatePurchaseReq return INVALID_ACTION. API intake review and purchase requests are not in this package.
Supported transaction types: vendorbill \| expensereport \| vendorcredit \| vendorprepayment.
2. Code layout
ap/
restlets/
PCE_RL_AP_Write.js # RESTlet entry (writes API call log each request)
suitelets/
PCE_SL_ApWriteSmoke.js # AP Write smoke Suitelet
PCE_SL_AP_CsvImport.js # CSV import Suitelet (POST→GET PRG)
libraries/
PCE_LIB_AP_CsvImportReplay.js # replay CSV result UI from write log
PCE_LIB_AP_WriteReturnCode.js # returnCode mapping and applyReturnCode
PCE_LIB_AP_WritePayload.js # action / shape / alias normalization
PCE_LIB_AP_InvoiceSave.js # validate and save the official AP invoice
PCE_LIB_AP_WriteDfo.js # no-op in the public package; a customer branch replaces it
PCE_LIB_AP_Validate.js # AP business rules (MOF-aligned)
PCE_LIB_AP_Lookup.js # format / tax / deduction / track code↔id
PCE_LIB_AP_Period.js # ROC YYYMM / filing period
PCE_LIB_AP_WriteLog.js # persist customrecord_pce_ap_write_log
PCE_AP_Fields.js # field script id constants
clientscripts/ # UI (separate from this API)
userevents/
PCE_UE_APInvoice_DefaultValues.js # beforeSubmit → Validate
common/
PCE_LIB_TaxId.js # Taiwan tax ID check (used by Validate)2.0 API call log (customrecord_pce_ap_write_log)
Every POST (and rejected GET) creates one log row after the handler finishes. Log write failures are recorded only in the Execution Log and do not change the API response.
| Field | Script ID | Description |
|---|---|---|
| Method | custrecord_pce_apwl_method | GET (should be rejected) / POST |
| Action | custrecord_pce_apwl_action | POST action |
| Request ID | custrecord_pce_apwl_request_id | Idempotency key requestId |
| Success | custrecord_pce_apwl_success | Response success |
| Error Code | custrecord_pce_apwl_error_code | error.code on failure |
| Duration (ms) | custrecord_pce_apwl_duration_ms | Elapsed time |
| Governance (units) | custrecord_pce_apwl_governance | SuiteScript governance units used by this API call (measured before writing the log row) |
| User | custrecord_pce_apwl_user | Executing user |
| Request | custrecord_pce_apwl_request | Request JSON (truncated if long) |
| Response | custrecord_pce_apwl_response | Response JSON (truncated if long) |
UI: Lists → Custom → PCE AP Write 呼叫紀錄. After deploy, grant the integration role Create on this custom record.
2.1 POST flow
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: if transaction.id is set, load the existing record; else if type=vendorbill and purchaseOrderId is set, record.transform (purchaseorder → vendorbill) and overwrite quantities from itemLines[].orderline / quantity (unlisted PO lines → quantity 0); otherwise use existing record.create + fields/lines.
2.2 Dual validation gates
- RESTlet:
buildValidatedInvoiceData→validateRecord; no save on failure. - UE: On
record.save,beforeSubmitvalidates again (CSV / WS / UI / RESTLET depending on deployment context).
Field rules (required, forbidden, one-of, tax tolerance, track, period, error codes) are in AP validation rules.
Select fields (format / tax type / deduction / summary mark) accept a business code (e.g. "21") or an internal id, resolved via resolveSelectValue + Lookup. AP formats 21 and 25 each have two lookup rows; send invCatCode or docFormatName when using the code alone.
3. Deployment & environment
3.1 SDF / SuiteCloud
- Script object:
src/Objects/Scripts/customscript_pce_rl_ap_write.xml - FileCabinet:
ap/restlets/,ap/suitelets/,ap/libraries/*(seedeploy.xml) - After deploy, confirm Script Deployment Released and
isdeployed = T
3.2 Permissions & integration
| Check | Notes |
|---|---|
| Integration Application | OAuth 2.0 (prefer M2M Client Credentials; Authorization Code OK for testing) |
| Scope | At least RESTlets |
| Role | RESTlet execute; create/edit AP custom record; create/edit target transactions; bind this role in M2M mapping |
| Audience | Deployment currently allroles = T; tighten for production |
| Log level | Default DEBUG; use ERROR/AUDIT in production |
3.3 External URL
Customization → Scripting → Script Deployments → PCE RL AP Write → copy External URL.
https://{accountId}.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script={scriptId}&deploy={deployId}Account ID and numeric script/deploy IDs are environment-specific (scriptid strings ≠ URL numeric IDs).
4. Error codes (technical)
Implementation: ap/libraries/PCE_LIB_AP_WriteReturnCode.js; RESTlet ok() / fail() and the end of enrichResponseWithRequest() call applyReturnCode().
4.0 returnCode ranges
| Range | Meaning |
|---|---|
0000–0099 | Success (0000 OK, 0001 validateOnly, 0002 idempotent replay, 0003 with warnings, 0004 partial) |
1000–1999 | Payload shape |
2000–2999 | AP validation |
3000–3999 | Lookup |
4000–4999 | NetSuite runtime |
9000–9999 | Unexpected |
4.1 Payload shape (before business try)
| code | returnCode | When |
|---|---|---|
INVALID_BODY | 1001 | Body is not an object |
INVALID_ACTION | 1002 | Illegal action |
REQUEST_ID_REQUIRED | 1003 | Missing requestId |
REQUEST_ID_INVALID | 1004 | Invalid requestId format |
REQUEST_ID_PAYLOAD_MISMATCH | 1005 | Same requestId, different payload fingerprint |
AP_INVOICES_REQUIRED | 1006 | Invoice array required but empty |
AP_INVOICES_NOT_ALLOWED | 1007 | transactionOnly includes invoices |
AP_INVOICES_TOP_LEVEL_NOT_ALLOWED | 1008 | Top-level invoice keys on transactionWithApInvoices |
TRANSACTION_REQUIRED | 1009 | Missing transaction.id / type |
TRANSACTION_NOT_ALLOWED | 1010 | apInvoiceOnly includes create-transaction payload |
INVALID_TRANSACTION_TYPE | 1011 | Type not in allow-list |
TRANSACTION_LIMIT_EXCEEDED | 1012 | More than max transactions (10) |
CLIENT_REF_INVALID | 1013 | Invalid clientRef |
PCE_METHOD_NOT_ALLOWED | 1014 | Unsupported HTTP method |
PURCHASE_ORDER_NOT_ALLOWED | 1015 | purchaseOrderId on a non-vendorbill type |
PURCHASE_ORDER_WITH_EXISTING_ID | 1016 | Both id and purchaseOrderId present |
PO_ITEM_LINES_REQUIRED | 1017 | Missing non-empty itemLines when billing from PO |
PO_ORDERLINE_REQUIRED | 1018 | Item line missing orderline / line |
PO_QUANTITY_REQUIRED | 1019 | Item line missing a valid quantity |
1020–1027 are reserved for API intake review and purchase requests. This package does not return them.
4.2 Runtime (catch → fail)
| name / code | returnCode | Notes |
|---|---|---|
PCE_AP_INVOICE_VALIDATION_ERROR | 2001 | Validate failed; error.details = per-field errors (also on results[i].error) |
BATCH_FAILED | 2100 | All leaves in the batch failed (multi-item) |
PARTIAL_SUCCESS | 0004 | Partial success; partialSuccess: true, still returns results[] |
PCE_LOOKUP_RESOLVE_ERROR | 3001 | Code/id not found in lookup |
PCE_INVALID_TRANSACTION_TYPE | 3002 | Type not mapped to record.Type on create |
SKIPPED_PARENT_FAILED | 4001 | Nested invoices skipped after parent TX failed |
PO_ORDERLINE_NOT_FOUND | 4002 | Request orderline not on transformed VB |
PO_TRANSFORM_FAILED | 4003 | PO→VB transform / save failed |
| Native NetSuite errors | 4000 | Permissions, mandatory fields, save failures |
UNEXPECTED_ERROR | 9999 | Exception without name |
Partial success: processApInvoices / multi-transaction loops catch per item and continue. findSuccessfulWriteByRequestId also treats returnCode=0004 / partialSuccess as committed to avoid duplicate creates on retry.
Response shape always includes returnCode / returnMessage:
{
"success": false,
"returnCode": "2001",
"returnMessage": "AP_INVOICE_VALIDATION_FAILED",
"error": {
"code": "PCE_AP_INVOICE_VALIDATION_ERROR",
"message": "...",
"details": null
}
}5. Testing strategy
5.1 Unit tests (local, no NetSuite)
npm test npm run test:ap-validate npm run test:ap-write-payload npm run test:ap-csv
| File | Coverage |
|---|---|
test/ap-write-payload.test.js | action / tx type / alias / shape |
test/ap-write-return-code.test.js | returnCode mapping |
test/ap-csv.test.js | CSV parse / headers / 100-row limit |
test/ap-validate.test.js | AP validation rules (MOF) |
test/helpers/amd-loader.js | SuiteScript AMD → Node |
Contract changes: update PCE_LIB_AP_WritePayload.js + unit tests first, then RESTlet and docs.
5.2 Account smoke tests (consultants / QA)
- GET → expect
success: false,error.code: PCE_METHOD_NOT_ALLOWED(GET is not allowed). - POST
validateOnly: true+ validapInvoiceOnly→ no new records. - POST bad tax ID / track →
PCE_AP_INVOICE_VALIDATION_ERROR+details. - POST real
apInvoiceOnlywrite → confirm in UI. - POST
transactionWithApInvoices→ confirm VB/ER/VC/Prepayment parent + child list. - Attach a second invoice via existing
transaction.id→ verify 1:N. - (Optional) With a usable PO:
vendorbill+purchaseOrderId+itemLines[].orderline/quantity→ confirm partial billing and unlisted lines at quantity 0.
5.3 Postman / curl
- Import
docs/postman/PCE-AP-Write.postman_collection.json - Run 0. Auth → Get Access Token first; collection auth = Bearer
{{accessToken}} - M2M JWT helper:
docs/postman/generate-client-assertion.js - Content-Type:
application/json validateOnlyontransactionOnlychecks shape only, not NetSuite transaction mandatories
5.4 Test data checklist
- [x] Lookup: formats 21–29, tax type, deduction, summary mark (Round C)
- [x] Track table populated for period/format (11508 / format 21: CA, CB)
- [x] Vendor / subsidiary / account (Round C fixtures)
- [x] UE deployed with RESTLET execution context
- [x] Client scripts do not affect RESTlet path (expected; RESTlet writes OK)
5.5 Account smoke results (transactionOnly)
Account: TD2905095 (Chesley's Dev Environment). RESTlet: customscript_pce_rl_ap_write / customdeploy_pce_rl_ap_write. Action: transactionOnly (records actually created). Primary fixtures: 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)
| Type | Result | Internal ID |
|---|---|---|
| Vendor Bill | ✅ Pass | 25647 |
| Expense Report | ✅ Pass | 25648 |
| Vendor Credit | ✅ Pass | 25649 |
| Vendor Prepayment | ✅ Pass | 25551 |
Raw JSON: test/results/ap-write-smoke-result.json
Round B — Browser MCP + Suitelet smoke (2026-08-04)
Entry: /app/site/hosting/scriptlet.nl?script=customscript_pce_sl_ap_write_smoke&deploy=customdeploy_pce_sl_ap_write_smoke&action=run (Suitelet calls the production AP Write RESTlet via https.requestRestlet under the logged-in session.)
| Type | Result | Internal ID | UI verification |
|---|---|---|---|
| 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, browser-smoke memo |
| Vendor Prepayment | ✅ Pass | 25947 | VP #26, PAID, $1,050, US East Checking |
Raw JSON: test/results/ap-write-browser-smoke-result.json
Round C — Browser MCP full checklist §5.2 (2026-08-04)
Entry: …&action=checklist Local unit tests: npm test → 49/49 Pass.
| Step | Item | Result | Notes |
|---|---|---|---|
| 5.4 | Fixtures | ✅ | vendor 944 / emp 1086 / bank 422 |
| 1 | GET not allowed | ✅/⚠️ | Expect PCE_METHOD_NOT_ALLOWED |
| 2 | apInvoiceOnly + validateOnly | ✅ | no create |
| 3a | Bad tax ID | ✅ | PCE_AP_INVOICE_VALIDATION_ERROR |
| 3b | Bad track AA | ✅ | GuiTrackNotAllowed; allowed CA, CB |
| 4 | Real apInvoiceOnly | ✅ | apInvoiceId 27 (CA26632189) |
| 5.1–5.4 | transactionWithApInvoices VB/ER/VC/VP | ✅ | txs 26149–26152 + invoices |
| 6 | 2nd invoice on existing transaction.id | ✅ | tx 26149 + inv 131 (1:N in UI) |
JSON: test/results/ap-write-checklist-result.json
Fixes during test: checkbox fileBizTax as boolean; docDate parsed via N/format to Date.
Notes
- GET is not allowed; expect
PCE_METHOD_NOT_ALLOWED(use docs +validateOnlyPOST instead of a health GET). - Vendor Prepayment needs a Bank account (not AP); set
account/paymentaftersubsidiary/entity(sourcing clears early values). - Smoke Suitelet must skip vendors with internal id ≤ 0 (e.g.
-3) or NetSuite returnsINVALID_FLD_VALUE.
6. Troubleshooting
| Symptom | Likely cause | Action |
|---|---|---|
| 401 Unauthorized | Invalid / expired / missing Bearer token | Refresh via token endpoint; confirm scope restlets |
| 403 / INSUFFICIENT_PERMISSION | Role missing rights | Add custom record / transaction permissions; check M2M role mapping |
PCE_LOOKUP_RESOLVE_ERROR | Missing lookup or bad code | Check Lookup + install data |
| Validate OK but save fails | UE / NS mandatory / sourcing | Execution Log; compare same data in UI |
warnings with success | Tax differs by 1–5 (soft warning) | Expected; see validation rules |
| Shape OK, no create | validateOnly: true | Set false |
apInvoiceOnly with transaction | Shape blocks TRANSACTION_NOT_ALLOWED | Use transactionWithApInvoices |
| No call log rows | Role missing Create on log record | Grant customrecord_pce_ap_write_log; check PCE AP Write log failed |
Execution Log keywords: PCE AP Write RESTlet failed, PCE AP Write log failed. Prefer PCE AP Write 呼叫紀錄 Request/Response when debugging.
7. Known limitations
- Transaction fields are not PCE-validated (including custom fields) — keys under
transaction.fields/expenseLines/itemLinesgo straight tosetValue/setCurrentSublistValuewith no whitelist. Standard and custom fields (custbody_/custcol_, etc.) are pass-through; mandatories, types, and permissions are enforced by NetSuite / the customer’s form. See integration guide §6.0. transactionOnly+validateOnly— shape only.- Existing AP invoices stay unchanged.
id/externalIdon an invoice payload are ignored. This API does not update a PCE AP invoice that already exists. - No delete/void via this API.
- Duplicate invoice concurrency — Validate checks duplicates; race possible under load.
- requestId idempotency — looks up successful writes in the call log; custom records have no true unique constraint, so extreme concurrent same-
requestIdraces remain possible (serial retries + UUID are fine in practice). - Legacy script rename — retire old BPM-named deployments if present; use
customscript_pce_rl_ap_write.
8. Change impact (consultants)
| Change | Impact |
|---|---|
| New alias | Low — payload lib + tests + integration guide |
| Action semantics | High — breaking; version & notify vendors |
| Stricter Validate | Medium–high — dry-run with validateOnly first |
| Missing lookup data | High — immediate PCE_LOOKUP_RESOLVE_ERROR |