fix(accounting): verify and fix Zarrin driver on real v228 install
Tested 20/20 on the owner's live Zarrin v228 (870 accounts, 656 items): Reads fixed: - listKarSakhte: now pages by 100 via pageNum (was returning only first 100 of 656) - goroohSakhte: HTTP 400 with a valid body is now accepted (was treated as missing) - parvande «مانده از قبل» row is not a balance; skip it and anchor the running balance on the live listMandeHesabha balance, walked back through movements - infogardeshsakhte: hand-printed JSON repaired, timeout raised to 90s - document delete: now searches all ledger pages, not just the first one - listhesabha names are «first * last»; the «*» is now stripped - keep-alive ECONNRESET: a socket hang-up on a reused connection is retried once on a fresh connection (server closes idle connections) Writes fixed (test account 953 «تست اپ گلدبانک»): - temporary flag: set from shomareSanad («موقت»), not noeFactor - noeFactor «فاکتور موقت» also flags a document left open in Zarrin's own UI New: test script apps/api/scripts/test-accounting-zarrin.ts (13 read + 7 write checks) Docs: §13 added to ACCOUNTING-INTEGRATION-PLAN.md; ACC-01 in ISSUES.md updated New: docs/FRESH-SERVER-DEPLOY-AUDIT.md (fresh-server deploy checklist) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
9fc0886a57
commit
8b74c85a2a
@@ -181,23 +181,37 @@ describe('balances and statement', () => {
|
||||
expect(pos[0].goldGrams.toString()).toBe('-75.409');
|
||||
});
|
||||
|
||||
it('folds a running balance from «مانده از قبل»', async () => {
|
||||
it('anchors the running balance on the real balance; «مانده از قبل» is not a movement', async () => {
|
||||
// 1) the range, opening row first (seen on v228: its figures are not a balance)
|
||||
http.mockReturnValueOnce(
|
||||
reply({
|
||||
state: true,
|
||||
detail: 'OK',
|
||||
data: [
|
||||
{ idRadif: '0', idFactor: '0', idHesab: '81', sharh: 'مانده از قبل', vaznTarazooPardakht: '10.0', vaznTarazooDaryaft: '0.0', poolPardakht: '0', poolDaryaft: '0' },
|
||||
{ idRadif: '470', idFactor: '221', idHesab: '81', noeRadif: 'چک', tarikh: '04/05/29', vaznTarazooPardakht: '0.0', vaznTarazooDaryaft: '2.5', poolPardakht: '810000000', poolDaryaft: '0', ayar: '0' },
|
||||
{ idRadif: '0', idFactor: '0', idHesab: '81', sharh: 'مانده از قبل', vaznTarazooPardakht: '10.14', vaznTarazooDaryaft: '10.24', poolPardakht: '5', poolDaryaft: '5' },
|
||||
{ idRadif: '470', idFactor: '221', idHesab: '81', noeRadif: 'چک', noeFactor: 'سند دریافت', tarikh: '04/05/29', vaznTarazooPardakht: '0.0', vaznTarazooDaryaft: '2.5', poolPardakht: '810000000', poolDaryaft: '0', ayar: '0' },
|
||||
{ idRadif: '471', idFactor: '222', idHesab: '81', noeRadif: 'کار ساخته', noeFactor: 'فاکتور موقت', tarikh: '04/05/30', vaznTarazooPardakht: '1.0', vaznTarazooDaryaft: '0.0', poolPardakht: '0', poolDaryaft: '0', ayar: '750' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
// 2) the account's balance now: −9.5 g in Zarrin = owes 9.5
|
||||
http.mockReturnValueOnce(reply({ state: true, msg: 'OK', data: [{ id: '81', geram: '-9.5', pool: '-810000000' }] }));
|
||||
// 3) what happened after the range
|
||||
http.mockReturnValueOnce(
|
||||
reply({ state: true, detail: 'OK', data: [{ idRadif: '0', sharh: 'مانده از قبل' }, { idRadif: '500', idHesab: '81', vaznTarazooPardakht: '1.0', vaznTarazooDaryaft: '0', poolPardakht: '0', poolDaryaft: '0' }] }),
|
||||
);
|
||||
|
||||
const rows = await z.getStatement(s, { code: '81', from: { jy: 1404, jm: 5, jd: 1 }, to: { jy: 1404, jm: 5, jd: 31 } });
|
||||
expect(url().searchParams.get('dateAz')).toBe('2025-07-23');
|
||||
expect(rows[0].goldBalance?.toString()).toBe('10');
|
||||
expect(rows[1]).toMatchObject({ documentRef: '221', date: '1404/05/29', title: 'چک' });
|
||||
expect(rows[1].goldBalance?.toString()).toBe('7.5');
|
||||
expect(rows[1].rialBalance?.toString()).toBe('810000000');
|
||||
expect(url(1).searchParams.get('kind')).toBe('listMandeHesabha');
|
||||
expect(url(2).searchParams.get('dateAz')).toBe('2025-08-23'); // the day after `to`
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows[0]).toMatchObject({ documentRef: '221', date: '1404/05/29', title: 'چک', temporary: false });
|
||||
// closing(to) = 9.5 − 1.0 after = 8.5; opening = 8.5 − (−2.5 + 1.0) = 10
|
||||
expect(rows[0].goldBalance?.toString()).toBe('7.5');
|
||||
expect(rows[0].rialBalance?.toString()).toBe('810000000');
|
||||
expect(rows[1]).toMatchObject({ temporary: true });
|
||||
expect(rows[1].goldBalance?.toString()).toBe('8.5');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -283,3 +297,81 @@ describe('change feed (real-time sync)', () => {
|
||||
expect(await z.getChanges(s, null)).toEqual({ cursor: '731', accountCodes: [], itemNames: [], overflow: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('v228 quirks', () => {
|
||||
it('reads every page of listKarSakhte (100 per page)', async () => {
|
||||
const page = (from: number, n: number) => reply({ state: true, msg: 'ok', data: Array.from({ length: n }, (_, i) => ({ id: String(from + i), namFelezsakhthe: `کار ${from + i}`, fkNoe: '16', NoeName: 'گرمی' })) });
|
||||
http.mockReturnValueOnce(page(1, 100)).mockReturnValueOnce(page(101, 100)).mockReturnValueOnce(page(201, 7));
|
||||
const items = await z.listKarSakhte(s);
|
||||
expect(items).toHaveLength(207);
|
||||
expect(url(1).searchParams.get('pageNum')).toBe('2');
|
||||
});
|
||||
|
||||
it('stops when the server ignores pageNum and repeats page 1', async () => {
|
||||
const page1 = () => reply({ state: true, msg: 'ok', data: Array.from({ length: 100 }, (_, i) => ({ id: String(i + 1), namFelezsakhthe: 'x' })) });
|
||||
http.mockReturnValueOnce(page1()).mockReturnValueOnce(page1());
|
||||
expect(await z.listKarSakhte(s)).toHaveLength(100);
|
||||
expect(http).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('accepts an HTTP 400 that carries a good reply (goroohSakhte on v228)', async () => {
|
||||
http.mockReturnValueOnce(reply({ state: true, detail: 'OK', data: [{ id: '26', name: 'النگو' }] }, 400));
|
||||
expect(await z.listKarSakhteGroups(s)).toEqual([{ id: '26', name: 'النگو' }]);
|
||||
});
|
||||
|
||||
it('repairs the hand-printed JSON of infogardeshsakhte', async () => {
|
||||
// Verbatim shape of a v228 reply.
|
||||
const body = [
|
||||
'{',
|
||||
' "data": [',
|
||||
' {',
|
||||
' "code_kar": 19,',
|
||||
' "nam_kar": دستبند آروشا,',
|
||||
' "tarikh_akharin_gardesh": 05/07/15,',
|
||||
' "sharh_akharin_gardesh": دستبند آروشا,',
|
||||
' "vazn_akharin_gardesh": -15.12,',
|
||||
' "mablagh_akharin_gardesh": 0.0,',
|
||||
' },',
|
||||
' ]',
|
||||
'}',
|
||||
].join('\n');
|
||||
http.mockReturnValueOnce(reply(body));
|
||||
const rows = await z.getKarSakhteMovement(s, { jy: 1405, jm: 7, jd: 1 }, { jy: 1405, jm: 7, jd: 20 });
|
||||
expect(rows).toEqual([
|
||||
expect.objectContaining({ itemCode: '19', itemName: 'دستبند آروشا', date: '1405/07/15', weight: new Prisma.Decimal('-15.12') }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('drops the «*» listhesabha puts between first and last name', async () => {
|
||||
const { accountName } = __zarrinInternals;
|
||||
expect(accountName('آرین * یوسفی')).toBe('آرین یوسفی');
|
||||
expect(accountName(' * هزینه هدایا (پیامک گروهی)')).toBe('هزینه هدایا (پیامک گروهی)');
|
||||
expect(accountName(' * ')).toBeNull();
|
||||
http.mockReturnValueOnce(reply({ data: [{ id: '953', nam: 'تست اپ * گلدبانک', gorooh: 'متفرقه', mobile: '09990001111' }] }));
|
||||
const { items } = await z.listCustomers(s, null);
|
||||
expect(items[0]).toMatchObject({ code: '953', name: 'تست اپ گلدبانک', groupName: 'متفرقه', mobile: '09990001111' });
|
||||
});
|
||||
|
||||
it('sees a document posted with movaghat=1 as temporary (shomareSanad «موقت»)', async () => {
|
||||
http.mockReturnValueOnce(
|
||||
reply({
|
||||
state: true,
|
||||
data: [
|
||||
{ idRadif: '71962', idFactor: '16792', idHesab: '953', noeFactor: 'فاکتور فروش', shomareSanad: 'موقت', noeRadif: 'کار ساخته', vaznTarazooPardakht: '1', vaznTarazooDaryaft: '0', poolPardakht: '0', poolDaryaft: '0' },
|
||||
{ idRadif: '71710', idFactor: '16722', idHesab: '953', noeFactor: 'فاکتور موقت', shomareSanad: 'باز', vaznTarazooPardakht: '0.1', vaznTarazooDaryaft: '0', poolPardakht: '0', poolDaryaft: '0' },
|
||||
{ idRadif: '52961', idFactor: '12282', idHesab: '953', noeFactor: 'فاکتور فروش', shomareSanad: '12282', vaznTarazooPardakht: '1', vaznTarazooDaryaft: '0', poolPardakht: '0', poolDaryaft: '0' },
|
||||
],
|
||||
}),
|
||||
);
|
||||
http.mockReturnValueOnce(reply({ state: true, data: [{ id: '953', geram: '-2.1', pool: '0' }] }));
|
||||
const today = toJalaliToday();
|
||||
const rows = await z.getStatement(s, { code: '953', from: today, to: today });
|
||||
expect(rows.map((r) => r.temporary)).toEqual([true, true, false]);
|
||||
});
|
||||
});
|
||||
|
||||
function toJalaliToday() {
|
||||
// Today in Tehran, so the statement needs no "after the range" call.
|
||||
const { toJalali } = jest.requireActual('../jalali');
|
||||
return toJalali(new Date());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user