Montify: robust pagination (beyond 50), endpoint diagnose button
This commit is contained in:
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import {
|
||||
Prisma,
|
||||
@@ -26,6 +21,7 @@ import { carryPreviousSource } from '../../contacts/person-source';
|
||||
import { ownerWhere, type Actor } from '../../common/record-scope';
|
||||
import { MontifyConnectionsService } from './connections.service';
|
||||
import {
|
||||
forEachPage,
|
||||
MontifyClient,
|
||||
MontifyHttpError,
|
||||
type MontifyAddress,
|
||||
@@ -56,7 +52,11 @@ type CustomerProfile = {
|
||||
goldWalletBalance: number | null;
|
||||
summary: { totalOrders: number | null; totalSpent: number | null };
|
||||
addresses: MontifyAddress[];
|
||||
engagement: { saved: unknown[]; likes: unknown[]; comments: unknown[] } | null;
|
||||
engagement: {
|
||||
saved: unknown[];
|
||||
likes: unknown[];
|
||||
comments: unknown[];
|
||||
} | null;
|
||||
};
|
||||
|
||||
function num(v: unknown): number | null {
|
||||
@@ -119,7 +119,10 @@ function formatAddress(a: MontifyAddress): string | null {
|
||||
return line;
|
||||
}
|
||||
|
||||
function splitName(c: MontifyCustomer): { firstName: string; lastName: string } {
|
||||
function splitName(c: MontifyCustomer): {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
} {
|
||||
let firstName = str(c.first_name) ?? '';
|
||||
let lastName = str(c.last_name) ?? '';
|
||||
if (!firstName && !lastName) {
|
||||
@@ -137,7 +140,11 @@ function splitName(c: MontifyCustomer): { firstName: string; lastName: string }
|
||||
|
||||
function customerLinkKey(connectionId: string, externalId: string) {
|
||||
return {
|
||||
connectionId_entityType_externalId: { connectionId, entityType: 'customer', externalId },
|
||||
connectionId_entityType_externalId: {
|
||||
connectionId,
|
||||
entityType: 'customer',
|
||||
externalId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -234,35 +241,46 @@ export class MontifySyncService {
|
||||
mode === 'incremental' && row.customersCursor
|
||||
? row.customersCursor.toISOString()
|
||||
: undefined;
|
||||
let page = 1;
|
||||
let lastPage = 1;
|
||||
this.logger.log(`Montify sync ${row.id} (${row.label}) start ${mode}${updatedAfter ? ` after ${updatedAfter}` : ''}`);
|
||||
do {
|
||||
const res = await client.listCustomers({ page, limit: CUSTOMER_PAGE_SIZE, updatedAfter });
|
||||
lastPage = res.meta.last_page;
|
||||
this.logger.log(
|
||||
`Montify sync ${row.id} page ${page}/${lastPage}: ${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}`);
|
||||
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();
|
||||
}
|
||||
if (result.customersFetched % 5 === 0) await saveProgress();
|
||||
}
|
||||
await saveProgress();
|
||||
if (!res.data.length) break;
|
||||
page += 1;
|
||||
} while (page <= lastPage && page <= 2000);
|
||||
await saveProgress();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
fatal = e instanceof Error ? e.message : String(e);
|
||||
result.errors.push(fatal);
|
||||
@@ -354,19 +372,18 @@ export class MontifySyncService {
|
||||
const ordersStartedAt = new Date();
|
||||
const ordersOk = await step('سفارشها', async () => {
|
||||
const since = mode === 'full' ? null : ordersSyncedAt;
|
||||
let page = 1;
|
||||
let lastPage = 1;
|
||||
do {
|
||||
const res = await client.listCustomerOrders(externalId, {
|
||||
page,
|
||||
updatedAfter: since ? new Date(since.getTime() - 60_000).toISOString() : undefined,
|
||||
});
|
||||
lastPage = res.meta.last_page;
|
||||
if (result) result.ordersFetched += res.data.length;
|
||||
for (const order of res.data) await this.upsertOrder(row, order, personId, externalId);
|
||||
if (!res.data.length) break;
|
||||
page += 1;
|
||||
} while (page <= lastPage && page <= 200);
|
||||
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,
|
||||
);
|
||||
return true;
|
||||
});
|
||||
if (ordersOk) {
|
||||
@@ -392,7 +409,11 @@ export class MontifySyncService {
|
||||
c: MontifyCustomer,
|
||||
addresses: MontifyAddress[],
|
||||
engagement: MontifyEngagement | null,
|
||||
): Promise<{ personId: string; outcome: 'created' | 'updated'; ordersSyncedAt: Date | null }> {
|
||||
): Promise<{
|
||||
personId: string;
|
||||
outcome: 'created' | 'updated';
|
||||
ordersSyncedAt: Date | null;
|
||||
}> {
|
||||
const externalId = String(c.id);
|
||||
const { firstName, lastName } = splitName(c);
|
||||
const mobile = normalizePhone(c.phone ?? null);
|
||||
@@ -429,7 +450,10 @@ export class MontifySyncService {
|
||||
|
||||
let person = existingLink
|
||||
? await this.prisma.person.findFirst({
|
||||
where: { id: existingLink.entityId, organizationId: row.organizationId },
|
||||
where: {
|
||||
id: existingLink.entityId,
|
||||
organizationId: row.organizationId,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -438,9 +462,7 @@ export class MontifySyncService {
|
||||
where: {
|
||||
organizationId: row.organizationId,
|
||||
OR: [
|
||||
...(mobile
|
||||
? mobileVariants(mobile).flatMap((m) => [{ mobile: m }, { phone: m }])
|
||||
: []),
|
||||
...(mobile ? mobileVariants(mobile).flatMap((m) => [{ mobile: m }, { phone: m }]) : []),
|
||||
...(email ? [{ email }] : []),
|
||||
],
|
||||
},
|
||||
@@ -550,8 +572,7 @@ export class MontifySyncService {
|
||||
if (!o?.id) return;
|
||||
const externalId = String(o.id);
|
||||
const customerExternalId = str(o.user_id) ?? customerHint;
|
||||
const personId =
|
||||
personIdHint ?? (await this.personForCustomer(row.id, customerExternalId));
|
||||
const personId = personIdHint ?? (await this.personForCustomer(row.id, customerExternalId));
|
||||
const items = (o.order_items ?? []).map((i) => ({
|
||||
id: str(i.id),
|
||||
productId: str(i.product_id),
|
||||
@@ -592,7 +613,12 @@ export class MontifySyncService {
|
||||
};
|
||||
await this.prisma.montifyOrder.upsert({
|
||||
where: { connectionId_externalId: { connectionId: row.id, externalId } },
|
||||
create: { organizationId: row.organizationId, connectionId: row.id, externalId, ...data },
|
||||
create: {
|
||||
organizationId: row.organizationId,
|
||||
connectionId: row.id,
|
||||
externalId,
|
||||
...data,
|
||||
},
|
||||
update: data,
|
||||
});
|
||||
}
|
||||
@@ -619,7 +645,12 @@ export class MontifySyncService {
|
||||
};
|
||||
await this.prisma.montifyTransaction.upsert({
|
||||
where: { connectionId_externalId: { connectionId: row.id, externalId } },
|
||||
create: { organizationId: row.organizationId, connectionId: row.id, externalId, ...data },
|
||||
create: {
|
||||
organizationId: row.organizationId,
|
||||
connectionId: row.id,
|
||||
externalId,
|
||||
...data,
|
||||
},
|
||||
update: data,
|
||||
});
|
||||
}
|
||||
@@ -691,12 +722,20 @@ export class MontifySyncService {
|
||||
|
||||
async personStores(actor: Actor, personId: string): Promise<MontifyPersonStoreDto[]> {
|
||||
const person = await this.prisma.person.findFirst({
|
||||
where: { id: personId, organizationId: actor.organizationId, ...ownerWhere(actor) },
|
||||
where: {
|
||||
id: personId,
|
||||
organizationId: actor.organizationId,
|
||||
...ownerWhere(actor),
|
||||
},
|
||||
select: { id: true, firstName: true, lastName: true },
|
||||
});
|
||||
if (!person) throw new NotFoundException('شخص پیدا نشد');
|
||||
const links = await this.prisma.montifyLink.findMany({
|
||||
where: { organizationId: actor.organizationId, entityType: 'customer', entityId: personId },
|
||||
where: {
|
||||
organizationId: actor.organizationId,
|
||||
entityType: 'customer',
|
||||
entityId: personId,
|
||||
},
|
||||
include: { connection: { select: { label: true } } },
|
||||
orderBy: { syncedAt: 'desc' },
|
||||
});
|
||||
@@ -705,12 +744,18 @@ export class MontifySyncService {
|
||||
for (const l of links) {
|
||||
const [orders, transactions] = await Promise.all([
|
||||
this.prisma.montifyOrder.findMany({
|
||||
where: { connectionId: l.connectionId, customerExternalId: l.externalId },
|
||||
where: {
|
||||
connectionId: l.connectionId,
|
||||
customerExternalId: l.externalId,
|
||||
},
|
||||
orderBy: [{ externalCreatedAt: 'desc' }, { createdAt: 'desc' }],
|
||||
take: 20,
|
||||
}),
|
||||
this.prisma.montifyTransaction.findMany({
|
||||
where: { connectionId: l.connectionId, customerExternalId: l.externalId },
|
||||
where: {
|
||||
connectionId: l.connectionId,
|
||||
customerExternalId: l.externalId,
|
||||
},
|
||||
orderBy: [{ externalCreatedAt: 'desc' }, { createdAt: 'desc' }],
|
||||
take: 20,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user