import { z } from 'zod'; import { timestampSchema } from './common.schemas'; import { shopOnlineOrderStatusEnum } from './storefront.schemas'; export const saleTransactionTypeEnum = z.enum(['sale', 'refund']); export const agentLanguageEnum = z.enum(['en', 'pcm', 'ha', 'yo', 'ig']); export const paymentMethodEnum = z.enum([ 'cash', 'transfer', 'credit', 'momo', 'opay', 'online_paystack', ]); export const paymentSplitSchema = z.object({ method: paymentMethodEnum, amount: z.number().positive(), }); export const paymentSplitsSchema = z.array(paymentSplitSchema).min(1); export const inputMethodEnum = z.enum([ 'litepos', 'litepos_barcode', 'voice_ptt', 'voice_kiosk', 'voice_whatsapp', 'voice_call', 'telephony', 'vision', 'storefront_web', ]); export const subscriptionTierEnum = z.enum(['free', 'standard', 'pro']); export const shopStatusEnum = z.enum(['active', 'suspended', 'churned']); export const movementTypeEnum = z.enum(['sale', 'restock', 'adjustment', 'return']); export const deviceRoleEnum = z.enum(['owner', 'assistant']); export const shopOwnerSchema = z .object({ id: z.string().uuid(), name: z.string(), phone: z.string(), email: z.string().nullable(), }) .merge(timestampSchema); export const shopSchema = z .object({ id: z.string().uuid(), ownerId: z.string().uuid(), shopName: z.string(), agentName: z.string(), agentLanguage: agentLanguageEnum, whatsappPaired: z.boolean(), country: z.string(), currency: z.string(), timezone: z.string(), businessType: z.string().nullable(), location: z .object({ lat: z.number().optional(), lng: z.number().optional(), address: z.string().optional(), market: z.string().optional(), city: z.string().optional(), state: z.string().optional(), pickupHours: z.string().optional(), }) .nullable(), status: shopStatusEnum, subscriptionTier: subscriptionTierEnum, lastSyncAt: z.coerce.date().nullable(), lastActiveAt: z.coerce.date().nullable(), storefrontSlug: z.string().nullable(), storefrontEnabled: z.boolean(), paystackSubaccountCode: z.string().nullable(), storefrontDirectTransferEnabled: z.boolean(), storefrontPaymentInstructions: z.string().nullable(), }) .merge(timestampSchema); export const shopListItemSchema = z.object({ id: z.string().uuid(), shopName: z.string(), agentName: z.string(), businessType: z.string().nullable(), status: shopStatusEnum, }); export const registerOwnerAndShopSchema = z.object({ ownerName: z.string().min(1).max(100), phone: z.string().min(10).max(15), pin: z.string().min(4).max(6), otpCode: z.string().length(6), shopName: z.string().min(1).max(200), agentName: z.string().min(1).max(50), agentLanguage: agentLanguageEnum.default('en'), country: z.string().length(2).default('NG'), currency: z.string().length(3).default('NGN'), timezone: z.string().default('Africa/Lagos'), businessType: z.string().max(50).optional(), location: z .object({ lat: z.number().optional(), lng: z.number().optional(), address: z.string().optional(), market: z.string().optional(), city: z.string().optional(), state: z.string().optional(), pickupHours: z.string().optional(), }) .optional(), deviceId: z.string(), deviceName: z.string().optional(), osVersion: z.string().optional(), appVersion: z.string().optional(), installerCode: z.string().optional(), }); export const addShopSchema = z.object({ shopName: z.string().min(1).max(200), agentName: z.string().min(1).max(50), agentLanguage: agentLanguageEnum.default('en'), country: z.string().length(2).default('NG'), currency: z.string().length(3).default('NGN'), businessType: z.string().max(50).optional(), location: z .object({ lat: z.number().optional(), lng: z.number().optional(), address: z.string().optional(), market: z.string().optional(), city: z.string().optional(), state: z.string().optional(), pickupHours: z.string().optional(), }) .optional(), }); export const updateShopSchema = z.object({ ownerName: z.string().min(1).max(100).optional(), shopName: z.string().min(1).max(200).optional(), agentName: z.string().min(1).max(50).optional(), agentLanguage: agentLanguageEnum.optional(), agentPersonality: z.record(z.unknown()).optional(), businessType: z.string().max(50).optional(), location: z .object({ lat: z.number().optional(), lng: z.number().optional(), address: z.string().optional(), market: z.string().optional(), city: z.string().optional(), state: z.string().optional(), pickupHours: z.string().optional(), }) .optional(), storefrontSlug: z.union([z.string().max(80), z.null()]).optional(), storefrontEnabled: z.boolean().optional(), paystackSubaccountCode: z.string().max(100).nullable().optional(), storefrontDirectTransferEnabled: z.boolean().optional(), storefrontPaymentInstructions: z .union([z.string().max(2000), z.null()]) .optional(), }); export const shopProductSchema = z .object({ id: z.string().uuid(), name: z.string(), nameVariants: z.string().nullable(), category: z.string().nullable(), unit: z.string().nullable(), currentStock: z.number(), costPrice: z.number().nullable(), sellingPrice: z.number().nullable(), targetMarginPct: z.number().nullable(), autoReorderEnabled: z.boolean(), autoReorderMaxAmount: z.number().nullable(), preferredSupplierId: z.string().nullable(), reorderThreshold: z.number().nullable(), barcode: z.string().nullable(), iconPhotoUrl: z.string().nullable(), gridPosition: z.number().int().nullable(), gridColor: z.string().nullable(), isActive: z.boolean(), visibleOnline: z.boolean(), }) .merge(timestampSchema); export const createProductSchema = z.object({ name: z.string().min(1).max(200), nameVariants: z.string().optional(), category: z.string().optional(), unit: z.string().optional(), currentStock: z.number().min(0).default(0), costPrice: z.number().min(0).optional(), sellingPrice: z.number().min(0).optional(), targetMarginPct: z.number().min(0).max(100).optional(), autoReorderEnabled: z.boolean().optional(), autoReorderMaxAmount: z.number().min(0).optional(), preferredSupplierId: z.string().optional(), reorderThreshold: z.number().min(0).optional(), barcode: z.string().optional(), gridPosition: z.number().int().optional(), gridColor: z.string().optional(), }); export const updateProductSchema = createProductSchema.partial().extend({ iconPhotoUrl: z.string().url().optional(), visibleOnline: z.boolean().optional(), }); export const shopSaleSchema = z.object({ id: z.string().uuid(), productId: z.string().uuid(), quantity: z.number(), unitPrice: z.number(), totalAmount: z.number(), paymentMethod: paymentMethodEnum, paymentSplits: paymentSplitsSchema.nullable().optional(), inputMethod: inputMethodEnum, recordedBy: z.string(), transcription: z.string().nullable(), confidenceScore: z.number().nullable(), approved: z.boolean(), receiptPrinted: z.boolean(), recordedAt: z.coerce.date(), customerId: z.string().uuid().nullable(), callSessionId: z.string().uuid().nullable(), onlineOrderId: z.string().uuid().nullable(), transactionType: saleTransactionTypeEnum.default('sale'), originalSaleId: z.string().uuid().nullable(), staffProfileId: z.string().uuid().nullable(), }); export const shopCustomerSchema = z .object({ id: z.string().uuid(), name: z.string(), phone: z.string().nullable(), creditBalance: z.number(), totalSpent: z.number(), lastVisitAt: z.coerce.date().nullable(), }) .merge(timestampSchema); export const createCustomerSchema = z.object({ name: z.string().min(1).max(200), phone: z.string().optional(), }); export const updateCustomerSchema = z.object({ name: z.string().min(1).max(200).optional(), phone: z.string().optional(), }); export const creditHistoryEventTypeEnum = z.enum(['credit_sale', 'repayment']); export const creditHistoryEventSchema = z.object({ id: z.string().uuid(), type: creditHistoryEventTypeEnum, amount: z.number(), occurredAt: z.coerce.date(), paymentMethod: z.string().nullable(), note: z.string().nullable(), saleId: z.string().uuid().nullable(), productId: z.string().uuid().nullable(), }); export const createCreditPaymentSchema = z.object({ amount: z.number().positive(), paymentMethod: z.string().max(50).optional(), note: z.string().max(500).optional(), }); export const debtReminderChannelEnum = z.enum(['whatsapp', 'sms']); export const sendCustomerReminderSchema = z.object({ channel: debtReminderChannelEnum, }); export const sendCustomerReminderResponseSchema = z.object({ message: z.string(), channel: debtReminderChannelEnum, }); export const reEngageCustomerSchema = z.object({ /** Skip at_risk tag check when owner explicitly approves. */ ownerOverride: z.boolean().optional(), }); export const reEngageCustomerResponseSchema = z.object({ message: z.string(), messageId: z.string().uuid().optional(), }); export const createSaleSchema = z .object({ productId: z.string().uuid(), quantity: z.number().positive(), unitPrice: z.number().min(0).optional(), paymentMethod: paymentMethodEnum.default('cash'), paymentSplits: paymentSplitsSchema.optional(), inputMethod: inputMethodEnum.default('litepos'), customerId: z.string().uuid().optional(), staffProfileId: z.string().uuid().optional(), recordedAt: z.coerce.date().optional(), }) .superRefine((data, ctx) => { const hasCreditSplit = data.paymentSplits?.some((split) => split.method === 'credit') ?? false; if (hasCreditSplit && !data.customerId) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'customerId is required when payment includes credit', path: ['customerId'], }); } }); export const stockMovementSchema = z.object({ id: z.string().uuid(), productId: z.string().uuid(), movementType: movementTypeEnum, quantity: z.number(), balanceAfter: z.number(), source: z.string().nullable(), notes: z.string().nullable(), callSessionId: z.string().uuid().nullable(), createdAt: z.coerce.date(), }); export const storefrontOrderStatusFilterEnum = z.enum([ 'all', 'awaiting_payment', 'awaiting_approval', 'paid', 'reserved', 'ready', 'collected', 'cancelled', ]); export const storefrontOrderSchema = z.object({ id: z.string().uuid(), status: shopOnlineOrderStatusEnum, currency: z.string(), totalAmount: z.number(), buyerEmail: z.string(), buyerPhone: z.string().nullable(), buyerName: z.string().nullable(), itemCount: z.number(), itemsPreview: z.array(z.string()), trackingUrl: z.string().url(), createdAt: z.coerce.date(), updatedAt: z.coerce.date(), }); /** @deprecated Use storefrontOrderSchema */ export const storefrontManualOrderSchema = storefrontOrderSchema; export const updateStorefrontOrderStatusBodySchema = z.object({ action: z.enum(['approve', 'ready', 'collected', 'cancel']), }); export const updateStorefrontOrderStatusResponseSchema = z.object({ id: z.string().uuid(), status: shopOnlineOrderStatusEnum, }); export const dailyReportSchema = z.object({ id: z.string().uuid(), reportDate: z.coerce.date(), totalSales: z.number(), totalTransactions: z.number(), salesByMethod: z.object({ litepos: z.number(), litepos_barcode: z.number(), voice_ptt: z.number(), voice_kiosk: z.number(), voice_whatsapp: z.number(), voice_call: z.number(), telephony: z.number(), vision: z.number(), }), salesByPayment: z.object({ cash: z.number(), transfer: z.number(), credit: z.number(), momo: z.number(), opay: z.number(), }), topProducts: z.array( z.object({ productId: z.string().uuid(), name: z.string(), qty: z.number(), revenue: z.number(), }), ), lowStockItems: z.array( z.object({ productId: z.string().uuid(), name: z.string(), currentStock: z.number(), }), ), callSummary: z .object({ totalCalls: z.number(), totalDuration: z.number(), proactiveCalls: z.number(), actionsFromCalls: z.number(), }) .nullable(), tokensUsed: z.number().int().nullable(), sentViaWhatsapp: z.boolean(), generatedAt: z.coerce.date(), }); export type ShopOwner = z.infer; export type Shop = z.infer; export type ShopListItem = z.infer; export type RegisterOwnerAndShopInput = z.infer; export type AddShopInput = z.infer; export type UpdateShopInput = z.infer; export type ShopProduct = z.infer; export type CreateProductInput = z.infer; export type UpdateProductInput = z.infer; export type ShopSale = z.infer; export type ShopCustomer = z.infer; export type CreateCustomerInput = z.infer; export type UpdateCustomerInput = z.infer; export type CreditHistoryEvent = z.infer; export type CreateCreditPaymentInput = z.infer; export type DebtReminderChannel = z.infer; export type SendCustomerReminderInput = z.infer; export type PaymentSplit = z.infer; export type CreateSaleInput = z.infer; export type StockMovement = z.infer; export type StorefrontOrder = z.infer; export type StorefrontManualOrder = StorefrontOrder; export type UpdateStorefrontOrderStatusInput = z.infer< typeof updateStorefrontOrderStatusBodySchema >; export type DailyReport = z.infer;