import { calculateNetSaleProceeds, createSale, validateSaleItem } from '../domain/sale.js'; import { createApplicationError } from '../../../shared/errors/application-error.js'; export function createSaleService({ saleRepository }) { return { async create({ sellerId, inventoryItemId, costBasisMinor, items, ...input }) { const saleItems = items ?? [{ inventoryItemId, soldPriceMinor: input.soldPriceMinor, costBasisMinor }]; const totalSoldPriceMinor = items ? saleItems.reduce((total, item) => total + item.soldPriceMinor, 0) : input.soldPriceMinor; const sale = createSale({ sellerId, ...input, soldPriceMinor: totalSoldPriceMinor }); const validatedItems = saleItems.map((item) => ({ inventoryItemId: item.inventoryItemId, soldPriceMinor: validateSaleItem(item).soldPriceMinor, costBasisMinor: validateSaleItem(item).costBasisMinor, })); const result = items ? await saleRepository.createForInventoryItems({ sale, items: validatedItems }) : await saleRepository.createForInventoryItem({ sale, inventoryItemId, costBasisMinor: validatedItems[0].costBasisMinor, }); if (result.status === 'NOT_FOUND') { throw createApplicationError('INVENTORY_NOT_FOUND', 'Inventory item was not found.', 404); } if (result.status === 'NOT_ACTIVE') { throw createApplicationError( 'INVENTORY_NOT_AVAILABLE_FOR_SALE', 'Inventory item is not available for sale.', 409, ); } return withFinancialSummary(result); }, async list({ sellerId }) { const sales = await saleRepository.listBySellerId(sellerId); return sales.map(withFinancialSummary); }, async void({ saleId, sellerId, actorUserId, reason }) { if (typeof reason !== 'string' || reason.trim().length < 3) { throw createApplicationError('SALE_VOID_REASON_REQUIRED', 'A void reason is required.', 400); } const result = await saleRepository.voidForSeller({ saleId, sellerId, actorUserId, reason: reason.trim(), }); if (result.status === 'NOT_FOUND') { throw createApplicationError('SALE_NOT_FOUND', 'Sale was not found.', 404); } if (result.status === 'ALREADY_VOIDED') { throw createApplicationError('SALE_ALREADY_VOIDED', 'Sale is already voided.', 409); } return withFinancialSummary(result); }, }; } function withFinancialSummary(result) { const netProceedsMinor = calculateNetSaleProceeds(result.sale); const saleItems = result.saleItems ?? [result.saleItem]; return { ...result, netProceedsMinor, profitMinor: netProceedsMinor - saleItems.reduce((total, item) => total + item.costBasisMinor, 0), }; }