2026-10-10 18:01:55 +03:30
|
|
|
|
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
2026-10-10 15:59:12 +03:30
|
|
|
|
import { Cron } from '@nestjs/schedule';
|
|
|
|
|
|
import {
|
|
|
|
|
|
Prisma,
|
|
|
|
|
|
type MontifyConnection,
|
|
|
|
|
|
type MontifyOrder as MontifyOrderRow,
|
|
|
|
|
|
type MontifyTransaction as MontifyTransactionRow,
|
|
|
|
|
|
} from '@prisma/client';
|
|
|
|
|
|
import type {
|
|
|
|
|
|
MontifyConnectionDto,
|
|
|
|
|
|
MontifyEngagementDto,
|
|
|
|
|
|
MontifyOrderDto,
|
|
|
|
|
|
MontifyOrderItemDto,
|
|
|
|
|
|
MontifyPersonStoreDto,
|
|
|
|
|
|
MontifySyncResultDto,
|
|
|
|
|
|
MontifyTransactionDto,
|
|
|
|
|
|
} from '@mento/shared';
|
2026-10-13 18:04:53 +03:30
|
|
|
|
import { parseBirthDate } from '../../common/excel';
|
2026-10-10 15:59:12 +03:30
|
|
|
|
import { PrismaService } from '../../prisma/prisma.service';
|
|
|
|
|
|
import { normalizeEmail, normalizePhone } from '../../contacts/normalize';
|
|
|
|
|
|
import { carryPreviousSource } from '../../contacts/person-source';
|
|
|
|
|
|
import { ownerWhere, type Actor } from '../../common/record-scope';
|
|
|
|
|
|
import { MontifyConnectionsService } from './connections.service';
|
|
|
|
|
|
import {
|
2026-10-10 18:01:55 +03:30
|
|
|
|
forEachPage,
|
2026-10-10 15:59:12 +03:30
|
|
|
|
MontifyClient,
|
|
|
|
|
|
MontifyHttpError,
|
|
|
|
|
|
type MontifyAddress,
|
|
|
|
|
|
type MontifyCustomer,
|
|
|
|
|
|
type MontifyEngagement,
|
|
|
|
|
|
type MontifyOrder,
|
|
|
|
|
|
type MontifyTransaction,
|
|
|
|
|
|
} from './montify-client';
|
|
|
|
|
|
|
|
|
|
|
|
const CUSTOMER_PAGE_SIZE = 50;
|
|
|
|
|
|
const ENGAGEMENT_KEEP = 50;
|
2026-10-10 16:56:58 +03:30
|
|
|
|
/** مسیری که پشتسرهم 404 بدهد (پیاده نشده) تا پایان همان دور رد میشود */
|
|
|
|
|
|
const SKIP_AFTER_NOT_FOUND = 3;
|
|
|
|
|
|
|
|
|
|
|
|
type EndpointStats = Map<string, { ok: number; notFound: number }>;
|
2026-10-10 15:59:12 +03:30
|
|
|
|
|
|
|
|
|
|
/** پروفایل فروشگاهی مشتری (MontifyLink.data) — نامها طبق جدول نگاشت مستند */
|
|
|
|
|
|
type CustomerProfile = {
|
|
|
|
|
|
fullName: string | null;
|
|
|
|
|
|
username: string | null;
|
|
|
|
|
|
avatar: string | null;
|
|
|
|
|
|
birthday: string | null;
|
|
|
|
|
|
gender: string | null;
|
|
|
|
|
|
type: string | null;
|
|
|
|
|
|
job: string | null;
|
|
|
|
|
|
storeId: string | null;
|
|
|
|
|
|
walletBalance: number | null;
|
|
|
|
|
|
goldWalletBalance: number | null;
|
|
|
|
|
|
summary: { totalOrders: number | null; totalSpent: number | null };
|
|
|
|
|
|
addresses: MontifyAddress[];
|
2026-10-10 18:01:55 +03:30
|
|
|
|
engagement: {
|
|
|
|
|
|
saved: unknown[];
|
|
|
|
|
|
likes: unknown[];
|
|
|
|
|
|
comments: unknown[];
|
|
|
|
|
|
} | null;
|
2026-10-10 15:59:12 +03:30
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function num(v: unknown): number | null {
|
|
|
|
|
|
if (v === null || v === undefined || v === '') return null;
|
|
|
|
|
|
const n = Number(v);
|
|
|
|
|
|
return Number.isFinite(n) ? n : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function dec(v: unknown): Prisma.Decimal | null {
|
|
|
|
|
|
const n = num(v);
|
|
|
|
|
|
return n === null ? null : new Prisma.Decimal(n);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function decNum(v: Prisma.Decimal | null): number | null {
|
|
|
|
|
|
return v === null ? null : Number(v);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function str(v: unknown): string | null {
|
|
|
|
|
|
if (v === null || v === undefined) return null;
|
|
|
|
|
|
const s = String(v).trim();
|
|
|
|
|
|
return s ? s : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** متن نمایشی فروشگاه (مثل sts_txt) اگر باشد، وگرنه مقدار خام */
|
|
|
|
|
|
function label(obj: unknown, key: string): string | null {
|
|
|
|
|
|
if (!obj || typeof obj !== 'object') return null;
|
|
|
|
|
|
const o = obj as Record<string, unknown>;
|
|
|
|
|
|
return str(o[`${key}_txt`]) ?? str(o[key]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseDate(v: unknown): Date | null {
|
|
|
|
|
|
const s = str(v);
|
|
|
|
|
|
if (!s) return null;
|
|
|
|
|
|
const d = new Date(s);
|
|
|
|
|
|
return Number.isNaN(d.getTime()) ? null : d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function arr(v: unknown): unknown[] {
|
|
|
|
|
|
return Array.isArray(v) ? v : [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function mobileVariants(mobile: string): string[] {
|
|
|
|
|
|
const digits = mobile.replace(/\D/g, '');
|
|
|
|
|
|
let core = digits;
|
|
|
|
|
|
if (core.startsWith('0098')) core = core.slice(4);
|
|
|
|
|
|
else if (core.startsWith('98') && core.length === 12) core = core.slice(2);
|
|
|
|
|
|
else if (core.startsWith('0')) core = core.slice(1);
|
|
|
|
|
|
if (!core) return [mobile];
|
|
|
|
|
|
return [...new Set([mobile, core, `0${core}`, `98${core}`, `+98${core}`])];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatAddress(a: MontifyAddress): string | null {
|
|
|
|
|
|
const parts = [str(a.province), str(a.city), str(a.address)].filter(Boolean);
|
|
|
|
|
|
if (!parts.length) return null;
|
|
|
|
|
|
let line = parts.join('، ');
|
|
|
|
|
|
const postal = str(a.postal_code);
|
|
|
|
|
|
if (postal) line += ` — کدپستی ${postal}`;
|
|
|
|
|
|
const receiver = [str(a.receiver_name), str(a.receiver_phone)].filter(Boolean).join(' ');
|
|
|
|
|
|
if (receiver) line += ` — گیرنده: ${receiver}`;
|
|
|
|
|
|
return line;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 18:01:55 +03:30
|
|
|
|
function splitName(c: MontifyCustomer): {
|
|
|
|
|
|
firstName: string;
|
|
|
|
|
|
lastName: string;
|
|
|
|
|
|
} {
|
2026-10-10 15:59:12 +03:30
|
|
|
|
let firstName = str(c.first_name) ?? '';
|
|
|
|
|
|
let lastName = str(c.last_name) ?? '';
|
|
|
|
|
|
if (!firstName && !lastName) {
|
|
|
|
|
|
const parts = (str(c.name) ?? '').split(/\s+/).filter(Boolean);
|
|
|
|
|
|
if (parts.length > 1) {
|
|
|
|
|
|
firstName = parts[0]!;
|
|
|
|
|
|
lastName = parts.slice(1).join(' ');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
lastName = parts[0] ?? '';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!firstName && !lastName) lastName = str(c.username) ?? `مشتری ${c.id}`;
|
|
|
|
|
|
return { firstName, lastName };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function customerLinkKey(connectionId: string, externalId: string) {
|
|
|
|
|
|
return {
|
2026-10-10 18:01:55 +03:30
|
|
|
|
connectionId_entityType_externalId: {
|
|
|
|
|
|
connectionId,
|
|
|
|
|
|
entityType: 'customer',
|
|
|
|
|
|
externalId,
|
|
|
|
|
|
},
|
2026-10-10 15:59:12 +03:30
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
|
export class MontifySyncService {
|
|
|
|
|
|
private readonly logger = new Logger(MontifySyncService.name);
|
|
|
|
|
|
private readonly running = new Set<string>();
|
|
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
|
private readonly prisma: PrismaService,
|
|
|
|
|
|
private readonly connections: MontifyConnectionsService,
|
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
|
|
/* ——— فاز ۱ و ۲: Polling و Incremental Sync ——— */
|
|
|
|
|
|
|
|
|
|
|
|
async startSync(
|
|
|
|
|
|
organizationId: string,
|
|
|
|
|
|
connectionId: string,
|
|
|
|
|
|
mode: 'incremental' | 'full',
|
|
|
|
|
|
): Promise<MontifyConnectionDto> {
|
|
|
|
|
|
const row = await this.connections.getOrThrow(organizationId, connectionId);
|
|
|
|
|
|
if (!row.enabled) throw new BadRequestException('این اتصال غیرفعال است');
|
2026-10-10 16:18:25 +03:30
|
|
|
|
if (!this.running.has(connectionId)) {
|
|
|
|
|
|
await new Promise<void>((started) => {
|
|
|
|
|
|
void this.runSync(row, mode, started);
|
|
|
|
|
|
});
|
2026-10-10 15:59:12 +03:30
|
|
|
|
}
|
|
|
|
|
|
return this.connections.getDto(organizationId, connectionId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Cron('*/10 * * * *')
|
|
|
|
|
|
async cronSyncAll(): Promise<void> {
|
|
|
|
|
|
const rows = await this.prisma.montifyConnection.findMany({
|
|
|
|
|
|
where: { enabled: true },
|
|
|
|
|
|
take: 50,
|
|
|
|
|
|
});
|
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
|
if (this.running.has(row.id)) continue;
|
|
|
|
|
|
await this.runSync(row, 'incremental');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 16:18:25 +03:30
|
|
|
|
private async runSync(
|
|
|
|
|
|
row: MontifyConnection,
|
|
|
|
|
|
mode: 'incremental' | 'full',
|
|
|
|
|
|
onStarted?: () => void,
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
if (this.running.has(row.id)) {
|
|
|
|
|
|
onStarted?.();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-10-10 15:59:12 +03:30
|
|
|
|
this.running.add(row.id);
|
|
|
|
|
|
const startedAt = new Date();
|
|
|
|
|
|
const result: MontifySyncResultDto = {
|
|
|
|
|
|
mode,
|
|
|
|
|
|
customersFetched: 0,
|
|
|
|
|
|
customersCreated: 0,
|
|
|
|
|
|
customersUpdated: 0,
|
|
|
|
|
|
ordersFetched: 0,
|
|
|
|
|
|
transactionsFetched: 0,
|
|
|
|
|
|
errors: [],
|
|
|
|
|
|
startedAt: startedAt.toISOString(),
|
|
|
|
|
|
finishedAt: null,
|
|
|
|
|
|
};
|
|
|
|
|
|
const saveProgress = () =>
|
|
|
|
|
|
this.prisma.montifyConnection.update({
|
|
|
|
|
|
where: { id: row.id },
|
|
|
|
|
|
data: { lastSyncResult: result as unknown as Prisma.InputJsonValue },
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-10 16:18:25 +03:30
|
|
|
|
try {
|
|
|
|
|
|
await this.prisma.montifyConnection.update({
|
|
|
|
|
|
where: { id: row.id },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
syncStatus: 'running',
|
|
|
|
|
|
syncStartedAt: startedAt,
|
|
|
|
|
|
lastSyncResult: result as unknown as Prisma.InputJsonValue,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
this.running.delete(row.id);
|
|
|
|
|
|
this.logger.warn(`Montify sync ${row.id} start: ${e instanceof Error ? e.message : e}`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
onStarted?.();
|
|
|
|
|
|
}
|
2026-10-10 15:59:12 +03:30
|
|
|
|
|
2026-10-10 16:56:58 +03:30
|
|
|
|
const endpoints: EndpointStats = new Map();
|
2026-10-10 15:59:12 +03:30
|
|
|
|
let cursor = row.customersCursor;
|
|
|
|
|
|
let fatal: string | null = null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const client = this.connections.clientFor(row);
|
|
|
|
|
|
const updatedAfter =
|
|
|
|
|
|
mode === 'incremental' && row.customersCursor
|
|
|
|
|
|
? row.customersCursor.toISOString()
|
|
|
|
|
|
: undefined;
|
2026-10-10 18:01:55 +03:30
|
|
|
|
this.logger.log(
|
|
|
|
|
|
`Montify sync ${row.id} (${row.label}) start ${mode}${updatedAfter ? ` after ${updatedAfter}` : ''}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
await forEachPage(
|
|
|
|
|
|
(page) =>
|
|
|
|
|
|
client.listCustomers({
|
|
|
|
|
|
page,
|
|
|
|
|
|
limit: CUSTOMER_PAGE_SIZE,
|
|
|
|
|
|
updatedAfter,
|
|
|
|
|
|
}),
|
|
|
|
|
|
async (res, page) => {
|
|
|
|
|
|
this.logger.log(
|
|
|
|
|
|
`Montify sync ${row.id} page ${page}/${res.meta.last_page || '?'}: ${res.data.length} customers (total ${res.meta.total || '?'})`,
|
|
|
|
|
|
);
|
|
|
|
|
|
for (const customer of res.data) {
|
|
|
|
|
|
result.customersFetched += 1;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const outcome = await this.syncCustomerDeep(
|
|
|
|
|
|
row,
|
|
|
|
|
|
client,
|
|
|
|
|
|
customer,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
result,
|
|
|
|
|
|
endpoints,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (outcome === 'created') result.customersCreated += 1;
|
|
|
|
|
|
else result.customersUpdated += 1;
|
|
|
|
|
|
const updatedAt = parseDate(customer.updated_at);
|
|
|
|
|
|
if (updatedAt && (!cursor || updatedAt > cursor)) cursor = updatedAt;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (e instanceof MontifyHttpError && e.status === 401) throw e;
|
|
|
|
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
|
|
|
|
result.errors.push(`مشتری ${customer.id}: ${msg}`);
|
|
|
|
|
|
this.logger.warn(`Montify ${row.id} customer ${customer.id}: ${msg}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (result.customersFetched % 5 === 0) await saveProgress();
|
2026-10-10 15:59:12 +03:30
|
|
|
|
}
|
2026-10-10 18:01:55 +03:30
|
|
|
|
await saveProgress();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-10-10 15:59:12 +03:30
|
|
|
|
} catch (e) {
|
|
|
|
|
|
fatal = e instanceof Error ? e.message : String(e);
|
|
|
|
|
|
result.errors.push(fatal);
|
|
|
|
|
|
this.logger.warn(`Montify sync ${row.id}: ${fatal}`);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.running.delete(row.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 16:56:58 +03:30
|
|
|
|
const missing = [...endpoints.entries()]
|
|
|
|
|
|
.filter(([, s]) => s.notFound > 0)
|
|
|
|
|
|
.map(
|
|
|
|
|
|
([label, s]) =>
|
|
|
|
|
|
`مسیر ${label}: ${s.notFound} بار 404 داد` +
|
|
|
|
|
|
(s.ok ? '' : ' (احتمالاً در این فروشگاه پیاده نشده)'),
|
|
|
|
|
|
);
|
2026-10-10 15:59:12 +03:30
|
|
|
|
result.finishedAt = new Date().toISOString();
|
2026-10-10 16:56:58 +03:30
|
|
|
|
result.errors = [...missing, ...result.errors].slice(0, 50);
|
2026-10-10 17:11:51 +03:30
|
|
|
|
this.logger.log(
|
|
|
|
|
|
`Montify sync ${row.id} done: ${result.customersFetched} customers, ${result.ordersFetched} orders, ` +
|
|
|
|
|
|
`${result.transactionsFetched} transactions, ${result.errors.length} errors${fatal ? ` — fatal: ${fatal}` : ''}`,
|
|
|
|
|
|
);
|
2026-10-10 15:59:12 +03:30
|
|
|
|
await this.prisma.montifyConnection.update({
|
|
|
|
|
|
where: { id: row.id },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
syncStatus: fatal ? 'error' : 'ok',
|
|
|
|
|
|
lastSyncAt: fatal ? undefined : new Date(),
|
|
|
|
|
|
customersCursor: cursor,
|
|
|
|
|
|
lastSyncResult: result as unknown as Prisma.InputJsonValue,
|
|
|
|
|
|
lastHealthAt: new Date(),
|
|
|
|
|
|
lastHealthOk: !fatal,
|
|
|
|
|
|
lastError: fatal
|
|
|
|
|
|
? fatal.slice(0, 500)
|
|
|
|
|
|
: result.errors.length
|
|
|
|
|
|
? result.errors.slice(0, 3).join(' | ').slice(0, 500)
|
|
|
|
|
|
: null,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* گام ۲ استراتژی مستند: برای هر مشتری، آدرسها، سفارشها، تراکنشها و دادههای تعاملی
|
|
|
|
|
|
* جداگانه دریافت و Upsert میشوند.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async syncCustomerDeep(
|
|
|
|
|
|
row: MontifyConnection,
|
|
|
|
|
|
client: MontifyClient,
|
|
|
|
|
|
customer: MontifyCustomer,
|
|
|
|
|
|
mode: 'incremental' | 'full',
|
|
|
|
|
|
result: MontifySyncResultDto | null,
|
2026-10-10 16:56:58 +03:30
|
|
|
|
endpoints: EndpointStats = new Map(),
|
2026-10-10 15:59:12 +03:30
|
|
|
|
): Promise<'created' | 'updated'> {
|
|
|
|
|
|
const externalId = String(customer.id);
|
|
|
|
|
|
const step = async <T>(label: string, fn: () => Promise<T>): Promise<T | null> => {
|
2026-10-10 16:56:58 +03:30
|
|
|
|
const stats = endpoints.get(label) ?? { ok: 0, notFound: 0 };
|
|
|
|
|
|
endpoints.set(label, stats);
|
|
|
|
|
|
if (stats.ok === 0 && stats.notFound >= SKIP_AFTER_NOT_FOUND) return null;
|
2026-10-10 15:59:12 +03:30
|
|
|
|
try {
|
2026-10-10 16:56:58 +03:30
|
|
|
|
const value = await fn();
|
|
|
|
|
|
stats.ok += 1;
|
|
|
|
|
|
return value;
|
2026-10-10 15:59:12 +03:30
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (e instanceof MontifyHttpError && e.status === 401) throw e;
|
2026-10-10 16:56:58 +03:30
|
|
|
|
if (e instanceof MontifyHttpError && e.status === 404) {
|
|
|
|
|
|
stats.notFound += 1;
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-10-10 15:59:12 +03:30
|
|
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
|
|
|
|
result?.errors.push(`مشتری ${externalId} (${label}): ${msg}`);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const addresses =
|
|
|
|
|
|
(await step('آدرسها', () => client.listCustomerAddresses(externalId))) ??
|
|
|
|
|
|
customer.addresses ??
|
|
|
|
|
|
[];
|
|
|
|
|
|
const engagement =
|
|
|
|
|
|
(await step('تعاملات', () => client.getCustomerEngagement(externalId))) ??
|
|
|
|
|
|
customer.engagement ??
|
|
|
|
|
|
null;
|
|
|
|
|
|
|
|
|
|
|
|
const { personId, outcome, ordersSyncedAt } = await this.upsertCustomer(
|
|
|
|
|
|
row,
|
|
|
|
|
|
customer,
|
|
|
|
|
|
addresses,
|
|
|
|
|
|
engagement,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const ordersStartedAt = new Date();
|
|
|
|
|
|
const ordersOk = await step('سفارشها', async () => {
|
|
|
|
|
|
const since = mode === 'full' ? null : ordersSyncedAt;
|
2026-10-10 18:01:55 +03:30
|
|
|
|
await forEachPage(
|
|
|
|
|
|
(page) =>
|
|
|
|
|
|
client.listCustomerOrders(externalId, {
|
|
|
|
|
|
page,
|
|
|
|
|
|
updatedAfter: since ? new Date(since.getTime() - 60_000).toISOString() : undefined,
|
|
|
|
|
|
}),
|
|
|
|
|
|
async (res) => {
|
|
|
|
|
|
if (result) result.ordersFetched += res.data.length;
|
|
|
|
|
|
for (const order of res.data) await this.upsertOrder(row, order, personId, externalId);
|
|
|
|
|
|
},
|
|
|
|
|
|
200,
|
|
|
|
|
|
);
|
2026-10-10 15:59:12 +03:30
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
if (ordersOk) {
|
|
|
|
|
|
await this.prisma.montifyLink.update({
|
|
|
|
|
|
where: customerLinkKey(row.id, externalId),
|
|
|
|
|
|
data: { ordersSyncedAt: ordersStartedAt },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await step('تراکنشها', async () => {
|
|
|
|
|
|
const list = await client.listCustomerTransactions(externalId);
|
|
|
|
|
|
if (result) result.transactionsFetched += list.length;
|
|
|
|
|
|
for (const t of list) await this.upsertTransaction(row, t, personId, externalId);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return outcome;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ——— مشتری → شخص ——— */
|
|
|
|
|
|
|
|
|
|
|
|
private async upsertCustomer(
|
|
|
|
|
|
row: MontifyConnection,
|
|
|
|
|
|
c: MontifyCustomer,
|
|
|
|
|
|
addresses: MontifyAddress[],
|
|
|
|
|
|
engagement: MontifyEngagement | null,
|
2026-10-10 18:01:55 +03:30
|
|
|
|
): Promise<{
|
|
|
|
|
|
personId: string;
|
|
|
|
|
|
outcome: 'created' | 'updated';
|
|
|
|
|
|
ordersSyncedAt: Date | null;
|
|
|
|
|
|
}> {
|
2026-10-10 15:59:12 +03:30
|
|
|
|
const externalId = String(c.id);
|
|
|
|
|
|
const { firstName, lastName } = splitName(c);
|
|
|
|
|
|
const mobile = normalizePhone(c.phone ?? null);
|
|
|
|
|
|
const email = normalizeEmail(c.email ?? null);
|
|
|
|
|
|
const addressLines = addresses.map(formatAddress).filter((a): a is string => Boolean(a));
|
|
|
|
|
|
const profile: CustomerProfile = {
|
|
|
|
|
|
fullName: str(c.name) ?? (`${firstName} ${lastName}`.trim() || null),
|
|
|
|
|
|
username: str(c.username),
|
|
|
|
|
|
avatar: str(c.avatar),
|
|
|
|
|
|
birthday: str(c.birthday),
|
|
|
|
|
|
gender: label(c, 'gender'),
|
|
|
|
|
|
type: label(c, 'type'),
|
|
|
|
|
|
job: str(c.job),
|
|
|
|
|
|
storeId: str(c.store_id),
|
|
|
|
|
|
walletBalance: num(c.cash_balance),
|
|
|
|
|
|
goldWalletBalance: num(c.gold_balance),
|
|
|
|
|
|
summary: {
|
|
|
|
|
|
totalOrders: num(c.summary?.total_orders),
|
|
|
|
|
|
totalSpent: num(c.summary?.total_spent),
|
|
|
|
|
|
},
|
|
|
|
|
|
addresses,
|
|
|
|
|
|
engagement: engagement
|
|
|
|
|
|
? {
|
|
|
|
|
|
saved: arr(engagement.saved).slice(0, ENGAGEMENT_KEEP),
|
|
|
|
|
|
likes: arr(engagement.likes).slice(0, ENGAGEMENT_KEEP),
|
|
|
|
|
|
comments: arr(engagement.comments).slice(0, ENGAGEMENT_KEEP),
|
|
|
|
|
|
}
|
|
|
|
|
|
: null,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const existingLink = await this.prisma.montifyLink.findUnique({
|
|
|
|
|
|
where: customerLinkKey(row.id, externalId),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let person = existingLink
|
|
|
|
|
|
? await this.prisma.person.findFirst({
|
2026-10-10 18:01:55 +03:30
|
|
|
|
where: {
|
|
|
|
|
|
id: existingLink.entityId,
|
|
|
|
|
|
organizationId: row.organizationId,
|
|
|
|
|
|
},
|
2026-10-10 15:59:12 +03:30
|
|
|
|
})
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
if (!person && (mobile || email)) {
|
|
|
|
|
|
person = await this.prisma.person.findFirst({
|
|
|
|
|
|
where: {
|
|
|
|
|
|
organizationId: row.organizationId,
|
|
|
|
|
|
OR: [
|
2026-10-10 18:01:55 +03:30
|
|
|
|
...(mobile ? mobileVariants(mobile).flatMap((m) => [{ mobile: m }, { phone: m }]) : []),
|
2026-10-10 15:59:12 +03:30
|
|
|
|
...(email ? [{ email }] : []),
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
orderBy: { createdAt: 'asc' },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let personId: string;
|
|
|
|
|
|
let outcome: 'created' | 'updated';
|
|
|
|
|
|
if (person) {
|
|
|
|
|
|
const ownedByStore = person.importSource === 'montify';
|
|
|
|
|
|
const current = Array.isArray(person.addresses)
|
|
|
|
|
|
? (person.addresses as unknown[]).map((a) => String(a))
|
|
|
|
|
|
: [];
|
|
|
|
|
|
const merged = [...current];
|
|
|
|
|
|
for (const a of addressLines) if (!merged.includes(a)) merged.push(a);
|
|
|
|
|
|
const tags = person.tags.includes(row.label) ? person.tags : [...person.tags, row.label];
|
|
|
|
|
|
await this.prisma.person.update({
|
|
|
|
|
|
where: { id: person.id },
|
|
|
|
|
|
data: {
|
|
|
|
|
|
...(ownedByStore ? { firstName, lastName } : {}),
|
|
|
|
|
|
...(mobile && (ownedByStore || !person.mobile) ? { mobile } : {}),
|
|
|
|
|
|
...(email && (ownedByStore || !person.email) ? { email } : {}),
|
|
|
|
|
|
...(str(c.job) && !person.position ? { position: str(c.job) } : {}),
|
2026-10-13 18:04:53 +03:30
|
|
|
|
...(!person.birthDate && parseBirthDate(profile.birthday)
|
|
|
|
|
|
? { birthDate: parseBirthDate(profile.birthday) }
|
|
|
|
|
|
: {}),
|
2026-10-10 15:59:12 +03:30
|
|
|
|
addresses: merged as Prisma.InputJsonValue,
|
|
|
|
|
|
tags,
|
|
|
|
|
|
...(!existingLink && !ownedByStore && !person.previousImportSource
|
|
|
|
|
|
? carryPreviousSource(person, 'montify')
|
|
|
|
|
|
: {}),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
personId = person.id;
|
|
|
|
|
|
outcome = 'updated';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const notes = [str(c.bio), str(c.description)].filter(Boolean).join('\n') || null;
|
|
|
|
|
|
const created = await this.prisma.person.create({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
organizationId: row.organizationId,
|
|
|
|
|
|
firstName,
|
|
|
|
|
|
lastName,
|
|
|
|
|
|
mobile,
|
|
|
|
|
|
email,
|
|
|
|
|
|
position: str(c.job),
|
|
|
|
|
|
notes,
|
|
|
|
|
|
addresses: addressLines as Prisma.InputJsonValue,
|
|
|
|
|
|
tags: [row.label],
|
|
|
|
|
|
importSource: 'montify',
|
|
|
|
|
|
sourceRef: externalId,
|
2026-10-13 18:04:53 +03:30
|
|
|
|
birthDate: parseBirthDate(profile.birthday),
|
2026-10-10 15:59:12 +03:30
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
personId = created.id;
|
|
|
|
|
|
outcome = 'created';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const link = await this.prisma.montifyLink.upsert({
|
|
|
|
|
|
where: customerLinkKey(row.id, externalId),
|
|
|
|
|
|
create: {
|
|
|
|
|
|
organizationId: row.organizationId,
|
|
|
|
|
|
connectionId: row.id,
|
|
|
|
|
|
entityType: 'customer',
|
|
|
|
|
|
externalId,
|
|
|
|
|
|
entityId: personId,
|
|
|
|
|
|
externalUpdatedAt: parseDate(c.updated_at),
|
|
|
|
|
|
data: profile as unknown as Prisma.InputJsonValue,
|
|
|
|
|
|
},
|
|
|
|
|
|
update: {
|
|
|
|
|
|
entityId: personId,
|
|
|
|
|
|
externalUpdatedAt: parseDate(c.updated_at),
|
|
|
|
|
|
data: profile as unknown as Prisma.InputJsonValue,
|
|
|
|
|
|
syncedAt: new Date(),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!existingLink || existingLink.entityId !== personId) {
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
this.prisma.montifyOrder.updateMany({
|
|
|
|
|
|
where: { connectionId: row.id, customerExternalId: externalId },
|
|
|
|
|
|
data: { personId },
|
|
|
|
|
|
}),
|
|
|
|
|
|
this.prisma.montifyTransaction.updateMany({
|
|
|
|
|
|
where: { connectionId: row.id, customerExternalId: externalId },
|
|
|
|
|
|
data: { personId },
|
|
|
|
|
|
}),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { personId, outcome, ordersSyncedAt: link.ordersSyncedAt };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ——— سفارش و تراکنش ——— */
|
|
|
|
|
|
|
|
|
|
|
|
private async personForCustomer(connectionId: string, customerExternalId: string | null) {
|
|
|
|
|
|
if (!customerExternalId) return null;
|
|
|
|
|
|
const link = await this.prisma.montifyLink.findUnique({
|
|
|
|
|
|
where: customerLinkKey(connectionId, customerExternalId),
|
|
|
|
|
|
select: { entityId: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
return link?.entityId ?? null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async upsertOrder(
|
|
|
|
|
|
row: MontifyConnection,
|
|
|
|
|
|
o: MontifyOrder,
|
|
|
|
|
|
personIdHint: string | null,
|
|
|
|
|
|
customerHint: string | null,
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
if (!o?.id) return;
|
|
|
|
|
|
const externalId = String(o.id);
|
|
|
|
|
|
const customerExternalId = str(o.user_id) ?? customerHint;
|
2026-10-10 18:01:55 +03:30
|
|
|
|
const personId = personIdHint ?? (await this.personForCustomer(row.id, customerExternalId));
|
2026-10-10 15:59:12 +03:30
|
|
|
|
const items = (o.order_items ?? []).map((i) => ({
|
|
|
|
|
|
id: str(i.id),
|
|
|
|
|
|
productId: str(i.product_id),
|
|
|
|
|
|
productName: str(i.product_name),
|
|
|
|
|
|
qty: num(i.qty),
|
|
|
|
|
|
price: num(i.price),
|
|
|
|
|
|
weight: num(i.weight),
|
|
|
|
|
|
totalPrice: num(i.total_price),
|
|
|
|
|
|
}));
|
|
|
|
|
|
const data = {
|
|
|
|
|
|
customerExternalId,
|
|
|
|
|
|
personId,
|
|
|
|
|
|
orderReference: str(o.code),
|
|
|
|
|
|
orderStatus: str(o.sts),
|
|
|
|
|
|
paymentStatus: str(o.payment_sts),
|
|
|
|
|
|
paymentType: str(o.payment_type),
|
|
|
|
|
|
totalPrice: dec(o.total_price),
|
|
|
|
|
|
discountTotalPrice: dec(o.discount_total_price),
|
|
|
|
|
|
totalDiscounts: dec(o.total_discounts),
|
|
|
|
|
|
totalTax: dec(o.total_tax),
|
|
|
|
|
|
totalQty: dec(o.total_qty),
|
|
|
|
|
|
profit: dec(o.profit),
|
|
|
|
|
|
percentProfit: dec(o.percent_profit),
|
|
|
|
|
|
sumGold: dec(o.sum_gold),
|
|
|
|
|
|
sumSilver: dec(o.sum_silver),
|
|
|
|
|
|
address: (o.address ?? Prisma.JsonNull) as Prisma.InputJsonValue,
|
|
|
|
|
|
note: str(o.note),
|
|
|
|
|
|
cancelReason: str(o.cancel_reason),
|
|
|
|
|
|
cancelledAt: parseDate(o.cancelled_at),
|
|
|
|
|
|
closedAt: parseDate(o.closed_at),
|
|
|
|
|
|
codeDiscount: str(o.code_discount),
|
|
|
|
|
|
items: items as unknown as Prisma.InputJsonValue,
|
|
|
|
|
|
externalCreatedAt: parseDate(o.created_at),
|
|
|
|
|
|
externalUpdatedAt: parseDate(o.updated_at),
|
|
|
|
|
|
deletedAt: parseDate(o.deleted_at),
|
|
|
|
|
|
raw: o as unknown as Prisma.InputJsonValue,
|
|
|
|
|
|
syncedAt: new Date(),
|
|
|
|
|
|
};
|
|
|
|
|
|
await this.prisma.montifyOrder.upsert({
|
|
|
|
|
|
where: { connectionId_externalId: { connectionId: row.id, externalId } },
|
2026-10-10 18:01:55 +03:30
|
|
|
|
create: {
|
|
|
|
|
|
organizationId: row.organizationId,
|
|
|
|
|
|
connectionId: row.id,
|
|
|
|
|
|
externalId,
|
|
|
|
|
|
...data,
|
|
|
|
|
|
},
|
2026-10-10 15:59:12 +03:30
|
|
|
|
update: data,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async upsertTransaction(
|
|
|
|
|
|
row: MontifyConnection,
|
|
|
|
|
|
t: MontifyTransaction,
|
|
|
|
|
|
personId: string,
|
|
|
|
|
|
customerExternalId: string,
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
if (!t?.id) return;
|
|
|
|
|
|
const externalId = String(t.id);
|
|
|
|
|
|
const data = {
|
|
|
|
|
|
customerExternalId: str(t.user_id) ?? customerExternalId,
|
|
|
|
|
|
personId,
|
|
|
|
|
|
type: str(t.type),
|
|
|
|
|
|
amount: dec(t.amount),
|
|
|
|
|
|
currency: str(t.currency),
|
|
|
|
|
|
status: str(t.status),
|
|
|
|
|
|
referenceCode: str(t.reference_code),
|
|
|
|
|
|
externalCreatedAt: parseDate(t.created_at),
|
|
|
|
|
|
raw: t as unknown as Prisma.InputJsonValue,
|
|
|
|
|
|
syncedAt: new Date(),
|
|
|
|
|
|
};
|
|
|
|
|
|
await this.prisma.montifyTransaction.upsert({
|
|
|
|
|
|
where: { connectionId_externalId: { connectionId: row.id, externalId } },
|
2026-10-10 18:01:55 +03:30
|
|
|
|
create: {
|
|
|
|
|
|
organizationId: row.organizationId,
|
|
|
|
|
|
connectionId: row.id,
|
|
|
|
|
|
externalId,
|
|
|
|
|
|
...data,
|
|
|
|
|
|
},
|
2026-10-10 15:59:12 +03:30
|
|
|
|
update: data,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ——— فاز ۳: وبهوک ——— */
|
|
|
|
|
|
|
|
|
|
|
|
async processWebhook(
|
|
|
|
|
|
row: MontifyConnection,
|
|
|
|
|
|
deliveryRowId: string,
|
|
|
|
|
|
event: string,
|
|
|
|
|
|
payload: { data?: Record<string, unknown> } | null,
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = payload?.data ?? {};
|
|
|
|
|
|
const id = str(data.id);
|
|
|
|
|
|
if (!id) throw new Error('شناسه رکورد در payload نیست');
|
|
|
|
|
|
const client = this.connections.clientFor(row);
|
|
|
|
|
|
if (event.startsWith('customer.')) {
|
|
|
|
|
|
const customer = await client.getCustomer(id);
|
|
|
|
|
|
await this.syncCustomerDeep(row, client, customer, 'incremental', null);
|
|
|
|
|
|
} else if (event.startsWith('order.')) {
|
|
|
|
|
|
let order: MontifyOrder;
|
|
|
|
|
|
try {
|
|
|
|
|
|
order = await client.getOrder(id);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
order = data as unknown as MontifyOrder;
|
|
|
|
|
|
}
|
|
|
|
|
|
const customerId = str(order.user_id);
|
|
|
|
|
|
const personId = await this.personForCustomer(row.id, customerId);
|
|
|
|
|
|
if (!personId && customerId) {
|
|
|
|
|
|
const customer = await client.getCustomer(customerId);
|
|
|
|
|
|
await this.syncCustomerDeep(row, client, customer, 'incremental', null);
|
|
|
|
|
|
}
|
|
|
|
|
|
await this.upsertOrder(row, order, null, customerId);
|
|
|
|
|
|
}
|
|
|
|
|
|
await this.prisma.montifyWebhookDelivery.update({
|
|
|
|
|
|
where: { id: deliveryRowId },
|
|
|
|
|
|
data: { status: 'processed' },
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
|
|
|
|
this.logger.warn(`Montify webhook ${row.id} ${event}: ${msg}`);
|
|
|
|
|
|
await this.prisma.montifyWebhookDelivery.update({
|
|
|
|
|
|
where: { id: deliveryRowId },
|
|
|
|
|
|
data: { status: 'failed', error: msg.slice(0, 500) },
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ——— نمایش ——— */
|
|
|
|
|
|
|
|
|
|
|
|
async listOrders(organizationId: string, connectionId?: string): Promise<MontifyOrderDto[]> {
|
|
|
|
|
|
const rows = await this.prisma.montifyOrder.findMany({
|
|
|
|
|
|
where: { organizationId, ...(connectionId ? { connectionId } : {}) },
|
|
|
|
|
|
include: { connection: { select: { label: true } } },
|
|
|
|
|
|
orderBy: [{ externalCreatedAt: 'desc' }, { createdAt: 'desc' }],
|
|
|
|
|
|
take: 50,
|
|
|
|
|
|
});
|
|
|
|
|
|
const personIds = [...new Set(rows.map((r) => r.personId).filter((x): x is string => !!x))];
|
|
|
|
|
|
const people = personIds.length
|
|
|
|
|
|
? await this.prisma.person.findMany({
|
|
|
|
|
|
where: { id: { in: personIds } },
|
|
|
|
|
|
select: { id: true, firstName: true, lastName: true },
|
|
|
|
|
|
})
|
|
|
|
|
|
: [];
|
|
|
|
|
|
const nameOf = new Map(people.map((p) => [p.id, `${p.firstName} ${p.lastName}`.trim()]));
|
|
|
|
|
|
return rows.map((r) => this.orderDto(r, r.connection.label, nameOf));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async personStores(actor: Actor, personId: string): Promise<MontifyPersonStoreDto[]> {
|
|
|
|
|
|
const person = await this.prisma.person.findFirst({
|
2026-10-10 18:01:55 +03:30
|
|
|
|
where: {
|
|
|
|
|
|
id: personId,
|
|
|
|
|
|
organizationId: actor.organizationId,
|
|
|
|
|
|
...ownerWhere(actor),
|
|
|
|
|
|
},
|
2026-10-10 15:59:12 +03:30
|
|
|
|
select: { id: true, firstName: true, lastName: true },
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!person) throw new NotFoundException('شخص پیدا نشد');
|
|
|
|
|
|
const links = await this.prisma.montifyLink.findMany({
|
2026-10-10 18:01:55 +03:30
|
|
|
|
where: {
|
|
|
|
|
|
organizationId: actor.organizationId,
|
|
|
|
|
|
entityType: 'customer',
|
|
|
|
|
|
entityId: personId,
|
|
|
|
|
|
},
|
2026-10-10 15:59:12 +03:30
|
|
|
|
include: { connection: { select: { label: true } } },
|
|
|
|
|
|
orderBy: { syncedAt: 'desc' },
|
|
|
|
|
|
});
|
|
|
|
|
|
const nameOf = new Map([[person.id, `${person.firstName} ${person.lastName}`.trim()]]);
|
|
|
|
|
|
const out: MontifyPersonStoreDto[] = [];
|
|
|
|
|
|
for (const l of links) {
|
|
|
|
|
|
const [orders, transactions] = await Promise.all([
|
|
|
|
|
|
this.prisma.montifyOrder.findMany({
|
2026-10-10 18:01:55 +03:30
|
|
|
|
where: {
|
|
|
|
|
|
connectionId: l.connectionId,
|
|
|
|
|
|
customerExternalId: l.externalId,
|
|
|
|
|
|
},
|
2026-10-10 15:59:12 +03:30
|
|
|
|
orderBy: [{ externalCreatedAt: 'desc' }, { createdAt: 'desc' }],
|
|
|
|
|
|
take: 20,
|
|
|
|
|
|
}),
|
|
|
|
|
|
this.prisma.montifyTransaction.findMany({
|
2026-10-10 18:01:55 +03:30
|
|
|
|
where: {
|
|
|
|
|
|
connectionId: l.connectionId,
|
|
|
|
|
|
customerExternalId: l.externalId,
|
|
|
|
|
|
},
|
2026-10-10 15:59:12 +03:30
|
|
|
|
orderBy: [{ externalCreatedAt: 'desc' }, { createdAt: 'desc' }],
|
|
|
|
|
|
take: 20,
|
|
|
|
|
|
}),
|
|
|
|
|
|
]);
|
|
|
|
|
|
const p = (l.data ?? {}) as Partial<CustomerProfile>;
|
|
|
|
|
|
const engagement: MontifyEngagementDto | null = p.engagement
|
|
|
|
|
|
? {
|
|
|
|
|
|
saved: arr(p.engagement.saved).length,
|
|
|
|
|
|
likes: arr(p.engagement.likes).length,
|
|
|
|
|
|
comments: arr(p.engagement.comments).length,
|
|
|
|
|
|
}
|
|
|
|
|
|
: null;
|
|
|
|
|
|
out.push({
|
|
|
|
|
|
connectionId: l.connectionId,
|
|
|
|
|
|
connectionLabel: l.connection.label,
|
|
|
|
|
|
externalCustomerId: l.externalId,
|
|
|
|
|
|
fullName: p.fullName ?? null,
|
|
|
|
|
|
username: p.username ?? null,
|
|
|
|
|
|
customerType: p.type ?? null,
|
|
|
|
|
|
walletBalance: p.walletBalance ?? null,
|
|
|
|
|
|
goldWalletBalance: p.goldWalletBalance ?? null,
|
|
|
|
|
|
totalOrders: p.summary?.totalOrders ?? null,
|
|
|
|
|
|
totalSpent: p.summary?.totalSpent ?? null,
|
|
|
|
|
|
engagement,
|
|
|
|
|
|
orders: orders.map((o) => this.orderDto(o, l.connection.label, nameOf)),
|
|
|
|
|
|
transactions: transactions.map((t) => this.transactionDto(t)),
|
|
|
|
|
|
syncedAt: l.syncedAt.toISOString(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private orderDto(
|
|
|
|
|
|
r: MontifyOrderRow,
|
|
|
|
|
|
connectionLabel: string,
|
|
|
|
|
|
nameOf: Map<string, string>,
|
|
|
|
|
|
): MontifyOrderDto {
|
|
|
|
|
|
const items = (Array.isArray(r.items) ? r.items : []) as unknown as MontifyOrderItemDto[];
|
|
|
|
|
|
const raw = r.raw;
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: r.id,
|
|
|
|
|
|
connectionId: r.connectionId,
|
|
|
|
|
|
connectionLabel,
|
|
|
|
|
|
externalId: r.externalId,
|
|
|
|
|
|
customerExternalId: r.customerExternalId,
|
|
|
|
|
|
personId: r.personId,
|
|
|
|
|
|
personName: r.personId ? (nameOf.get(r.personId) ?? null) : null,
|
|
|
|
|
|
orderReference: r.orderReference,
|
|
|
|
|
|
orderStatus: label(raw, 'sts') ?? r.orderStatus,
|
|
|
|
|
|
paymentStatus: label(raw, 'payment_sts') ?? r.paymentStatus,
|
|
|
|
|
|
paymentType: label(raw, 'payment_type') ?? r.paymentType,
|
|
|
|
|
|
totalPrice: decNum(r.totalPrice),
|
|
|
|
|
|
discountTotalPrice: decNum(r.discountTotalPrice),
|
|
|
|
|
|
totalDiscounts: decNum(r.totalDiscounts),
|
|
|
|
|
|
totalQty: decNum(r.totalQty),
|
|
|
|
|
|
sumGold: decNum(r.sumGold),
|
|
|
|
|
|
sumSilver: decNum(r.sumSilver),
|
|
|
|
|
|
note: r.note,
|
|
|
|
|
|
cancelReason: r.cancelReason,
|
|
|
|
|
|
cancelledAt: r.cancelledAt?.toISOString() ?? null,
|
|
|
|
|
|
closedAt: r.closedAt?.toISOString() ?? null,
|
|
|
|
|
|
items: items.map((i) => ({
|
|
|
|
|
|
productId: i.productId ?? null,
|
|
|
|
|
|
productName: i.productName ?? null,
|
|
|
|
|
|
qty: i.qty ?? null,
|
|
|
|
|
|
price: i.price ?? null,
|
|
|
|
|
|
weight: i.weight ?? null,
|
|
|
|
|
|
totalPrice: i.totalPrice ?? null,
|
|
|
|
|
|
})),
|
|
|
|
|
|
createdAt: r.externalCreatedAt?.toISOString() ?? null,
|
|
|
|
|
|
updatedAt: r.externalUpdatedAt?.toISOString() ?? null,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private transactionDto(t: MontifyTransactionRow): MontifyTransactionDto {
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: t.id,
|
|
|
|
|
|
externalId: t.externalId,
|
|
|
|
|
|
type: label(t.raw, 'type') ?? t.type,
|
|
|
|
|
|
amount: decNum(t.amount),
|
|
|
|
|
|
currency: label(t.raw, 'currency') ?? t.currency,
|
|
|
|
|
|
status: label(t.raw, 'status') ?? t.status,
|
|
|
|
|
|
referenceCode: t.referenceCode,
|
|
|
|
|
|
createdAt: t.externalCreatedAt?.toISOString() ?? null,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|