Stage M5 and Stage M6

This commit is contained in:
Nông Đức Huy
2026-08-13 23:20:23 +07:00
parent 7688657d3c
commit 624a6402bf
77 changed files with 4879 additions and 176 deletions
+67
View File
@@ -0,0 +1,67 @@
import { z } from 'zod';
import { anyIdSchema } from './common';
import { offsetPageQuerySchema } from './pagination';
/**
* Cart and checkout input.
*
* A cart line carries a variant and a quantity and nothing else. There is
* deliberately no price field anywhere in this file: prices are the API's to
* compute, and accepting one from the client would make every total a
* negotiation.
*/
/** One line cannot exceed this, regardless of stock — it is a shop, not a wholesaler. */
export const MAX_LINE_QUANTITY = 20;
export const addCartLineSchema = z.object({
variantId: anyIdSchema,
quantity: z.coerce.number().int().min(1).max(MAX_LINE_QUANTITY).default(1),
});
export const updateCartLineSchema = z.object({
/** Zero removes the line, which is what a quantity stepper reaching 0 means. */
quantity: z.coerce.number().int().min(0).max(MAX_LINE_QUANTITY),
});
/** Vietnamese addresses: ward and district are optional because rural
* addresses frequently have neither, and rejecting those loses real orders. */
export const shippingAddressSchema = z.object({
fullName: z.string().trim().min(1).max(160),
phone: z
.string()
.trim()
.regex(/^(?:\+84|0)\d{8,10}$/, 'Enter a Vietnamese phone number'),
line1: z.string().trim().min(1).max(255),
line2: z.string().trim().max(255).nullish(),
ward: z.string().trim().max(120).nullish(),
district: z.string().trim().max(120).nullish(),
province: z.string().trim().min(1).max(120),
countryCode: z.string().trim().length(2).default('VN'),
postalCode: z.string().trim().max(20).nullish(),
});
export const placeOrderSchema = z.object({
email: z.string().trim().toLowerCase().email().max(255),
shippingAddress: shippingAddressSchema,
customerNote: z.string().trim().max(1000).nullish(),
});
export const orderListQuerySchema = offsetPageQuerySchema.extend({
q: z.string().trim().max(120).optional(),
status: z.enum(['PENDING', 'CONFIRMED', 'FULFILLED', 'COMPLETED', 'CANCELLED']).optional(),
});
export const updateOrderStatusSchema = z.object({
status: z.enum(['PENDING', 'CONFIRMED', 'FULFILLED', 'COMPLETED', 'CANCELLED']),
/** Required when cancelling: "why" is the whole value of the audit entry. */
reason: z.string().trim().max(500).nullish(),
});
export type AddCartLineInput = z.output<typeof addCartLineSchema>;
export type UpdateCartLineInput = z.output<typeof updateCartLineSchema>;
export type ShippingAddressInput = z.output<typeof shippingAddressSchema>;
export type PlaceOrderInput = z.output<typeof placeOrderSchema>;
export type OrderListQuery = z.output<typeof orderListQuerySchema>;
export type UpdateOrderStatusInput = z.output<typeof updateOrderStatusSchema>;
+1
View File
@@ -17,3 +17,4 @@ export * from './auth';
export * from './catalog';
export * from './users';
export * from './catalog-admin';
export * from './commerce';