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
@@ -1,6 +1,7 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { AccountingSyncService } from '../accounting-sync/accounting-sync.service';
// passwordHash must never leave the API — every customer read goes through this.
const PUBLIC_CUSTOMER_SELECT = {
@@ -35,7 +36,10 @@ const PUBLIC_CUSTOMER_SELECT = {
*/
@Injectable()
export class CustomersService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly accounting: AccountingSyncService,
) {}
async getOrThrow(customerId: string) {
const customer = await this.prisma.customer.findUnique({
@@ -43,7 +47,21 @@ export class CustomersService {
select: PUBLIC_CUSTOMER_SELECT,
});
if (!customer) throw new NotFoundException(`Customer ${customerId} not found`);
return customer;
// D27 — with the accounting integration on, the customer sees the balance
// the accounting software holds. The apps only display these two fields;
// the credit-limit check runs server-side on the internal ledger, untouched.
const accounting = await this.accounting.customerBalance(customerId);
if (accounting) {
return {
...customer,
goldDebtGrams: accounting.gold,
fiatDebtRial: accounting.rial,
balanceSource: 'accounting' as const,
balanceAt: accounting.at,
};
}
return { ...customer, balanceSource: 'app' as const, balanceAt: null };
}
getOrders(customerId: string) {