feat: accounting integration (D27), SMS relay driver (D26), rebuild user apps

- Add Tahesab/Zarrin accounting sync module with admin panel UI
- Add SMS relay driver as default provider, refactor SMS into driver pattern
- Update customer services, orders, products, and catalog queries
- Rebuild user-app and user-app-v2 dist assets
- Add Prisma migration for accounting tables
- Update docker-compose and env examples
- Update parity docs (DECISIONS, ISSUES, PROGRESS)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-10-06 16:48:36 +03:30
co-authored by Claude Opus 4.6
parent a72fa607a5
commit 19d490e790
179 changed files with 12538 additions and 370 deletions
@@ -6,6 +6,7 @@ import { TableService } from '../admin-table/table.service';
import type { TableQueryDto } from '../admin-table/table-query.dto';
import { faToEn } from '../customer-auth/digits';
import { SmsService } from '../sms/sms.service';
import { AccountingHooksService } from '../accounting-sync/accounting-hooks.service';
/**
* Wholesale ("عمده") customer lists — legacy views/customer/omde/*.
@@ -97,6 +98,7 @@ export class AdminCustomersService {
private readonly prisma: PrismaService,
private readonly table: TableService,
private readonly sms: SmsService,
private readonly accounting: AccountingHooksService,
) {}
/**
@@ -802,7 +804,16 @@ export class AdminCustomersService {
patch.shopImageUrl = (data.shop_image as string) || null;
}
return this.prisma.customer.update({ where: { id }, data: patch });
// D27 — the fields the accounting account carries. A change to one of them
// is pushed to a linked account.
const contactChanged = ['fname', 'lname', 'name', 'mobile', 'national_code', 'phone', 'address'].some(
(k) => data[k] !== undefined,
);
return this.prisma.$transaction(async (tx) => {
const updated = await tx.customer.update({ where: { id }, data: patch });
if (contactChanged) await this.accounting.customerEdited(tx, id);
return updated;
});
}
/**
@@ -957,7 +968,15 @@ export class AdminCustomersService {
throw new BadRequestException('مرحله‌ی تایید مشخص نشده است');
}
return this.prisma.customer.update({ where: { id }, data: { verificationStep: next } });
return this.prisma.$transaction(async (tx) => {
const updated = await tx.customer.update({ where: { id }, data: { verificationStep: next } });
// D27 — a verified customer gets an account in the accounting software
// (queued; searched by mobile first, so an existing account is linked).
if (next === VerificationStep.VERIFIED && c.verificationStep !== VerificationStep.VERIFIED) {
await this.accounting.customerVerified(tx, id);
}
return updated;
});
}
/** Legacy `GET major/archive/{id}` — MajorCustomerController::archive. */