diff --git a/apps/api/src/modules/sales/domain/sale.js b/apps/api/src/modules/sales/domain/sale.js new file mode 100644 index 0000000..766dee6 --- /dev/null +++ b/apps/api/src/modules/sales/domain/sale.js @@ -0,0 +1,99 @@ +function requiredText(value, fieldName) { + if (typeof value !== 'string' || value.trim().length === 0) { + throw new Error(`${fieldName} is required.`); + } + + return value.trim(); +} + +function requirePositiveMinorUnit(value, fieldName) { + if (!Number.isInteger(value) || value <= 0) { + throw new Error(`${fieldName} must be a positive integer minor unit.`); + } + + return value; +} + +function requireNonNegativeMinorUnit(value, fieldName) { + if (!Number.isInteger(value) || value < 0) { + throw new Error(`${fieldName} must be a non-negative integer minor unit.`); + } + + return value; +} + +export function createSale({ + sellerId, + saleDate, + saleLocation, + soldPriceMinor, + currency = 'USD', + platformFeesMinor = 0, + paymentProcessingFeesMinor = 0, + shippingCostMinor = 0, + taxesMinor = 0, + otherFeesMinor = 0, + buyerName, + buyerContact, + notes, +}) { + return { + sellerId: requiredText(sellerId, 'Seller ID'), + saleDate: requiredText(saleDate, 'Sale date'), + ...(typeof saleLocation === 'string' && saleLocation.trim().length > 0 + ? { saleLocation: saleLocation.trim() } + : {}), + soldPriceMinor: requirePositiveMinorUnit(soldPriceMinor, 'Sold price'), + currency: requiredText(currency, 'Currency'), + platformFeesMinor: requireNonNegativeMinorUnit(platformFeesMinor, 'Platform fees'), + paymentProcessingFeesMinor: requireNonNegativeMinorUnit( + paymentProcessingFeesMinor, + 'Payment processing fees', + ), + shippingCostMinor: requireNonNegativeMinorUnit(shippingCostMinor, 'Shipping cost'), + taxesMinor: requireNonNegativeMinorUnit(taxesMinor, 'Taxes'), + otherFeesMinor: requireNonNegativeMinorUnit(otherFeesMinor, 'Other fees'), + ...(typeof buyerName === 'string' && buyerName.trim().length > 0 + ? { buyerName: buyerName.trim() } + : {}), + ...(typeof buyerContact === 'string' && buyerContact.trim().length > 0 + ? { buyerContact: buyerContact.trim() } + : {}), + ...(typeof notes === 'string' && notes.trim().length > 0 ? { notes: notes.trim() } : {}), + }; +} + +export function createSaleItem({ + saleId, + inventoryItemId, + soldPriceMinor, + costBasisMinor, +}) { + return { + saleId: requiredText(saleId, 'Sale ID'), + inventoryItemId: requiredText(inventoryItemId, 'Inventory item ID'), + soldPriceMinor: requirePositiveMinorUnit(soldPriceMinor, 'Sold price'), + costBasisMinor: requireNonNegativeMinorUnit(costBasisMinor, 'Cost basis'), + }; +} + +export function calculateNetSaleProceeds({ + soldPriceMinor, + platformFeesMinor = 0, + paymentProcessingFeesMinor = 0, + shippingCostMinor = 0, + taxesMinor = 0, + otherFeesMinor = 0, +}) { + const salePrice = requirePositiveMinorUnit(soldPriceMinor, 'Sold price'); + const platformFees = requireNonNegativeMinorUnit(platformFeesMinor, 'Platform fees'); + const processingFees = requireNonNegativeMinorUnit( + paymentProcessingFeesMinor, + 'Payment processing fees', + ); + const shipping = requireNonNegativeMinorUnit(shippingCostMinor, 'Shipping cost'); + const taxes = requireNonNegativeMinorUnit(taxesMinor, 'Taxes'); + const otherFees = requireNonNegativeMinorUnit(otherFeesMinor, 'Other fees'); + + return salePrice - platformFees - processingFees - shipping - taxes - otherFees; +} diff --git a/apps/api/tests/domain/sale.test.js b/apps/api/tests/domain/sale.test.js new file mode 100644 index 0000000..75d492d --- /dev/null +++ b/apps/api/tests/domain/sale.test.js @@ -0,0 +1,57 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + calculateNetSaleProceeds, + createSale, + createSaleItem, +} from '../../src/modules/sales/domain/sale.js'; + +test('creates a valid sale record and calculates net proceeds after fees', () => { + const sale = createSale({ + sellerId: 'seller-id', + saleDate: '2026-09-08', + saleLocation: 'eBay', + soldPriceMinor: 4999, + currency: 'USD', + platformFeesMinor: 300, + paymentProcessingFeesMinor: 120, + shippingCostMinor: 50, + taxesMinor: 90, + otherFeesMinor: 40, + buyerName: 'Alex', + buyerContact: 'alex@example.com', + }); + + assert.equal(sale.soldPriceMinor, 4999); + assert.equal(sale.currency, 'USD'); + assert.equal(calculateNetSaleProceeds({ + soldPriceMinor: 4999, + platformFeesMinor: 300, + paymentProcessingFeesMinor: 120, + shippingCostMinor: 50, + taxesMinor: 90, + otherFeesMinor: 40, + }), 4399); +}); + +test('rejects invalid sales and negative fee values', () => { + assert.throws( + () => createSale({ + sellerId: 'seller-id', + saleDate: '2026-09-08', + soldPriceMinor: 0, + }), + /must be a positive integer minor unit/, + ); + + assert.throws( + () => createSaleItem({ + saleId: 'sale-id', + inventoryItemId: 'inventory-item-id', + soldPriceMinor: 2000, + costBasisMinor: -1, + }), + /must be a non-negative integer minor unit/, + ); +});