Ctrl K

PCE AP Write API — Technical & Testing Guide (Internal)

Audience: Internal engineers, implementation consultants, QA
Scope: Architecture, deployment, validation chain, troubleshooting, unit/integration testing
Related: Integration guide (vendors) · 中文版 · Postman collection

1. Service overview

ItemValue
PurposeExternal systems write Taiwan AP e-invoice records (customrecord_pce_ap_invoice) and/or AP transactions via RESTlet
Scriptcustomscript_pce_rl_ap_write
Deploymentcustomdeploy_pce_rl_ap_write
Entryap/restlets/PCE_RL_AP_Write.js
Payload contractap/libraries/PCE_LIB_AP_WritePayload.js
Validationap/libraries/PCE_LIB_AP_Validate.js (before write + UE beforeSubmit)
Call logcustomrecord_pce_ap_write_log (auto-saves request/response; includes rejected GETs)
HTTPPOST only (GET returns PCE_METHOD_NOT_ALLOWED)

1.1 Actions

actionBehavior
apInvoiceOnlyCreate AP invoice(s) only; must not include a transaction to create
transactionOnlyCreate or reference a transaction; must not include apInvoices
transactionWithApInvoicesResolve/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.

FieldScript IDDescription
Methodcustrecord_pce_apwl_methodGET (should be rejected) / POST
Actioncustrecord_pce_apwl_actionPOST action
Request IDcustrecord_pce_apwl_request_idIdempotency key requestId
Successcustrecord_pce_apwl_successResponse success
Error Codecustrecord_pce_apwl_error_codeerror.code on failure
Duration (ms)custrecord_pce_apwl_duration_msElapsed time
Governance (units)custrecord_pce_apwl_governanceSuiteScript governance units used by this API call (measured before writing the log row)
Usercustrecord_pce_apwl_userExecuting user
Requestcustrecord_pce_apwl_requestRequest JSON (truncated if long)
Responsecustrecord_pce_apwl_responseResponse 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

  1. RESTlet: buildValidatedInvoiceData → validateRecord; no save on failure.
  2. UE: On record.save, beforeSubmit validates 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/* (see deploy.xml)
  • After deploy, confirm Script Deployment Released and isdeployed = T

3.2 Permissions & integration

CheckNotes
Integration ApplicationOAuth 2.0 (prefer M2M Client Credentials; Authorization Code OK for testing)
ScopeAt least RESTlets
RoleRESTlet execute; create/edit AP custom record; create/edit target transactions; bind this role in M2M mapping
AudienceDeployment currently allroles = T; tighten for production
Log levelDefault 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

RangeMeaning
0000–0099Success (0000 OK, 0001 validateOnly, 0002 idempotent replay, 0003 with warnings, 0004 partial)
1000–1999Payload shape
2000–2999AP validation
3000–3999Lookup
4000–4999NetSuite runtime
9000–9999Unexpected

4.1 Payload shape (before business try)

codereturnCodeWhen
INVALID_BODY1001Body is not an object
INVALID_ACTION1002Illegal action
REQUEST_ID_REQUIRED1003Missing requestId
REQUEST_ID_INVALID1004Invalid requestId format
REQUEST_ID_PAYLOAD_MISMATCH1005Same requestId, different payload fingerprint
AP_INVOICES_REQUIRED1006Invoice array required but empty
AP_INVOICES_NOT_ALLOWED1007transactionOnly includes invoices
AP_INVOICES_TOP_LEVEL_NOT_ALLOWED1008Top-level invoice keys on transactionWithApInvoices
TRANSACTION_REQUIRED1009Missing transaction.id / type
TRANSACTION_NOT_ALLOWED1010apInvoiceOnly includes create-transaction payload
INVALID_TRANSACTION_TYPE1011Type not in allow-list
TRANSACTION_LIMIT_EXCEEDED1012More than max transactions (10)
CLIENT_REF_INVALID1013Invalid clientRef
PCE_METHOD_NOT_ALLOWED1014Unsupported HTTP method
PURCHASE_ORDER_NOT_ALLOWED1015purchaseOrderId on a non-vendorbill type
PURCHASE_ORDER_WITH_EXISTING_ID1016Both id and purchaseOrderId present
PO_ITEM_LINES_REQUIRED1017Missing non-empty itemLines when billing from PO
PO_ORDERLINE_REQUIRED1018Item line missing orderline / line
PO_QUANTITY_REQUIRED1019Item 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 / codereturnCodeNotes
PCE_AP_INVOICE_VALIDATION_ERROR2001Validate failed; error.details = per-field errors (also on results[i].error)
BATCH_FAILED2100All leaves in the batch failed (multi-item)
PARTIAL_SUCCESS0004Partial success; partialSuccess: true, still returns results[]
PCE_LOOKUP_RESOLVE_ERROR3001Code/id not found in lookup
PCE_INVALID_TRANSACTION_TYPE3002Type not mapped to record.Type on create
SKIPPED_PARENT_FAILED4001Nested invoices skipped after parent TX failed
PO_ORDERLINE_NOT_FOUND4002Request orderline not on transformed VB
PO_TRANSFORM_FAILED4003PO→VB transform / save failed
Native NetSuite errors4000Permissions, mandatory fields, save failures
UNEXPECTED_ERROR9999Exception 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
FileCoverage
test/ap-write-payload.test.jsaction / tx type / alias / shape
test/ap-write-return-code.test.jsreturnCode mapping
test/ap-csv.test.jsCSV parse / headers / 100-row limit
test/ap-validate.test.jsAP validation rules (MOF)
test/helpers/amd-loader.jsSuiteScript AMD → Node

Contract changes: update PCE_LIB_AP_WritePayload.js + unit tests first, then RESTlet and docs.

5.2 Account smoke tests (consultants / QA)

  1. GET → expect success: false, error.code: PCE_METHOD_NOT_ALLOWED (GET is not allowed).
  2. POST validateOnly: true + valid apInvoiceOnly → no new records.
  3. POST bad tax ID / track → PCE_AP_INVOICE_VALIDATION_ERROR + details.
  4. POST real apInvoiceOnly write → confirm in UI.
  5. POST transactionWithApInvoices → confirm VB/ER/VC/Prepayment parent + child list.
  6. Attach a second invoice via existing transaction.id → verify 1:N.
  7. (Optional) With a usable PO: vendorbill + purchaseOrderId + itemLines[].orderline / quantity → confirm partial billing and unlisted lines at quantity 0.

5.3 Postman / curl

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)

TypeResultInternal ID
Vendor Bill✅ Pass25647
Expense Report✅ Pass25648
Vendor Credit✅ Pass25649
Vendor Prepayment✅ Pass25551

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.)

TypeResultInternal IDUI verification
Vendor Bill✅ Pass25747Bill #188, Lancome, $1,050, Pending Approval
Expense Report✅ Pass25847Employee Abigail Khan
Vendor Credit✅ Pass25848Bill Credit #15, browser-smoke memo
Vendor Prepayment✅ Pass25947VP #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.

StepItemResultNotes
5.4Fixtures✅vendor 944 / emp 1086 / bank 422
1GET not allowed✅/⚠️Expect PCE_METHOD_NOT_ALLOWED
2apInvoiceOnly + validateOnly✅no create
3aBad tax ID✅PCE_AP_INVOICE_VALIDATION_ERROR
3bBad track AA✅GuiTrackNotAllowed; allowed CA, CB
4Real apInvoiceOnly✅apInvoiceId 27 (CA26632189)
5.1–5.4transactionWithApInvoices VB/ER/VC/VP✅txs 26149–26152 + invoices
62nd 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 + validateOnly POST instead of a health GET).
  • Vendor Prepayment needs a Bank account (not AP); set account / payment after subsidiary / entity (sourcing clears early values).
  • Smoke Suitelet must skip vendors with internal id ≤ 0 (e.g. -3) or NetSuite returns INVALID_FLD_VALUE.

6. Troubleshooting

SymptomLikely causeAction
401 UnauthorizedInvalid / expired / missing Bearer tokenRefresh via token endpoint; confirm scope restlets
403 / INSUFFICIENT_PERMISSIONRole missing rightsAdd custom record / transaction permissions; check M2M role mapping
PCE_LOOKUP_RESOLVE_ERRORMissing lookup or bad codeCheck Lookup + install data
Validate OK but save failsUE / NS mandatory / sourcingExecution Log; compare same data in UI
warnings with successTax differs by 1–5 (soft warning)Expected; see validation rules
Shape OK, no createvalidateOnly: trueSet false
apInvoiceOnly with transactionShape blocks TRANSACTION_NOT_ALLOWEDUse transactionWithApInvoices
No call log rowsRole missing Create on log recordGrant 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

  1. Transaction fields are not PCE-validated (including custom fields) — keys under transaction.fields / expenseLines / itemLines go straight to setValue / setCurrentSublistValue with 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.
  2. transactionOnly + validateOnly — shape only.
  3. Existing AP invoices stay unchanged. id / externalId on an invoice payload are ignored. This API does not update a PCE AP invoice that already exists.
  4. No delete/void via this API.
  5. Duplicate invoice concurrency — Validate checks duplicates; race possible under load.
  6. requestId idempotency — looks up successful writes in the call log; custom records have no true unique constraint, so extreme concurrent same-requestId races remain possible (serial retries + UUID are fine in practice).
  7. Legacy script rename — retire old BPM-named deployments if present; use customscript_pce_rl_ap_write.

8. Change impact (consultants)

ChangeImpact
New aliasLow — payload lib + tests + integration guide
Action semanticsHigh — breaking; version & notify vendors
Stricter ValidateMedium–high — dry-run with validateOnly first
Missing lookup dataHigh — immediate PCE_LOOKUP_RESOLVE_ERROR