100 lines
3.6 KiB
TypeScript
100 lines
3.6 KiB
TypeScript
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}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|