feat(public): dedupe merged variants and merge specs/options from secondary origins
This commit is contained in:
@@ -39,6 +39,7 @@ const PUBLIC_GOOD_INCLUDE = {
|
||||
include: {
|
||||
originGood: {
|
||||
include: {
|
||||
detail: true,
|
||||
variants: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
||||
},
|
||||
},
|
||||
@@ -355,11 +356,17 @@ export class PublicService {
|
||||
private toPublicGoodDetail(good: PublicGoodRow): PublicGoodDetailDto {
|
||||
const base = this.toPublicGood(good);
|
||||
const detail = good.originGood.detail;
|
||||
// Merge primary and secondary origin good variants (dedup identical URLs).
|
||||
const allVariants = [
|
||||
// Merge primary and secondary origin goods: variants deduped by
|
||||
// color+size (primary wins), detail specs filled from secondaries
|
||||
// for sizes/options the primary does not have.
|
||||
const secondaryDetails = good.mergedOriginGoods
|
||||
.map((m) => m.originGood.detail)
|
||||
.filter((d): d is NonNullable<typeof d> => Boolean(d));
|
||||
const secondaryVariantLists = good.mergedOriginGoods.map((m) => m.originGood.variants);
|
||||
const allVariants = this.dedupeVariants([
|
||||
...good.originGood.variants,
|
||||
...good.mergedOriginGoods.flatMap((m) => m.originGood.variants),
|
||||
];
|
||||
...secondaryVariantLists.flat(),
|
||||
]);
|
||||
return {
|
||||
...base,
|
||||
productCode: detail?.productCode ?? null,
|
||||
@@ -380,9 +387,9 @@ export class PublicService {
|
||||
},
|
||||
media: (detail?.media as Record<string, unknown> | null) ?? null,
|
||||
mediaByColor: this.groupImagesByColor(allVariants),
|
||||
options: (detail?.options as Record<string, unknown> | null) ?? null,
|
||||
sizeChart: (detail?.sizeChart as Record<string, unknown> | null) ?? null,
|
||||
packageSpecs: (detail?.packageSpecs as Record<string, unknown> | null) ?? null,
|
||||
options: this.mergeOptions(detail, secondaryDetails),
|
||||
sizeChart: this.mergeRowsByKey(detail, secondaryDetails, 'sizeChart'),
|
||||
packageSpecs: this.mergeRowsByKey(detail, secondaryDetails, 'packageSpecs'),
|
||||
variants: allVariants.map((variant) => ({
|
||||
id: variant.sdsVariantId,
|
||||
sku: variant.sku,
|
||||
@@ -405,6 +412,90 @@ export class PublicService {
|
||||
};
|
||||
}
|
||||
|
||||
/** Norm key for variant/dedupe matching: case-insensitive, trimmed. */
|
||||
private static normName(value: string | null | undefined): string {
|
||||
return (value ?? '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
/** Dedupe variants by color+size (case-insensitive); first (primary) wins. */
|
||||
private dedupeVariants(variants: PublicGoodRow['originGood']['variants']) {
|
||||
const seen = new Set<string>();
|
||||
const result: PublicGoodRow['originGood']['variants'] = [];
|
||||
for (const variant of variants) {
|
||||
const key = `${PublicService.normName(variant.colorName)}|${PublicService.normName(variant.sizeName)}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
result.push(variant);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a `{ rows: [...] }` spec (sizeChart / packageSpecs) across primary
|
||||
* and secondary details: primary rows win, rows for sizes the primary
|
||||
* lacks are appended from secondaries in order.
|
||||
*/
|
||||
private mergeRowsByKey(
|
||||
primary: PublicGoodRow['originGood']['detail'],
|
||||
secondaries: NonNullable<PublicGoodRow['originGood']['detail']>[],
|
||||
field: 'sizeChart' | 'packageSpecs',
|
||||
): Record<string, unknown> | null {
|
||||
const primaryRows = (primary?.[field] as { rows?: Array<Record<string, unknown>> } | null)?.rows;
|
||||
if (!Array.isArray(primaryRows) && secondaries.length === 0) return null;
|
||||
const rows: Array<Record<string, unknown>> = Array.isArray(primaryRows) ? [...primaryRows] : [];
|
||||
const seen = new Set(rows.map((r) => PublicService.normName(String(r?.sizeName ?? ''))));
|
||||
for (const sec of secondaries) {
|
||||
const secRows = (sec[field] as { rows?: Array<Record<string, unknown>> } | null)?.rows;
|
||||
if (!Array.isArray(secRows)) continue;
|
||||
for (const row of secRows) {
|
||||
const key = PublicService.normName(String(row?.sizeName ?? ''));
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
if (rows.length === 0) return null;
|
||||
return { ...(primary?.[field] as Record<string, unknown> | null ?? {}), rows };
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge options across details: sizes/colors are unioned by name,
|
||||
* primary entries win and secondary-only ones are appended.
|
||||
*/
|
||||
private mergeOptions(
|
||||
primary: PublicGoodRow['originGood']['detail'],
|
||||
secondaries: NonNullable<PublicGoodRow['originGood']['detail']>[],
|
||||
): Record<string, unknown> | null {
|
||||
const primaryOptions = primary?.options as
|
||||
| { sizes?: Array<Record<string, unknown>>; colors?: Array<Record<string, unknown>> }
|
||||
| null;
|
||||
if (!primaryOptions && secondaries.length === 0) return null;
|
||||
const merged: Record<string, unknown> = { ...(primaryOptions ?? {}) };
|
||||
for (const listKey of ['sizes', 'colors'] as const) {
|
||||
const base = Array.isArray(primaryOptions?.[listKey])
|
||||
? [...(primaryOptions![listKey] as Array<Record<string, unknown>>)]
|
||||
: null;
|
||||
if (!base && secondaries.length === 0) continue;
|
||||
const rows = base ?? [];
|
||||
const seen = new Set(rows.map((r) => PublicService.normName(String(r?.name ?? ''))));
|
||||
for (const sec of secondaries) {
|
||||
const secOptions = sec.options as
|
||||
| { sizes?: Array<Record<string, unknown>>; colors?: Array<Record<string, unknown>> }
|
||||
| null;
|
||||
const secRows = secOptions?.[listKey];
|
||||
if (!Array.isArray(secRows)) continue;
|
||||
for (const row of secRows) {
|
||||
const key = PublicService.normName(String(row?.name ?? ''));
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
merged[listKey] = rows;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
private async buildTagGroupFilters(
|
||||
selectedGroups: PublicTagFilterDto[],
|
||||
): Promise<Prisma.GoodWhereInput[]> {
|
||||
|
||||
Reference in New Issue
Block a user