Files
lunarfront-app/packages/shared/src/schemas/inventory.schema.ts
ryan 9d51fb2118 feat: repair-POS integration, receipt formats, manager overrides, price adjustments
- Add thermal/full-page receipt format toggle (per-device, localStorage)
- Full-page receipt uses clean invoice layout matching repair PDF style
- Settings page reorganized into tabbed sections (Store, Locations, Modules, Receipt, POS Security, Advanced)
- Manager override system: configurable PIN prompt for void, refund, discount, cash in/out
- Discount threshold setting: require manager approval above X%
- Consumable product type: tracked for internal job costing, excluded from POS search, receipts, and customer-facing totals
- Repair line item dialog: product picker dropdown for parts/consumables from inventory
- Repair → POS checkout: load ready-for-pickup tickets into repair_payment transactions with proper tax categories (labor=service, parts=goods)
- Transaction completion auto-updates repair ticket status to picked_up
- POS Repairs dialog with Pickup and New Intake tabs, customer account lookup
- Inline price adjustment on cart items: % off, $ off, or set price with live preview
- Order-level discount button with same three input modes
- Backend: migration 0043 (consumable enum + is_consumable flag), createFromRepairTicket service, ready-for-pickup endpoint
- Fix: backend dev script uses --env-file for turbo compatibility

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:32:28 +00:00

115 lines
4.2 KiB
TypeScript

import { z } from 'zod'
/** Coerce empty strings to undefined — solves HTML form inputs sending '' for blank optional fields */
function opt<T extends z.ZodTypeAny>(schema: T) {
return z.preprocess((v) => (v === '' ? undefined : v), schema.optional())
}
export const CategoryCreateSchema = z.object({
name: z.string().min(1).max(255),
description: opt(z.string()),
parentId: opt(z.string().uuid()),
sortOrder: z.number().int().default(0),
})
export type CategoryCreateInput = z.infer<typeof CategoryCreateSchema>
export const CategoryUpdateSchema = CategoryCreateSchema.partial()
export type CategoryUpdateInput = z.infer<typeof CategoryUpdateSchema>
export const SupplierCreateSchema = z.object({
name: z.string().min(1).max(255),
contactName: opt(z.string().max(255)),
email: opt(z.string().email()),
phone: opt(z.string().max(50)),
website: opt(z.string().max(255)),
accountNumber: opt(z.string().max(100)),
paymentTerms: opt(z.string().max(100)),
notes: opt(z.string()),
})
export type SupplierCreateInput = z.infer<typeof SupplierCreateSchema>
export const SupplierUpdateSchema = SupplierCreateSchema.partial()
export type SupplierUpdateInput = z.infer<typeof SupplierUpdateSchema>
// System slugs — used for code-level business logic references.
// Actual valid values are stored in lookup tables and are company-configurable.
export const SystemItemCondition = z.enum(['new', 'excellent', 'good', 'fair', 'poor'])
export const SystemUnitStatus = z.enum([
'available',
'sold',
'rented',
'on_trial',
'in_repair',
'layaway',
'lost',
'retired',
])
// API validation accepts any string slug (validated against lookup table at service layer)
export const ItemCondition = z.string().min(1).max(100)
export const UnitStatus = z.string().min(1).max(100)
export const ProductCreateSchema = z.object({
sku: opt(z.string().max(100)),
upc: opt(z.string().max(100)),
name: z.string().min(1).max(255),
description: opt(z.string()),
brand: opt(z.string().max(255)),
model: opt(z.string().max(255)),
categoryId: opt(z.string().uuid()),
locationId: opt(z.string().uuid()),
isSerialized: z.boolean().default(false),
isRental: z.boolean().default(false),
isDualUseRepair: z.boolean().default(false),
isConsumable: z.boolean().default(false),
price: z.number().min(0).optional(),
minPrice: z.number().min(0).optional(),
rentalRateMonthly: z.number().min(0).optional(),
qtyOnHand: z.number().int().min(0).default(0),
qtyReorderPoint: z.number().int().min(0).optional(),
})
export type ProductCreateInput = z.infer<typeof ProductCreateSchema>
export const ProductUpdateSchema = ProductCreateSchema.partial()
export type ProductUpdateInput = z.infer<typeof ProductUpdateSchema>
export const ProductSearchSchema = z.object({
q: z.string().min(1).max(255),
})
export const InventoryUnitCreateSchema = z.object({
productId: z.string().uuid(),
locationId: opt(z.string().uuid()),
serialNumber: opt(z.string().max(255)),
condition: ItemCondition.default('new'),
status: UnitStatus.default('available'),
purchaseDate: opt(z.string().date()),
purchaseCost: z.number().min(0).optional(),
notes: opt(z.string()),
})
export type InventoryUnitCreateInput = z.infer<typeof InventoryUnitCreateSchema>
export const InventoryUnitUpdateSchema = InventoryUnitCreateSchema.omit({ productId: true }).partial()
export type InventoryUnitUpdateInput = z.infer<typeof InventoryUnitUpdateSchema>
export const ProductSupplierCreateSchema = z.object({
supplierId: z.string().uuid(),
supplierSku: opt(z.string().max(100)),
isPreferred: z.boolean().default(false),
})
export type ProductSupplierCreateInput = z.infer<typeof ProductSupplierCreateSchema>
export const ProductSupplierUpdateSchema = ProductSupplierCreateSchema.omit({ supplierId: true }).partial()
export type ProductSupplierUpdateInput = z.infer<typeof ProductSupplierUpdateSchema>
export const StockReceiptCreateSchema = z.object({
supplierId: opt(z.string().uuid()),
inventoryUnitId: opt(z.string().uuid()),
qty: z.number().int().min(1).default(1),
costPerUnit: z.number().min(0),
receivedDate: z.string().date(),
invoiceNumber: opt(z.string().max(100)),
notes: opt(z.string()),
})
export type StockReceiptCreateInput = z.infer<typeof StockReceiptCreateSchema>