Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 67 additions & 22 deletions src/lib/services/brand.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,30 +325,54 @@ export class BrandService {
throw new Error('Brand not found');
}

// Filter out duplicates and invalid IDs
const uniqueProductIds = [...new Set(productIds.filter(Boolean))];
if (uniqueProductIds.length === 0) {
return { updated: 0, errors: [] };
}

// Step 1: Find all accessible products from the provided list
const existingProducts = await prisma.product.findMany({
where: {
id: { in: uniqueProductIds },
storeId,
deletedAt: null,
},
select: { id: true },
});

const foundIds = new Set(existingProducts.map((p) => p.id));
const errors: string[] = [];
let updated = 0;

// Process each product
for (const productId of productIds) {
// Identify missing products for error reporting
for (const id of uniqueProductIds) {
if (!foundIds.has(id)) {
errors.push(`Product ${id} not found or not accessible`);
}
}

// Step 2: Update all found products in a single operation
let updated = 0;
if (foundIds.size > 0) {
try {
const result = await prisma.product.updateMany({
where: {
id: productId,
id: { in: Array.from(foundIds) },
storeId,
deletedAt: null,
},
data: {
brandId,
},
});

if (result.count === 0) {
errors.push(`Product ${productId} not found or not accessible`);
} else {
updated += result.count;
}
updated = result.count;
} catch (error) {
errors.push(`Failed to update product ${productId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// In case of bulk update failure, we report it once or per product depending on preferred behavior
// Since the original code processed each product, if one failed, others might succeed.
// With updateMany, if it fails, it fails for all.
// For consistency with original error format, we'll add it once but referencing the batch.
errors.push(`Failed to update product batch: ${errorMessage}`);
}
}

Expand All @@ -363,15 +387,40 @@ export class BrandService {
productIds: string[],
storeId: string
): Promise<{ updated: number; errors: string[] }> {
// Filter out duplicates and invalid IDs
const uniqueProductIds = [...new Set(productIds.filter(Boolean))];
if (uniqueProductIds.length === 0) {
return { updated: 0, errors: [] };
}

// Step 1: Find all accessible products assigned to this brand
const existingProducts = await prisma.product.findMany({
where: {
id: { in: uniqueProductIds },
brandId,
storeId,
deletedAt: null,
},
select: { id: true },
});

const foundIds = new Set(existingProducts.map((p) => p.id));
const errors: string[] = [];
let updated = 0;

// Process each product
for (const productId of productIds) {
// Identify missing products for error reporting
for (const id of uniqueProductIds) {
if (!foundIds.has(id)) {
errors.push(`Product ${id} not found, not assigned to this brand, or not accessible`);
}
}

// Step 2: Update all found products in a single operation
let updated = 0;
if (foundIds.size > 0) {
try {
const result = await prisma.product.updateMany({
where: {
id: productId,
id: { in: Array.from(foundIds) },
brandId,
storeId,
deletedAt: null,
Expand All @@ -380,14 +429,10 @@ export class BrandService {
brandId: null,
},
});

if (result.count === 0) {
errors.push(`Product ${productId} not found, not assigned to this brand, or not accessible`);
} else {
updated += result.count;
}
updated = result.count;
} catch (error) {
errors.push(`Failed to update product ${productId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
errors.push(`Failed to update product batch: ${errorMessage}`);
}
}

Expand Down
118 changes: 118 additions & 0 deletions src/test/services/brand.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { brandService } from '@/lib/services/brand.service';
import { prismaMock } from '@/test/mocks/prisma';

describe('BrandService', () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe('assignProductsToBrand', () => {
it('should assign products to a brand efficiently', async () => {
const brandId = 'brand_123';
const storeId = 'store_123';
const productIds = ['prod_1', 'prod_2', 'prod_3'];

// Mock brand check
prismaMock.brand.findFirst.mockResolvedValue({ id: brandId } as any);

// Mock findMany to return all products
prismaMock.product.findMany.mockResolvedValue(
productIds.map(id => ({ id })) as any
);

// Mock updateMany result
prismaMock.product.updateMany.mockResolvedValue({ count: 3 });

const result = await brandService.assignProductsToBrand(brandId, productIds, storeId);

expect(result.updated).toBe(3);
expect(result.errors).toHaveLength(0);

// Verify single call to findMany and single call to updateMany (Efficient)
expect(prismaMock.product.findMany).toHaveBeenCalledTimes(1);
expect(prismaMock.product.updateMany).toHaveBeenCalledTimes(1);

// Verify query parameters
expect(prismaMock.product.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({
id: { in: expect.arrayContaining(productIds) }
})
}));
});

it('should handle missing products and still update others', async () => {
const brandId = 'brand_123';
const storeId = 'store_123';
const productIds = ['prod_1', 'prod_2', 'prod_3'];

prismaMock.brand.findFirst.mockResolvedValue({ id: brandId } as any);

// Mock findMany to return only 2 products
prismaMock.product.findMany.mockResolvedValue([
{ id: 'prod_1' },
{ id: 'prod_2' }
] as any);

prismaMock.product.updateMany.mockResolvedValue({ count: 2 });

const result = await brandService.assignProductsToBrand(brandId, productIds, storeId);

expect(result.updated).toBe(2);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toContain('Product prod_3 not found or not accessible');

expect(prismaMock.product.updateMany).toHaveBeenCalledTimes(1);
expect(prismaMock.product.updateMany).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({
id: { in: ['prod_1', 'prod_2'] }
})
}));
});
});

describe('removeProductsFromBrand', () => {
it('should remove products from a brand efficiently', async () => {
const brandId = 'brand_123';
const storeId = 'store_123';
const productIds = ['prod_1', 'prod_2', 'prod_3'];

// Mock findMany to return all products
prismaMock.product.findMany.mockResolvedValue(
productIds.map(id => ({ id })) as any
);

// Mock updateMany results
prismaMock.product.updateMany.mockResolvedValue({ count: 3 });

const result = await brandService.removeProductsFromBrand(brandId, productIds, storeId);

expect(result.updated).toBe(3);
expect(result.errors).toHaveLength(0);

// Verify efficient calls
expect(prismaMock.product.findMany).toHaveBeenCalledTimes(1);
expect(prismaMock.product.updateMany).toHaveBeenCalledTimes(1);
});

it('should handle products not assigned to brand', async () => {
const brandId = 'brand_123';
const storeId = 'store_123';
const productIds = ['prod_1', 'prod_2', 'prod_3'];

// Mock findMany to return only 1 product
prismaMock.product.findMany.mockResolvedValue([
{ id: 'prod_1' }
] as any);

prismaMock.product.updateMany.mockResolvedValue({ count: 1 });

const result = await brandService.removeProductsFromBrand(brandId, productIds, storeId);

expect(result.updated).toBe(1);
expect(result.errors).toHaveLength(2);
expect(result.errors[0]).toContain('Product prod_2 not found, not assigned to this brand, or not accessible');
expect(result.errors[1]).toContain('Product prod_3 not found, not assigned to this brand, or not accessible');
});
});
});
Loading