Add ZarinPal e-payment and related checkout/search fixes.

Business settings can store ZarinPal merchant credentials, and the payment registry can initiate and verify ZarinPal alongside Mellat.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-16 01:45:41 +03:30
co-authored by Cursor
parent c76c3967bb
commit b991a60cbc
19 changed files with 542 additions and 39 deletions
+57 -3
View File
@@ -35,6 +35,56 @@ type UserListRow = {
isActive: boolean;
};
/**
* ILIKE patterns so local Iranian input (09…) matches stored E.164 (+98…).
* `09024300` must match `+989024300340`; short fragments like `4300` still match.
*/
function cellNumberLikePatterns(input: string): string[] {
const trimmed = input.trim();
if (!trimmed) return [];
const patterns = new Set<string>();
patterns.add(`%${trimmed}%`);
const digits = trimmed.replace(/\D/g, '');
if (!digits) return [...patterns];
patterns.add(`%${digits}%`);
let rest = digits;
if (rest.startsWith('00')) rest = rest.slice(2);
if (rest.startsWith('98')) {
patterns.add(`%+${rest}%`);
const national = rest.slice(2);
if (national) {
patterns.add(`%0${national}%`);
patterns.add(`%${national}%`);
}
} else if (rest.startsWith('0')) {
const national = rest.slice(1);
if (national) {
patterns.add(`%+98${national}%`);
patterns.add(`%98${national}%`);
}
} else if (rest.startsWith('9')) {
patterns.add(`%+98${rest}%`);
patterns.add(`%98${rest}%`);
patterns.add(`%0${rest}%`);
}
return [...patterns];
}
function cellNumberIlikeFilter(input: string | undefined): Prisma.Sql {
const patterns = cellNumberLikePatterns(input ?? '');
if (!patterns.length) return Prisma.empty;
return Prisma.sql`AND (${Prisma.join(
patterns.map((pattern) => Prisma.sql`u.cell_number ILIKE ${pattern}`),
' OR ',
)})`;
}
@Injectable()
export class UsersService {
constructor(
@@ -111,7 +161,6 @@ export class UsersService {
const skip = (page - 1) * pageSize;
const nameLike = query.name?.trim() ? `%${query.name.trim()}%` : null;
const cellLike = query.cellNumber?.trim() ? `%${query.cellNumber.trim()}%` : null;
const roleSlug = query.role?.trim() || null;
const businessId = query.businessId ?? null;
const membership = query.membership ?? null;
@@ -125,7 +174,7 @@ export class UsersService {
OR (COALESCE(u.first_name, '') || ' ' || COALESCE(u.last_name, '')) ILIKE ${nameLike}
)
` : Prisma.empty}
${cellLike ? Prisma.sql`AND u.cell_number ILIKE ${cellLike}` : Prisma.empty}
${cellNumberIlikeFilter(query.cellNumber)}
${roleSlug ? Prisma.sql`
AND EXISTS (
SELECT 1
@@ -346,6 +395,11 @@ export class UsersService {
const q = query.q.trim();
const limit = Math.min(Math.max(query.limit ?? 20, 1), 50);
const like = `%${q}%`;
const cellPatterns = cellNumberLikePatterns(q);
const cellMatch = Prisma.join(
cellPatterns.map((pattern) => Prisma.sql`u.cell_number ILIKE ${pattern}`),
' OR ',
);
const items = await this.prisma.$queryRaw<
{
@@ -365,7 +419,7 @@ export class UsersService {
FROM users u
WHERE u.is_active = TRUE
AND (
u.cell_number ILIKE ${like}
${cellMatch}
OR u.first_name ILIKE ${like}
OR u.last_name ILIKE ${like}
OR u.email ILIKE ${like}