feat: product design surveys, cart expiry/buy-later, and catalog polish

Add like/dislike surveys with majority alerts for manufacturing decisions, clearer accounting mapping copy, and related cart/catalog admin-app improvements.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mahmood
2026-11-12 17:55:52 +03:30
co-authored by Cursor
parent 032fa800d9
commit 7dd74ff4f7
74 changed files with 6351 additions and 893 deletions
@@ -1,5 +1,6 @@
import { Prisma } from '@prisma/client';
import { AccountingHttpError, httpRequest, parseLooseJson } from '../accounting-http';
import { accountingDebug, accountingDebugEnabled } from '../accounting-debug-log';
import { JalaliDate, jalaliToGregorian, jalaliToIsoDate, toIsoDateTehran, toJalali } from '../jalali';
const jalaliKey = (d: JalaliDate) => d.jy * 10000 + d.jm * 100 + d.jd;
@@ -80,6 +81,14 @@ function isMissingKind(err: unknown) {
return err instanceof ZarrinHttpError && err.status === 400;
}
function scrubToken(text: string, token: string) {
if (token) for (const t of new Set([token, encodeURIComponent(token)])) text = text.split(t).join('***');
return text;
}
/** Polled every few seconds; traced only when they fail. */
const QUIET_KINDS = new Set(['parLastTenRadif', 'listMandeHesabha', 'listhesabha', 'listKarSakhte', 'mojoodiKala', 'version', 'listGorooheHesabha', 'parvande']);
/**
* What the server said on a non-2xx reply, short enough for the admin screen.
* Error pages may echo the request URL, so the token is scrubbed.
@@ -94,8 +103,7 @@ function errorBodyText(body: string, token: string) {
} catch {
text = body.replace(/<(script|style)[\s\S]*?<\/\1>/gi, ' ').replace(/<[^>]+>/g, ' ');
}
if (token) for (const t of new Set([token, encodeURIComponent(token)])) text = text.split(t).join('***');
text = text.replace(/\s+/g, ' ').trim();
text = scrubToken(text, token).replace(/\s+/g, ' ').trim();
return text.length > 200 ? `${text.slice(0, 200)}…` : text;
}
@@ -133,15 +141,28 @@ async function call(
if (v !== null && v !== undefined && v !== '') url.searchParams.set(k, String(v));
}
const started = Date.now();
const trace = (extra: Record<string, unknown>) => {
if (!accountingDebugEnabled()) return;
if (QUIET_KINDS.has(kind) && !extra.error) return;
accountingDebug('zarrin.call', { kind, params, ms: Date.now() - started, ...extra });
};
let res;
try {
res = await httpRequest({ method: 'GET', url: url.toString(), timeoutMs: opts.timeoutMs ?? s.timeoutMs, ca: s.caCert });
} catch (err) {
trace({ error: (err as Error).message, code: (err as AccountingHttpError).code });
if (err instanceof AccountingHttpError) {
throw new AccountingProviderError(`زرین (${kind}): ${err.message}`, err.ambiguous);
}
throw err;
}
trace({
status: res.status,
body: scrubToken(res.body, s.token).slice(0, 2000),
...(res.status < 200 || res.status >= 300 ? { error: `HTTP ${res.status}` } : {}),
});
if (res.status < 200 || res.status >= 300) {
// v228 answers goroohSakhte with HTTP 400 and a perfectly good body.