Recipe-app main
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { normalizeName } from '../common/utils/normalize-name';
|
||||
import { CreateProductDto } from './dto/create-product.dto';
|
||||
import { UpdateProductDto } from './dto/update-product.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ProductsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findAll() {
|
||||
return this.prisma.product.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findDuplicateCandidates() {
|
||||
const products = await this.prisma.product.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
const grouped = new Map<string, typeof products>();
|
||||
|
||||
for (const product of products) {
|
||||
const key = product.normalizedName;
|
||||
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, []);
|
||||
}
|
||||
|
||||
grouped.get(key)!.push(product);
|
||||
}
|
||||
|
||||
return Array.from(grouped.entries())
|
||||
.filter(([, items]) => items.length > 1)
|
||||
.map(([normalizedName, items]) => ({
|
||||
normalizedName,
|
||||
count: items.length,
|
||||
products: items,
|
||||
}));
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const product = await this.prisma.product.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new NotFoundException(`Product with id ${id} not found`);
|
||||
}
|
||||
|
||||
return product;
|
||||
}
|
||||
|
||||
async create(data: CreateProductDto) {
|
||||
const name = data.name.trim();
|
||||
const normalizedName = normalizeName(name);
|
||||
|
||||
const existing = await this.prisma.product.findUnique({
|
||||
where: { normalizedName },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
if (!existing.isActive) {
|
||||
return this.prisma.product.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
name,
|
||||
canonicalName: name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
return this.prisma.product.create({
|
||||
data: {
|
||||
name,
|
||||
normalizedName,
|
||||
canonicalName: name,
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: number, data: UpdateProductDto) {
|
||||
await this.findOne(id);
|
||||
|
||||
const updateData: {
|
||||
name?: string;
|
||||
normalizedName?: string;
|
||||
canonicalName?: string;
|
||||
} = {};
|
||||
|
||||
if (typeof data.name === 'string') {
|
||||
const name = data.name.trim();
|
||||
const normalizedName = normalizeName(name);
|
||||
|
||||
const existing = await this.prisma.product.findUnique({
|
||||
where: { normalizedName },
|
||||
});
|
||||
|
||||
if (existing && existing.id !== id) {
|
||||
if (!existing.isActive) {
|
||||
return this.prisma.product.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
name,
|
||||
canonicalName: name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
updateData.name = name;
|
||||
updateData.normalizedName = normalizedName;
|
||||
updateData.canonicalName = name;
|
||||
}
|
||||
|
||||
return this.prisma.product.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
}
|
||||
|
||||
async updateCanonicalName(id: number, canonicalName: string) {
|
||||
await this.findOne(id);
|
||||
|
||||
const cleaned = canonicalName.trim();
|
||||
|
||||
return this.prisma.product.update({
|
||||
where: { id },
|
||||
data: {
|
||||
canonicalName: cleaned,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
await this.findOne(id);
|
||||
|
||||
return this.prisma.product.update({
|
||||
where: { id },
|
||||
data: {
|
||||
isActive: false,
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async restore(id: number) {
|
||||
const product = await this.findOne(id);
|
||||
|
||||
if (product.isActive) {
|
||||
return product;
|
||||
}
|
||||
|
||||
return this.prisma.product.update({
|
||||
where: { id },
|
||||
data: {
|
||||
isActive: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async merge(sourceProductId: number, targetProductId: number) {
|
||||
if (sourceProductId === targetProductId) {
|
||||
throw new Error('sourceProductId och targetProductId kan inte vara samma');
|
||||
}
|
||||
|
||||
const source = await this.prisma.product.findUnique({
|
||||
where: { id: sourceProductId },
|
||||
});
|
||||
|
||||
if (!source) {
|
||||
throw new NotFoundException(
|
||||
`Source product with id ${sourceProductId} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
const target = await this.prisma.product.findUnique({
|
||||
where: { id: targetProductId },
|
||||
});
|
||||
|
||||
if (!target) {
|
||||
throw new NotFoundException(
|
||||
`Target product with id ${targetProductId} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const movedInventoryCount = await tx.inventoryItem.updateMany({
|
||||
where: { productId: sourceProductId },
|
||||
data: { productId: targetProductId },
|
||||
});
|
||||
|
||||
const softDeletedSource = await tx.product.update({
|
||||
where: { id: sourceProductId },
|
||||
data: {
|
||||
isActive: false,
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
message: 'Products merged successfully',
|
||||
sourceProductId,
|
||||
targetProductId,
|
||||
movedInventoryCount: movedInventoryCount.count,
|
||||
softDeletedSource,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async previewMerge(sourceProductId: number, targetProductId: number) {
|
||||
if (sourceProductId === targetProductId) {
|
||||
throw new Error('sourceProductId och targetProductId kan inte vara samma');
|
||||
}
|
||||
|
||||
const [source, target, sourceInventoryCount, targetInventoryCount] =
|
||||
await Promise.all([
|
||||
this.prisma.product.findUnique({
|
||||
where: { id: sourceProductId },
|
||||
}),
|
||||
this.prisma.product.findUnique({
|
||||
where: { id: targetProductId },
|
||||
}),
|
||||
this.prisma.inventoryItem.count({
|
||||
where: { productId: sourceProductId },
|
||||
}),
|
||||
this.prisma.inventoryItem.count({
|
||||
where: { productId: targetProductId },
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!source) {
|
||||
throw new NotFoundException(
|
||||
`Source product with id ${sourceProductId} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
throw new NotFoundException(
|
||||
`Target product with id ${targetProductId} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
source: {
|
||||
...source,
|
||||
inventoryCount: sourceInventoryCount,
|
||||
},
|
||||
target: {
|
||||
...target,
|
||||
inventoryCount: targetInventoryCount,
|
||||
},
|
||||
outcome: {
|
||||
inventoryItemsToMove: sourceInventoryCount,
|
||||
sourceWillBeSoftDeleted: true,
|
||||
targetWillRemainActive: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async backfillCanonicalNames() {
|
||||
const products = await this.prisma.product.findMany({
|
||||
where: {
|
||||
canonicalName: null,
|
||||
},
|
||||
});
|
||||
|
||||
const results = await this.prisma.$transaction(
|
||||
products.map((product) =>
|
||||
this.prisma.product.update({
|
||||
where: { id: product.id },
|
||||
data: {
|
||||
canonicalName: product.name,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
message: 'Canonical names backfilled successfully',
|
||||
updatedCount: results.length,
|
||||
products: results,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user