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:
s.esmaeili
2026-10-12 15:23:24 +03:30
co-authored by Claude Sonnet 4.6
parent 9fc0886a57
commit 8b74c85a2a
7 changed files with 574 additions and 50 deletions
@@ -86,6 +86,16 @@ function tlsErrorMessage(err: NodeJS.ErrnoException, pinned: boolean): string {
}
export function httpRequest(req: HttpRequest): Promise<HttpResponse> {
return send(req, false);
}
/**
* One attempt. `fresh` = on a new connection: a server that closes idle
* kept-alive connections (Zarrin v228 does) makes the next request on a pooled
* one fail with ECONNRESET / "socket hang up" before it was received. That one
* case is repeated once on a fresh connection; nothing else is ever retried.
*/
function send(req: HttpRequest, fresh: boolean): Promise<HttpResponse> {
let url: URL;
try {
url = new URL(req.url);
@@ -106,6 +116,7 @@ export function httpRequest(req: HttpRequest): Promise<HttpResponse> {
port: url.port || (isHttps ? 443 : 80),
path: `${url.pathname}${url.search}`,
headers,
...(fresh ? { agent: false } : {}),
};
const pins = isHttps ? pinnedFingerprints(req.ca) : new Set<string>();
if (isHttps) {
@@ -176,6 +187,10 @@ export function httpRequest(req: HttpRequest): Promise<HttpResponse> {
}, req.timeoutMs);
request.on('error', (err: NodeJS.ErrnoException) => {
if (!fresh && request.reusedSocket && err.code === 'ECONNRESET') {
finish(() => resolve(send(req, true)));
return;
}
// Node's own text for these suggests flags that do not apply here.
const text = isHttps && err.code && TLS_CERT_ERRORS.test(err.code) ? tlsErrorMessage(err, pins.size > 0) : err.message;
finish(() => reject(new AccountingHttpError(text, connected, err.code)));