Explaining the ERP cards and their entries led to a separate question: what must be checked before adding the numbers? I wrote a small validator that does not disguise a missing amount as zero.
This is newly written synthetic integer-KRW code, not extracted client software. It has no dependency on a screen, server or database. The example below exposes both a normal result and the reasons a total may need to wait.
Only return complete totals when every row is reviewable
Input is a list of rows with an id, amount and settlements array. Monetary values must be nonnegative safe integers in Korean won. An empty settlement array means a known empty list; a missing array means the settlement input has not been established. A stated zero amount differs from an absent amount. Avoiding silent correction is the beginning of this contract.
The result contains valid, totals, per-row details and issues. Any issue makes valid false and totals null. A screen may still show the known row details, but should not label a valid subset as the complete ledger. When connecting a UI, preserve the distinction: null is a review-required state, not another way to display zero.
Do not silently discard a questionable row
Identifiers use one to forty ASCII letters, digits, underscores or hyphens. Repeated identifiers block totals until the duplication is understood. In a real service, the same local number might legitimately occur at separate branches. In that case the caller should first define a branch-aware identifier. I do not ask the arithmetic function to guess the identity of a business transaction.
Settlements greater than the amount produce an over_settled issue. That code does not accuse anyone of an improper transaction. An advance or allocation difference could require a separate business policy. This module does not implement that policy; it makes the review boundary visible. Removing a questionable row and returning the remaining sum as if it were complete would conceal the very question that needs answering.
Check the accumulated values as well as the inputs
Individually safe integers can exceed the safe range when added together. The implementation checks individual settlements, the accumulated settlement amount and the ledger’s accumulated amounts, settlements and balances. Numeric strings and fractions are not automatically converted. If a service accepts text entry, parsing that text under an explicit rule should be a separate responsibility before calling this validator.
There can be at most 10,000 rows and 10,000 settlements per row. Those bounds do not replace a server request-size limit, authorization or database constraints. A production API needs its own limits on body size and processing time. Keeping tax, foreign exchange and return allocation outside this small module makes its actual responsibility easier to inspect.
Explore the implementation
Complete independent synthetic validator
// Independent synthetic teaching module; integer KRW only, no network or storage.
const money = n => Number.isSafeInteger(n) && n >= 0;
export function reconcileLedger(rows) {
if (!Array.isArray(rows) || rows.length > 10000) throw new TypeError('Expected at most 10000 rows');
const issues = [], details = [], seen = new Set();
let amount = 0, settled = 0, balance = 0;
for (let index = 0; index < rows.length; index++) {
const row = rows[index], codes = [];
if (!row || typeof row !== 'object' || Array.isArray(row)) {
issues.push({index, code:'row_shape'}); continue;
}
const validId = typeof row.id === 'string' && /^[A-Za-z0-9_-]{1,40}$/.test(row.id);
if (!validId) codes.push('invalid_id');
else if (seen.has(row.id)) codes.push('duplicate_id');
if (validId) seen.add(row.id);
if (!money(row.amount)) codes.push('unknown_or_invalid_amount');
if (!Array.isArray(row.settlements) || row.settlements.length > 10000) codes.push('invalid_settlements');
let paid = 0;
if (Array.isArray(row.settlements) && row.settlements.length <= 10000) {
for (const value of row.settlements) {
if (!money(value)) { codes.push('invalid_settlement'); break; }
if (!Number.isSafeInteger(paid + value)) { codes.push('settlement_overflow'); break; }
paid += value;
}
}
if (!codes.length && paid > row.amount) codes.push('over_settled');
const outstanding = codes.length ? null : row.amount - paid;
details.push({index, id:validId ? row.id : null, amount:money(row.amount) ? row.amount : null,
settled:codes.length ? null : paid, balance:outstanding, valid:!codes.length});
for (const code of codes) issues.push({index, code});
if (!codes.length) {
if (![amount + row.amount, settled + paid, balance + outstanding].every(Number.isSafeInteger)) {
issues.push({index, code:'total_overflow'});
} else { amount += row.amount; settled += paid; balance += outstanding; }
}
}
return {valid:!issues.length, totals:issues.length ? null : {amount, settled, balance}, details, issues};
}
export function syntheticRows() {
return [
{id:'S01', amount:24000000, settlements:[10000000,6800000]},
{id:'S02', amount:18000000, settlements:[12600000]},
{id:'S03', amount:6000000, settlements:[6000000]}
];
}Compare a normal result with a review-required result
The demo lets readers select a valid synthetic ledger, a missing amount, a duplicate, overcollection or aggregate integer overflow. Every row is a fixed teaching fixture. It does not upload a company file or accept operational records. The normal case totals KRW 48 million in amounts, KRW 35.4 million settled and KRW 12.6 million outstanding. The invalid cases expose null totals instead.
Korean and English demos explain the same model. Detailed issue codes retain identical machine-readable names in both languages. An operational UI should translate those codes into an understandable sentence and a place to correct the record. The demonstration recalculates from a changed fixture and writes the result with textContent. It makes no external request and persists no records.
Check the package before adapting it
The ZIP contains the implementation, independent tests, Korean and English standalone HTML demos, documented boundaries and explicit reuse permission. Extract it and run node test.mjs before adapting the code. The HTML inlines its implementation and can be opened without a server. No external library installation is required. Modification, redistribution and commercial reuse are permitted, with the example provided without warranty.
I ran the extracted package’s 21 test groups and compared 861 combinations of small amounts and partial settlements. The bilingual demo event wiring was evaluated separately. These are code-level checks, not rendered-browser or physical-device testing. Production adoption still requires dates, recognition policy, refunds, advances, currency rules, permissions, concurrent edits and storage. This validator owns only the monetary input and aggregation boundary within that larger ERP.