feat(sync): synchronize customer adresses and orders from Hiboutik sales (#1)

Co-authored-by: MastersGame <quentin.duwa1210@gmail.com>
Reviewed-on: #1
This commit is contained in:
2026-11-07 09:56:53 +00:00
parent c5f65dcb62
commit 9dddf7562a
16 changed files with 755 additions and 17 deletions

View File

@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { CustomerAddress } from '../../prisma/generated/client';
import { CustomerAddressCreateInput } from '../../prisma/generated/models/CustomerAddress';
/** Database access to customer addresses. Rows are matched with Hiboutik by `hiboutikId`. */
@Injectable()
export class CustomerAddressDatabaseRepository {
constructor(private readonly prisma: PrismaService) {}
/** Creates or updates the address with the same `hiboutikId`. */
async upsert(address: CustomerAddressCreateInput): Promise<CustomerAddress> {
return this.prisma.customerAddress.upsert({
where: { hiboutikId: address.hiboutikId },
create: address,
update: address,
});
}
/** Deletes the addresses of a customer that no longer exist in Hiboutik (all of them if `keptHiboutikIds` is empty). */
async deleteMissing(customerId: string, keptHiboutikIds: number[]): Promise<number> {
const result = await this.prisma.customerAddress.deleteMany({
where: { customerId, hiboutikId: { notIn: keptHiboutikIds } },
});
return result.count;
}
}

View File

@@ -0,0 +1,61 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { Order } from '../../prisma/generated/client';
import { OrderWithLinesInput } from '../../prisma/customObjects/prisma.custom.object';
import { OrderStatus } from '../models/orderStatus';
/** Database access to orders. Hiboutik sales are matched by `hiboutikSaleId`. */
@Injectable()
export class OrderDatabaseRepository {
constructor(private readonly prisma: PrismaService) {}
/** Among `hiboutikSaleIds`, the ones already stored as completed orders. */
async getCompletedSaleIds(hiboutikSaleIds: number[]): Promise<Set<number>> {
if (hiboutikSaleIds.length === 0) return new Set();
const orders = await this.prisma.order.findMany({
where: { hiboutikSaleId: { in: hiboutikSaleIds }, status: OrderStatus.COMPLETED },
select: { hiboutikSaleId: true },
});
return new Set(orders.map((order) => order.hiboutikSaleId).filter((id): id is number => id !== null));
}
/**
* Creates or updates the order with the same `hiboutikSaleId`, replacing all its lines (atomically).
* Each line is linked to the current product version matching its Hiboutik product and size, if it still exists.
* On update, `source` is kept: a website order sent to Hiboutik stays WEB.
*/
async upsert(order: OrderWithLinesInput): Promise<Order> {
if (order.hiboutikSaleId == null) throw new Error('Cannot upsert an order without hiboutikSaleId');
const { lines, source, ...orderFields } = order;
const linkedLines = await this.linkToProductVersions(lines);
return this.prisma.order.upsert({
where: { hiboutikSaleId: order.hiboutikSaleId },
create: { ...orderFields, source, lines: { create: linkedLines } },
update: {
...orderFields,
// an undefined relation is ignored by Prisma on update: explicitly unlink a customer removed from the sale
customer: orderFields.customer ?? { disconnect: true },
lines: { deleteMany: {}, create: linkedLines },
},
});
}
private async linkToProductVersions(lines: OrderWithLinesInput['lines']): Promise<OrderWithLinesInput['lines']> {
if (lines.length === 0) return lines;
const versions = await this.prisma.productVersion.findMany({
where: {
OR: lines.map((line) => ({ hiboutikSizeId: line.hiboutikSizeId, product: { hiboutikId: line.hiboutikProductId } })),
},
select: { id: true, hiboutikSizeId: true, product: { select: { hiboutikId: true } } },
});
const versionIds = new Map(versions.map((version) => [`${version.product.hiboutikId}:${version.hiboutikSizeId}`, version.id]));
return lines.map((line) => {
const versionId = versionIds.get(`${line.hiboutikProductId}:${line.hiboutikSizeId}`);
return versionId ? { ...line, productVersion: { connect: { id: versionId } } } : line;
});
}
}