Files
crm/apps/api/src/integrations/tahesab/balance.service.ts
T
MahdiandCursor 4b47b753c3 Add Tahesab jewelry accounting integration with multi-DB connections.
Encrypt tokens server-side, sync customers both ways, show balances/documents on the person Assets tab, and push sanads with idempotent Factor_Code logging.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 22:00:23 +03:30

133 lines
4.2 KiB
TypeScript

import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { TahesabConnectionsService } from './connections.service';
import { normalizeMandehResponse } from './tahesab-methods';
@Injectable()
export class TahesabBalanceService {
constructor(
private readonly prisma: PrismaService,
private readonly connections: TahesabConnectionsService,
) {}
async getAssets(
organizationId: string,
personId: string,
connectionId?: string,
) {
const person = await this.prisma.person.findFirst({
where: { id: personId, organizationId },
});
if (!person) throw new NotFoundException('شخص یافت نشد.');
const resolvedId = await this.connections.resolveConnectionId(
organizationId,
connectionId,
);
const link = await this.prisma.personAccountingLink.findFirst({
where: { personId, connectionId: resolvedId },
include: {
connection: {
select: { id: true, label: true, dbName: true, isDefault: true },
},
balanceCache: true,
},
});
if (!link) {
throw new BadRequestException(
'این شخص به اتصال ته‌حساب لینک نشده است. ابتدا تطبیق یا ثبت در ته‌حساب را انجام دهید.',
);
}
const { api } = await this.connections.getApi(organizationId, resolvedId);
const raw = await api.getMandeHesabByCode(link.moshtariCode);
const normalized = normalizeMandehResponse(raw);
const cache = await this.prisma.accountingBalanceCache.upsert({
where: { linkId: link.id },
create: {
linkId: link.id,
connectionId: resolvedId,
goldBalance: normalized.goldBalance,
rialBalance: normalized.rialBalance,
currencyBalance: normalized.currencyBalance,
totalGoldEquiv: normalized.totalGoldEquiv,
mandehDetail: normalized.mandehDetail as unknown as Prisma.InputJsonValue,
rawResponse: normalized.raw as Prisma.InputJsonValue,
fetchedAt: new Date(),
},
update: {
goldBalance: normalized.goldBalance,
rialBalance: normalized.rialBalance,
currencyBalance: normalized.currencyBalance,
totalGoldEquiv: normalized.totalGoldEquiv,
mandehDetail: normalized.mandehDetail as unknown as Prisma.InputJsonValue,
rawResponse: normalized.raw as Prisma.InputJsonValue,
fetchedAt: new Date(),
},
});
await this.prisma.personAccountingLink.update({
where: { id: link.id },
data: { lastBalanceSyncedAt: cache.fetchedAt },
});
return {
connectionId: resolvedId,
connectionLabel: link.connection.label,
dbName: link.connection.dbName,
moshtariCode: link.moshtariCode,
goldBalance: normalized.goldBalance,
rialBalance: normalized.rialBalance,
currencyBalance: normalized.currencyBalance,
totalGoldEquiv: normalized.totalGoldEquiv,
mandehDetail: normalized.mandehDetail,
fetchedAt: cache.fetchedAt.toISOString(),
};
}
async getCachedOrFetch(
organizationId: string,
personId: string,
connectionId?: string,
refresh = true,
) {
if (refresh) {
return this.getAssets(organizationId, personId, connectionId);
}
const resolvedId = await this.connections.resolveConnectionId(
organizationId,
connectionId,
);
const link = await this.prisma.personAccountingLink.findFirst({
where: { personId, connectionId: resolvedId },
include: {
connection: { select: { label: true, dbName: true } },
balanceCache: true,
},
});
if (!link?.balanceCache) {
return this.getAssets(organizationId, personId, connectionId);
}
const c = link.balanceCache;
return {
connectionId: resolvedId,
connectionLabel: link.connection.label,
dbName: link.connection.dbName,
moshtariCode: link.moshtariCode,
goldBalance: c.goldBalance,
rialBalance: c.rialBalance,
currencyBalance: c.currencyBalance,
totalGoldEquiv: c.totalGoldEquiv,
mandehDetail: c.mandehDetail,
fetchedAt: c.fetchedAt.toISOString(),
};
}
}