先核來源與已訂規則
本例各份獨立文件可分別完成。D 有標頭、L1=2、L2=−1,E 另有有效行 3。每條已存在明細須有非 NULL 正數數量;D 標頭與明細須一同成功。每份一儲存點,行錯便移除整份 D 暫建。此處明准只得標頭的草稿。
| 紀錄 | 輸入 JSON |
|---|---|
| 原例輸入 | {"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":-1}]},{"id":"E","lines":[{"item":"L1","quantity":3}]}]} |
比較兩條實際路徑
更正保 E 及其明細,留 D 的 275 拒收,沒有 D 碎片。逐行捕錯則會留 D 標頭及 L1。
| 路徑 | 輸出 JSON |
|---|---|
| 錯誤路徑 | {"errors":[{"document":"D","code":275}],"documents":[{"id":"D"},{"id":"E"}],"lines":[{"document_id":"D","item":"L1","quantity":2},{"document_id":"E","item":"L1","quantity":3}]} |
| 更正路徑 | {"errors":[{"document":"D","code":275}],"documents":[{"id":"E"}],"lines":[{"document_id":"E","item":"L1","quantity":3}]} |
import {DatabaseSync} from 'node:sqlite';
import {TextDecoder} from 'node:util';
import {inflateSync} from 'node:zlib';
import {posix} from 'node:path';
import {createHash} from 'node:crypto';
const rows=(db,sql)=>db.prepare(sql).all().map(r=>({...r}));
const failed=(action)=>{try{action();return null;}catch(e){return {code:e.errcode??e.code??e.name};}};
import assert from 'node:assert/strict';
const run=function run(i,corrected=true){
const db=new DatabaseSync(':memory:');try{db.exec('PRAGMA foreign_keys=ON;CREATE TABLE document(id TEXT PRIMARY KEY);CREATE TABLE line(document_id TEXT REFERENCES document(id),item TEXT,quantity INTEGER NOT NULL CHECK(quantity>0));');const errors=[];
for(const d of i.documents){if(corrected)db.exec('SAVEPOINT family;');db.prepare('INSERT INTO document VALUES(?)').run(d.id);
let error=null;for(const l of d.lines){error=failed(()=>db.prepare('INSERT INTO line VALUES(?,?,?)').run(d.id,l.item,l.quantity));if(error){errors.push({document:d.id,code:error.code});if(corrected)break;}}
if(corrected){if(error)db.exec('ROLLBACK TO family;');db.exec('RELEASE family;');}}
return {errors,documents:rows(db,'SELECT * FROM document ORDER BY id'),lines:rows(db,'SELECT * FROM line ORDER BY document_id,item')};
}finally{db.close();}
};
const cases=[{"label":"misleading base","input":{"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":-1}]},{"id":"E","lines":[{"item":"L1","quantity":3}]}]},"expected":{"errors":[{"document":"D","code":275}],"documents":[{"id":"D"},{"id":"E"}],"lines":[{"document_id":"D","item":"L1","quantity":2},{"document_id":"E","item":"L1","quantity":3}]},"corrected":false},{"label":"corrected base","input":{"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":-1}]},{"id":"E","lines":[{"item":"L1","quantity":3}]}]},"expected":{"errors":[{"document":"D","code":275}],"documents":[{"id":"E"}],"lines":[{"document_id":"E","item":"L1","quantity":3}]},"corrected":true},{"label":"complete entity commits","input":{"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":1}]}]},"expected":{"errors":[],"documents":[{"id":"D"}],"lines":[{"document_id":"D","item":"L1","quantity":2},{"document_id":"D","item":"L2","quantity":1}]},"corrected":true},{"label":"permitted header-only draft","input":{"documents":[{"id":"D","lines":[]}]},"expected":{"errors":[],"documents":[{"id":"D"}],"lines":[]},"corrected":true},{"label":"missing line quantity holds its complete family","input":{"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":null}]},{"id":"E","lines":[{"item":"L1","quantity":3}]}]},"expected":{"errors":[{"document":"D","code":1299}],"documents":[{"id":"E"}],"lines":[{"document_id":"E","item":"L1","quantity":3}]},"corrected":true}];
for(const test of cases){
const actual=run(test.input,test.corrected);
assert.deepEqual(actual,test.expected,test.label);
console.log(test.label,actual);
}
下次載入前核邊界
完整有效 D 可提交;明准只得標頭 D 則留標頭、沒有明細。D 已存在明細若欠數量,NOT NULL1299 會令整份 D 回復,獨立有效 E 照留。
| 情況 | 輸入 JSON | 預期輸出 JSON |
|---|---|---|
| 同份文件明細全有效 | {"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":1}]}]} | {"errors":[],"documents":[{"id":"D"}],"lines":[{"document_id":"D","item":"L1","quantity":2},{"document_id":"D","item":"L2","quantity":1}]} |
| 明准只得標頭草稿 | {"documents":[{"id":"D","lines":[]}]} | {"errors":[],"documents":[{"id":"D"}],"lines":[]} |
| 明細欠數量,整份文件暫停 | {"documents":[{"id":"D","lines":[{"item":"L1","quantity":2},{"item":"L2","quantity":null}]},{"id":"E","lines":[{"item":"L1","quantity":3}]}]} | {"errors":[{"document":"D","code":1299}],"documents":[{"id":"E"}],"lines":[{"document_id":"E","item":"L1","quantity":3}]} |
獨立 D 失敗,E 必須一同回復嗎?
查看參考答案
按此文件家族約定不用。整份 D 回復,完整 E 保留,D 未解錯誤另記。