PCE TW GUI MODULE — 銷項取號/手填單元測試指南
這些測試不連 NetSuite 帳號。test/helpers/amd-loader.js 在 Node 載入 SuiteScript define(),test/helpers/ar-book-allocate-harness.js 則 mock N/record、N/search。
1. 怎麼跑
在專案根目錄:
# 只跑取號/手填 npm run test:ar-book-allocate # 只跑某一檔 node --test test/ar-book-allocate.test.js node --test test/ar-book-allocate-scenarios.test.js # 只跑名稱符合的案例(Node 20+) node --test --test-name-pattern="格式 36" test/ar-book-allocate-scenarios.test.js # 全專案單元測試(含取號) npm test
package.json 的 test:ar-book-allocate 會跑:
| 檔案 | 內容 |
|---|---|
test/ar-book-allocate.test.js | 函式級:配號篩選、下一號、樂觀鎖、既有回歸 |
test/ar-book-allocate-scenarios.test.js | 路徑級:36/自動取號/手填情境 |
2. 預設發票本(不必每次重寫)
loadAllocateLib() 沒傳 searchRows 時,會放一本預設本(DEFAULT_BOOK):
| 欄位 | 預設值 | 意義 |
|---|---|---|
| id | 501 | internal id |
| 統編 | 24536806 | 賣方 |
| 期別 | 11502 | 民國 115 年 1–2 月雙月 |
| 字軌 | CA | |
| 起號/訖號 | 50/99 | 測試裡會補成 8 碼 |
| last_no | 74 | 下一號=CA00000075 |
| 狀態 | 1-使用中(id 10) | |
| 傳至加值中心 | T | 自動取號才用得到 |
狀態 id 對照(mock buildLookupCache):0→8、1→9、2→10、9→11。
3. 新增案例的最小樣板
在 test/ar-book-allocate-scenarios.test.js(或新檔)加上:
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const {
DEFAULT_BOOK,
state,
cloneSearchRow,
defaultAutoCriteria,
loadAllocateLib
} = require('./helpers/ar-book-allocate-harness');
test('一句話說明預期行為', () => {
loadAllocateLib(); // 每個 test 開頭都要呼叫,避免 mock 互相汙染
const result = state.allocateLib.resolveInvoiceNumberForIssue({
docFormatCode: '31',
manualInvoiceNo: 'CA00000060',
criteria: {
taxId: DEFAULT_BOOK.taxId,
period: DEFAULT_BOOK.period,
docDate: '2026/02/01'
}
});
assert.equal(result.ok, true);
assert.equal(result.invoiceNo, 'CA00000060');
});每個 test(...) 都要先 loadAllocateLib(...)。 函式會清掉上一筆的 mock 與 state。
跑過後確認綠燈:
npm run test:ar-book-allocate
4. 三條路徑要測什麼
開立入口一律是 resolveInvoiceNumberForIssue(options)。
state.allocateLib.resolveInvoiceNumberForIssue({
docFormatCode: '35', // '36' / '35' / '31' / '32' …
vacUpload: 'T', // 僅 35 有意義;只認 true 或 'T'
manualInvoiceNo: '', // 表單手填
nowMs: 1234567890123, // 僅 36:可重現的時間戳
criteria: defaultAutoCriteria()
});| 你想測的情況 | docFormatCode | vacUpload | manualInvoiceNo | 預期 |
|---|---|---|---|---|
| 免用統一發票 | 36 | 任意 | 空 | ok,號碼 SEUI+10 碼,不寫本 |
| 36 卻手填 | 36 | 任意 | 有值 | MANUAL_INVOICE_NO_NOT_ALLOWED |
| 電子發票上傳 | 35 | true 或 'T' | 空 | 自動取號 CA00000075 |
| 35 上傳卻手填 | 35 | 'T' | 有值 | MANUAL_INVOICE_NO_NOT_ALLOWED |
| 紙本/35 不上傳沒填 | 31/32/35+'F' | 空 | INVOICE_NO_REQUIRED | |
| 紙本手填 | 31 | CA00000060 | 反查本並回寫 last_no |
也可以直接打內層函式(較精準):
- 自動:
allocateInvoiceNumber(defaultAutoCriteria()) - 手填:
applyManualInvoiceNumber('CA00000060', { taxId, period, docDate })
5. 怎麼改發票本與「已用號碼」
loadAllocateLib(options):
| 參數 | 用途 |
|---|---|
searchRows | mock 搜尋到的發票本。用 cloneSearchRow({ id, values, texts }) 改欄位 |
usedInvoiceRows | 非空就視為號碼已被銷項主檔或 Invoice/CM 使用 |
recordLoadLastNos | 每次 record.load 看到的 last_no(用來測樂觀鎖衝突/重試) |
改一本的欄位
loadAllocateLib({
searchRows: [cloneSearchRow({
values: {
custrecord_pce_ar_book_last_no: '98',
custrecord_pce_ar_book_last_issue_dt: '2026/03/01',
custrecord_pce_ar_book_ns_platform: 'F',
custrecord_pce_ar_book_department: '11'
},
texts: { custrecord_pce_ar_book_status: '0-未使用' }
})]
});cloneSearchRow 會跟預設本合併,只需覆寫要變的欄位。
兩本(測最小 id、多本涵蓋)
loadAllocateLib({
searchRows: [
cloneSearchRow({ id: '501' }),
cloneSearchRow({
id: '502',
values: {
custrecord_pce_ar_book_track: 'CB',
custrecord_pce_ar_book_last_no: '10'
}
})
]
});號碼已使用
loadAllocateLib({ usedInvoiceRows: [{ id: '9001' }] });mock 不會比對號碼內容;只要 usedInvoiceRows 有列,任何非空白號碼都會被當成已用。
樂觀鎖:同一本被搶號
loadAllocateLib({
recordLoadLastNos: ['75', '74'] // 第 1 次 load 看到 75(衝突),第 2 次看到 74(成功)
});6. 斷言時看哪些欄位
成功:
assert.equal(result.ok, true); assert.equal(result.invoiceNo, 'CA00000075'); assert.equal(result.bookId, '501'); assert.equal(result.allocated, true); // 自動取號 assert.equal(result.skipped, true); // 格式 36
失敗:
assert.equal(result.ok, false); assert.equal(result.error.code, 'NO_ELIGIBLE_BOOK'); assert.match(result.error.message, /發票本 501/);
回寫發票本(state 在該次 loadAllocateLib 之後更新):
state | 意義 |
|---|---|
lastBookSaveValues.custrecord_pce_ar_book_last_no | 回寫的流水(8 碼) |
lastBookSaveValues.custrecord_pce_ar_book_last_issue_dt | 回寫日期 |
lastBookSaveValues.custrecord_pce_ar_book_status | 狀態 id(9=使用中、10=使用完畢) |
lastBookLoadId | 最後 record.load 的本 id |
bookLoadCount | load 次數(重試會 > 1) |
lastSearchConfig | 最後一次 search.create(可檢查 filter) |
lastBookSaveValues === null | 沒有回寫本(失敗或格式 36) |
7. 常用錯誤代碼(對應案例)
完整定義見取號文件 §8。寫測試時用 error.code,不要只比中文訊息。
| 代碼 | 建議怎麼造 |
|---|---|
MANUAL_INVOICE_NO_NOT_ALLOWED | 36 或 35+'T' 卻帶 manualInvoiceNo |
INVOICE_NO_REQUIRED | 31/32/35+'F' 且沒填號碼 |
INVALID_INVOICE_NUMBER | 手填 12345678、SEUI…、或本的字軌不是 2 碼英文 |
INVOICE_NO_ALREADY_USED | usedInvoiceRows: [{ id: '1' }] |
BOOK_NOT_FOUND | 手填 CA00000049(不在 50–99) |
BOOK_AMBIGUOUS | 兩本都涵蓋同一號,且沒再篩部門/期別 |
INVOICE_NO_OUT_OF_RANGE | commitManualBookUsage('501', '100', …) |
SELLER_TAX_ID_REQUIRED | 自動取號沒帶 taxId |
DOC_FORMAT_REQUIRED | 自動取號沒帶 docFormatId |
DOC_DATE_PERIOD_REQUIRED | 自動取號沒帶 period,且日期空白或無法解析(先算期別) |
DOC_DATE_INVALID | 已有 period,但 docDate 仍無法解析為日曆日 |
NO_ELIGIBLE_BOOK | 本未勾傳至加值中心,或配號欄有值但表單有填且不同 |
DOC_DATE_BEFORE_LAST_ISSUE | 本的 last_issue_dt 晚於開立日 |
BOOK_EXHAUSTED | last_no 已是訖號或超過 |
BOOK_NUMBER_CONFLICT | recordLoadLastNos 連續與預期下一號不符 |
BOOK_COMMIT_INVALID | commitBookAllocation('', …) 或流水空白 |
8. 建議覆蓋清單(加案例時核對)
分流
- [x] 36 產生 14 字
SEUI;手填拒絕;不寫本 - [x] 35+
T/true自動取號;手填拒絕 - [x] 35+
't'不視為上傳(只認true/'T') - [x] 31/32/35+
F必填手填
自動取號
- [x] 下一號=
last_no+1;空白 last=起號 - [x] 訖號可配,狀態改使用完畢
- [x] 多本只取最小 internal id,失敗不換本
- [x] 須勾選傳至加值中心
- [x] 部門、類別、配號須與發票本完全相同;空白只對空白
- [x] 日期不可早於
last_issue_dt;同日可 - [x] 衝突同一本最多重試 3 次
手填
- [x] 字軌 2+流水(可小寫、連字、不足 8 碼補零)
- [x] 不驗接續
last_no、不驗日期 - [x] 不要求傳至加值中心;狀態 2 仍可反查
- [x] 已用拒絕;找不到/多本涵蓋拒絕
- [x] 手填亦依部門/類別/配號完全相同篩選;空白只對空白
- [x] 成功
last_no=max(既有, 本次);起號可改未使用→使用中;訖號可改使用完畢
新規則出現時:先改 PCE_LIB_AR_BookAllocate.js,再在對應 describe 加一個失敗案例與一個成功案例。
9. 這套測試測不到的事
- 開立 Suitelet/Client Script 畫面(欄位停用、必填提示)
- 真正寫入 NetSuite 的治理、權限、SDF
maxlength - 合併開立時 Name 是否等於發票號碼(在
PCE_LIB_AR_Issue.js,不在本 library)
那些要靠帳號冒煙或另外對 Issue library 寫測試。
10. 相關檔案
| 項目 | 路徑 |
|---|---|
| 取號實作 | src/FileCabinet/SuiteApps/com.netsuite.pceguitw/ar/libraries/PCE_LIB_AR_BookAllocate.js |
| Harness | test/helpers/ar-book-allocate-harness.js |
| AMD 載入 | test/helpers/amd-loader.js |
| 函式測試 | test/ar-book-allocate.test.js |
| 情境測試 | test/ar-book-allocate-scenarios.test.js |