import { Injectable, Logger } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { PrismaService } from '../../prisma/prisma.service'; import { TahesabPersonLinkService } from './person-link.service'; /** ساعت و تاریخ امروز به وقت تهران، برای نوبت یک‌بار در هر ساعت. */ export function tehranSyncSlot(now = new Date()): { hour: number; slot: string } { const parts = new Intl.DateTimeFormat('en-GB', { timeZone: 'Asia/Tehran', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', hourCycle: 'h23', }).formatToParts(now); const read = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value ?? ''; let hour = Number(read('hour')); if (!Number.isFinite(hour) || hour === 24) hour = 0; const slot = `${read('year')}-${read('month')}-${read('day')}-${String(hour).padStart(2, '0')}`; return { hour, slot }; } @Injectable() export class TahesabCustomerSyncScheduler { private readonly logger = new Logger(TahesabCustomerSyncScheduler.name); private busy = false; constructor( private readonly prisma: PrismaService, private readonly links: TahesabPersonLinkService, ) {} @Cron('* * * * *', { timeZone: 'Asia/Tehran' }) async tick() { if (process.env.NODE_ENV === 'test' || this.busy) return; this.busy = true; try { const { hour, slot } = tehranSyncSlot(); const connections = await this.prisma.accountingConnection.findMany({ where: { provider: 'tahesab', enabled: true, customerSyncHours: { has: hour }, }, select: { id: true, organizationId: true, label: true, customerSyncLastSlot: true }, }); for (const connection of connections) { if (connection.customerSyncLastSlot === slot) continue; const claimed = await this.prisma.accountingConnection.updateMany({ where: { id: connection.id, OR: [ { customerSyncLastSlot: null }, { customerSyncLastSlot: { not: slot } }, ], }, data: { customerSyncLastSlot: slot }, }); if (claimed.count !== 1) continue; await this.run(connection.organizationId, connection.id, connection.label); } } catch (err) { this.logger.warn( `بررسی زمان‌بندی مشتریان ته‌حساب ناموفق بود: ${ err instanceof Error ? err.message : err }`, ); } finally { this.busy = false; } } private async run(organizationId: string, connectionId: string, label: string) { try { const result = await this.links.syncAllCustomers(organizationId, connectionId); await this.prisma.accountingConnection.update({ where: { id: connectionId }, data: { customerSyncLastRunAt: new Date(), customerSyncLastSummary: result.summary, customerSyncLastError: result.error, }, }); this.logger.log(`بروزرسانی خودکار «${label}»: ${result.summary}`); } catch (err) { const message = (err instanceof Error ? err.message : 'همگام‌سازی ناموفق بود').slice(0, 500); await this.prisma.accountingConnection.update({ where: { id: connectionId }, data: { customerSyncLastRunAt: new Date(), customerSyncLastSummary: null, customerSyncLastError: message, }, }); this.logger.warn(`بروزرسانی خودکار «${label}» ناموفق بود: ${message}`); } } }