Commit the working vertical slices for inventory image processing and the Sale workflow, including Sale history, audited voiding, multi-item persistence, admin UI, migrations, documentation, and tests.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user