Recipe-app main

This commit is contained in:
2026-04-09 09:14:39 +02:00
commit 962f4e4be5
10015 changed files with 2445177 additions and 0 deletions
+308
View File
@@ -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,
};
}
}