feat: complete inventory image and sales workflows
Quality / test (push) Canceled after 0s

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:
2026-09-09 00:35:57 -04:00
parent ea145191df
commit 8e5ce80196
32 changed files with 1092 additions and 226 deletions
+1
View File
@@ -7,6 +7,7 @@
"./inventory": "./inventory.js",
"./image": "./image.js",
"./listing": "./listing.js",
"./sale": "./sale.js",
"./product": "./product.js",
"./public-listing": "./public-listing.js",
"./public-inventory": "./public-inventory.js",
+47
View File
@@ -0,0 +1,47 @@
function toIsoString(value) {
return value instanceof Date ? value.toISOString() : value;
}
export function toAdminSaleDto({ sale, saleItem, inventoryItem, saleItems = [saleItem], inventoryItems = [inventoryItem], netProceedsMinor, profitMinor, productName }) {
return {
id: sale.id,
sellerId: sale.sellerId,
status: sale.status,
saleDate: sale.saleDate,
saleLocation: sale.saleLocation ?? null,
soldPriceMinor: sale.soldPriceMinor,
currency: sale.currency,
platformFeesMinor: sale.platformFeesMinor,
paymentProcessingFeesMinor: sale.paymentProcessingFeesMinor,
shippingCostMinor: sale.shippingCostMinor,
taxesMinor: sale.taxesMinor,
otherFeesMinor: sale.otherFeesMinor,
buyerName: sale.buyerName ?? null,
buyerContact: sale.buyerContact ?? null,
notes: sale.notes ?? null,
voidReason: sale.voidReason ?? null,
voidedAt: toIsoString(sale.voidedAt) ?? null,
productName: productName ?? null,
netProceedsMinor: netProceedsMinor ?? null,
profitMinor: profitMinor ?? null,
saleItem: {
id: saleItem.id,
inventoryItemId: saleItem.inventoryItemId,
soldPriceMinor: saleItem.soldPriceMinor,
costBasisMinor: saleItem.costBasisMinor,
},
saleItems: saleItems.map((item) => ({
id: item.id,
inventoryItemId: item.inventoryItemId,
soldPriceMinor: item.soldPriceMinor,
costBasisMinor: item.costBasisMinor,
})),
inventoryItem: {
id: inventoryItem.id,
status: inventoryItem.status,
},
inventoryItems: inventoryItems.map((item) => ({ id: item.id, status: item.status })),
createdAt: toIsoString(sale.createdAt),
updatedAt: toIsoString(sale.updatedAt),
};
}
+1
View File
@@ -7,6 +7,7 @@
"./inventory": "./inventory.js",
"./image": "./image.js",
"./listing": "./listing.js",
"./sale": "./sale.js",
"./product": "./product.js"
},
"dependencies": {
+45
View File
@@ -0,0 +1,45 @@
import { z } from 'zod';
const isoDate = /^\d{4}-\d{2}-\d{2}$/;
const currencyCode = /^[A-Za-z]{3}$/;
const saleItemSchema = z.object({
inventoryItemId: z.uuid(),
soldPriceMinor: z.int().positive(),
costBasisMinor: z.int().nonnegative(),
});
export const createSaleSchema = z.object({
inventoryItemId: z.uuid(),
costBasisMinor: z.int().nonnegative(),
saleDate: z.string().trim().regex(isoDate),
saleLocation: z.string().trim().max(255).optional(),
soldPriceMinor: z.int().positive(),
currency: z.string().trim().regex(currencyCode).optional(),
platformFeesMinor: z.int().nonnegative().optional(),
paymentProcessingFeesMinor: z.int().nonnegative().optional(),
shippingCostMinor: z.int().nonnegative().optional(),
taxesMinor: z.int().nonnegative().optional(),
otherFeesMinor: z.int().nonnegative().optional(),
buyerName: z.string().trim().max(255).optional(),
buyerContact: z.string().trim().max(255).optional(),
notes: z.string().trim().max(10000).optional(),
}).strict();
export const createMultiItemSaleSchema = z.object({
items: z.array(saleItemSchema).min(2),
saleDate: z.string().trim().regex(isoDate),
saleLocation: z.string().trim().max(255).optional(),
currency: z.string().trim().regex(currencyCode).optional(),
platformFeesMinor: z.int().nonnegative().optional(),
paymentProcessingFeesMinor: z.int().nonnegative().optional(),
shippingCostMinor: z.int().nonnegative().optional(),
taxesMinor: z.int().nonnegative().optional(),
otherFeesMinor: z.int().nonnegative().optional(),
buyerName: z.string().trim().max(255).optional(),
buyerContact: z.string().trim().max(255).optional(),
notes: z.string().trim().max(10000).optional(),
}).strict();
export const voidSaleSchema = z.object({
reason: z.string().trim().min(3).max(1000),
}).strict();