UNPKG

model-validator-ts

Version:

[![npm version](https://img.shields.io/npm/v/model-validator-ts.svg)](https://www.npmjs.com/package/model-validator-ts)

202 lines 8.47 kB
/** * Order Cancellation Business Logic Validation Example * * This example demonstrates how to handle complex business rules for order cancellation * in an imaginary ecommerce app: * The rules are that a product can be cancelled if: * - The order is not already cancelled * - The user has permission to cancel the order (it's owned by the user or the user is an admin) * - The order is not shipped or scheduled to ship within 24 hours * - The order does not contain non-cancellable items (like personalized products or digital downloads) * - The order did not use a special discount code * - The order is not fulfilled by a third party * - The order was created within the last 10 days * - The order belongs to the requesting customer (except admin panel) */ import { z } from "zod"; import { buildValidator } from "./index.js"; // At the end of the file you can find the types of Order, Product, etc. // And also the methods of each of this services, this is moved to the // end of the file to make the example more readable. In a real app // this would be defined acrosss your entire application. // Request Schema export const orderCancellationSchema = z.object({ orderId: z.string().min(1, "Order ID is required"), customerId: z.string().min(1, "Customer ID is required"), reason: z .string() .min(10, "Cancellation reason must be at least 10 characters") .max(500, "Reason too long"), source: z.enum(["customer-portal", "admin-panel", "api"]), }); // Business Logic Validation export const orderCancellationValidator = buildValidator() .input(orderCancellationSchema) .$deps() .rule({ id: "order-exists", description: "Check if order exists and belongs to customer", fn: async ({ data, deps, bag }) => { const order = await deps.orderService.findById(data.orderId); if (!order) { return bag.addError("orderId", "Order not found"); } // Pass order to subsequent rules return { context: { order } }; }, }) .rule({ id: "order-not-cancelled", description: "Check if order is not already cancelled", fn: async ({ context, bag }) => { if (context.order.status === "cancelled") { bag.addGlobalError("Order is already cancelled"); } }, }) .rule({ id: "permission-to-cancel", description: "Check if user has permission to cancel the order", fn: async ({ context, deps, bag }) => { if (deps.user.role !== "admin" && context.order.customerId !== deps.user.id) { return bag.addGlobalError("You do not have permission to cancel this order"); } }, }) .rule({ id: "fetch-shipping-info", description: "Fetch shipping information for the order", fn: async ({ context, deps, bag }) => { // ✨ CONTEXT PASSING: Use the order from the previous rule's context const { order } = context; try { const shippingStatus = await deps.shippingService.getShippingStatus(order.shippingId); return { context: { shippingStatus } }; } catch (error) { return bag.addGlobalError("Cannot process cancellation for this order for now, please try again later"); } }, }) .rule({ id: "not-shipped-or-shipping-soon", description: "Check if order is not shipped or planned to ship within 24 hours", fn: async ({ context, bag }) => { // ✨ CONTEXT PASSING: Use shipping status from the previous rule's context if (context.shippingStatus.isShipped) { return bag.addGlobalError("Cannot cancel orders that have already been shipped"); } if (context.shippingStatus.plannedShippingDate) { const hoursUntilShipping = (context.shippingStatus.plannedShippingDate.getTime() - Date.now()) / (1000 * 60 * 60); if (hoursUntilShipping <= 24 && hoursUntilShipping > 0) { return bag.addGlobalError(`Cannot cancel orders scheduled to ship within 24 hours (ships in ${Math.round(hoursUntilShipping)} hours)`); } } }, }) .rule({ id: "all-items-cancellable", description: "Check if all items in the order are cancellable", fn: async ({ context, deps, bag }) => { // ✨ CONTEXT PASSING: Use order from previous rule's context const nonCancellableItems = []; for (const item of context.order.items) { const product = await deps.productCatalog.findById(item.productId); if (product && !product.isCancellable) { nonCancellableItems.push(`${product.name} (${product.type})`); } } if (nonCancellableItems.length > 0) { bag.addGlobalError(`Order contains non-cancellable items: ${nonCancellableItems.join(", ")}`); } }, }) .rule({ id: "no-special-discounts", description: "Check if order doesn't have special discount codes", fn: async ({ context, deps, bag }) => { if (context.order.discountCode) { const isSpecial = await deps.discountService.isSpecialDiscount(context.order.discountCode); if (isSpecial) { bag.addGlobalError("Orders with special discount codes cannot be cancelled"); } } }, }) .rule({ id: "no-third-party-fulfillment", description: "Check if order is not fulfilled by third party", fn: async ({ context, bag }) => { if (context.order.fulfillmentType === "third-party") { bag.addGlobalError("Orders fulfilled by third-party vendors cannot be cancelled through this system"); } }, }) .rule({ id: "within-time-limit", description: "Check if order was created within the last 10 days", fn: async ({ context, bag }) => { const daysSinceCreation = (Date.now() - context.order.createdAt.getTime()) / (1000 * 60 * 60 * 24); if (daysSinceCreation > 10) { bag.addGlobalError(`Order cannot be cancelled after 10 days (created ${Math.round(daysSinceCreation)} days ago)`); } }, }); // Command that combines validation + execution export const cancelOrderCommand = orderCancellationValidator.command({ execute: async ({ data, deps, context, bag }) => { try { // Cancel the order const cancelledOrder = await deps.orderService.cancelOrder(data.orderId, data.reason); // Send notification await deps.notificationService.notifyCancellation(data.orderId, context.order.customerId, data.reason); return { success: true, orderId: cancelledOrder.id, status: cancelledOrder.status, refundAmount: cancelledOrder.totalAmount, message: "Order successfully cancelled. Refund will be processed within 3-5 business days.", }; } catch (error) { // Handle execution errors bag.addGlobalError(`Failed to cancel order: ${error instanceof Error ? error.message : "Unknown error"}. Try again later.`); return bag; } }, }); // Usage Example export async function exampleUsage(dependencies) { // Customer trying to cancel their own order const result = await cancelOrderCommand.provide(dependencies).run({ orderId: "order-123", customerId: "customer-456", reason: "Changed my mind about the purchase", source: "customer-portal", }); if (result.success) { // TypeScript knows result.result exists and has the correct shape just like zod return { success: true, data: { refundAmount: result.result.refundAmount, }, }; } else { // TypeScript knows result.errors exists just like zod return { success: false, // Just a plain object you can return to the client // and they can use in the UI for a global message and under each field // {result.errors.global && <p class="error">${result.errors.global}</p>} // <input class="error" name="orderId" value="${result.data.orderId}" /> // {errors.issuses.orderId[0] && <p class="error">${errors.issuses.orderId[0]}</p>} error: result.errors.toObject(), }; } } //# sourceMappingURL=order-cancellation.example.js.map