Extract versioned Model Delivery shared contracts
This commit is contained in:
commit
9133b921a7
15 files changed
+1185
No files matched your search
+140
@@ -0,0 +1,140 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const uuid = z.uuid();
|
||||
const timeValue = z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/);
|
||||
export const deliveryWindowSchema = z.object({ start: timeValue, end: timeValue }).strict().refine(value => value.start < value.end, { message: 'End time must be after start time' });
|
||||
export const deliveryWindowsSchema = z.object({
|
||||
morning: deliveryWindowSchema.nullable().default(null),
|
||||
afternoon: deliveryWindowSchema.nullable().default(null),
|
||||
lateNight: deliveryWindowSchema.nullable().default(null),
|
||||
}).strict();
|
||||
export const customerInput = z.object({
|
||||
name: z.string().trim().min(1).max(120),
|
||||
contactName: z.string().trim().max(100).default(''),
|
||||
phone: z.string().trim().max(30).regex(/^[0-9+().\-\s]*$/).default(''),
|
||||
comments: z.string().trim().max(1000).default(''),
|
||||
deliveryWindows: deliveryWindowsSchema.default({ morning: null, afternoon: null, lateNight: null }),
|
||||
}).strict();
|
||||
export const customerUpdateInput = customerInput;
|
||||
export const customerRemoveInput = z.object({}).strict();
|
||||
export const addressInput = z.object({
|
||||
customerId: uuid, label: z.string().trim().min(1).max(80), address: z.string().trim().min(1).max(300),
|
||||
city: z.string().trim().max(100).default(''), street: z.string().trim().max(120).default(''), buildingNumber: z.string().trim().max(20).default(''),
|
||||
}).strict();
|
||||
export const deliverySlotSchema = z.enum(['morning', 'afternoon', 'lateNight', 'custom']);
|
||||
export type DeliverySlot = z.infer<typeof deliverySlotSchema>;
|
||||
const deliveryTimingFields = {
|
||||
// Null is accepted only for records/operations created by protocol versions
|
||||
// that predate delivery timing. Current clients always send one of four slots.
|
||||
deliverySlot: deliverySlotSchema.nullable().default(null),
|
||||
specificTime: timeValue.nullable().default(null),
|
||||
deliveryWindow: deliveryWindowSchema.nullable().default(null),
|
||||
};
|
||||
type DeliveryTiming = { deliverySlot?: DeliverySlot | null; specificTime?: string | null; deliveryWindow?: { start: string; end: string } | null };
|
||||
function validateDeliveryTiming(value: DeliveryTiming, context: z.RefinementCtx) {
|
||||
if (value.deliverySlot === undefined) {
|
||||
if (value.specificTime !== undefined || value.deliveryWindow !== undefined) context.addIssue({ code: 'custom', path: ['deliverySlot'], message: 'Delivery slot is required with timing fields' });
|
||||
return;
|
||||
}
|
||||
if (value.deliverySlot === null) {
|
||||
if (value.specificTime !== null || value.deliveryWindow !== null) context.addIssue({ code: 'custom', path: ['deliverySlot'], message: 'Legacy delivery timing must be empty' });
|
||||
return;
|
||||
}
|
||||
if (value.deliverySlot === 'custom') {
|
||||
if (!value.specificTime) context.addIssue({ code: 'custom', path: ['specificTime'], message: 'Specific time is required' });
|
||||
if (value.deliveryWindow !== null) context.addIssue({ code: 'custom', path: ['deliveryWindow'], message: 'Custom time cannot include a delivery window' });
|
||||
return;
|
||||
}
|
||||
if (value.specificTime !== null) context.addIssue({ code: 'custom', path: ['specificTime'], message: 'Named slots cannot include a specific time' });
|
||||
}
|
||||
const deliveryInputObject = z.object({
|
||||
customerId: uuid,
|
||||
addressId: uuid,
|
||||
deliveryTypeId: uuid,
|
||||
deliveryTypeVersion: z.number().int().positive(),
|
||||
shipmentUnitHundredths: z.number().int().min(1).max(10000),
|
||||
unitPriceAgorot: z.number().int().min(0).max(100000000),
|
||||
scheduledDate: z.iso.date().nullable().default(null),
|
||||
...deliveryTimingFields,
|
||||
notes: z.string().trim().max(500),
|
||||
}).strict();
|
||||
export const deliveryInput = deliveryInputObject.superRefine(validateDeliveryTiming);
|
||||
|
||||
const envelope = { operationId: uuid, deviceId: uuid, entityId: uuid, baseVersion: z.literal(0) };
|
||||
export const deliveryUpdateInput = z.object({
|
||||
customerId: uuid,
|
||||
addressId: uuid,
|
||||
scheduledDate: z.iso.date().nullable().optional(),
|
||||
deliverySlot: deliverySlotSchema.nullable().optional(),
|
||||
specificTime: timeValue.nullable().optional(),
|
||||
deliveryWindow: deliveryWindowSchema.nullable().optional(),
|
||||
notes: z.string().trim().max(500),
|
||||
}).strict().superRefine(validateDeliveryTiming);
|
||||
export const deliveryCancelInput = z.object({ reason: z.string().trim().min(1).max(300) }).strict();
|
||||
export const operationSchema = z.discriminatedUnion('kind', [
|
||||
z.object({ ...envelope, kind: z.literal('customer.create'), data: customerInput }).strict(),
|
||||
z.object({ ...envelope, baseVersion: z.number().int().positive(), kind: z.literal('customer.update'), data: customerUpdateInput }).strict(),
|
||||
z.object({ ...envelope, baseVersion: z.number().int().positive(), kind: z.literal('customer.remove'), data: customerRemoveInput }).strict(),
|
||||
z.object({ ...envelope, kind: z.literal('address.create'), data: addressInput }).strict(),
|
||||
z.object({ ...envelope, kind: z.literal('delivery.create'), data: deliveryInput }).strict(),
|
||||
z.object({ ...envelope, baseVersion: z.number().int().positive(), kind: z.literal('delivery.update'), data: deliveryUpdateInput }).strict(),
|
||||
z.object({ ...envelope, baseVersion: z.number().int().positive(), kind: z.literal('delivery.cancel'), data: deliveryCancelInput }).strict(),
|
||||
]);
|
||||
export type Operation = z.infer<typeof operationSchema>;
|
||||
const meta = { id: uuid, version: z.number().int().positive(), createdAt: z.iso.datetime(), deletedAt: z.iso.datetime().nullable() };
|
||||
export const customerSchema = customerInput.extend({ ...meta, updatedAt: z.iso.datetime().nullable().default(null) });
|
||||
export const addressSchema = addressInput.extend(meta);
|
||||
export const deliveryTypeSchema = z.object({ ...meta, code: z.enum(['delivery', 'pickup', 'double']), name: z.string(), shipmentUnitHundredths: z.number().int().positive() });
|
||||
export const deliverySchema = deliveryInputObject.extend({
|
||||
...meta, deliveryNumber: z.number().int().positive(), totalPriceAgorot: z.number().int().nonnegative(), status: z.enum(['created', 'cancelled']),
|
||||
updatedAt: z.iso.datetime().nullable().default(null), cancelledAt: z.iso.datetime().nullable().default(null), cancellationReason: z.string().nullable().default(null),
|
||||
}).superRefine(validateDeliveryTiming);
|
||||
export type Customer = z.infer<typeof customerSchema>;
|
||||
export type CustomerAddress = z.infer<typeof addressSchema>;
|
||||
export type DeliveryType = z.infer<typeof deliveryTypeSchema>;
|
||||
export type Delivery = z.infer<typeof deliverySchema>;
|
||||
export const changeSchema = z.discriminatedUnion('entityType', [
|
||||
z.object({ entityType: z.literal('customer'), record: customerSchema }),
|
||||
z.object({ entityType: z.literal('address'), record: addressSchema }),
|
||||
z.object({ entityType: z.literal('deliveryType'), record: deliveryTypeSchema }),
|
||||
z.object({ entityType: z.literal('delivery'), record: deliverySchema }),
|
||||
]);
|
||||
export type Change = z.infer<typeof changeSchema>;
|
||||
export const pushResponseSchema = z.object({ change: changeSchema, replayed: z.boolean() });
|
||||
export const pullResponseSchema = z.object({
|
||||
changes: z.array(z.object({ sequence: z.string().regex(/^\d+$/), change: changeSchema })),
|
||||
nextCursor: z.string().regex(/^\d+$/), hasMore: z.boolean(),
|
||||
});
|
||||
export type PullResponse = z.infer<typeof pullResponseSchema>;
|
||||
export function totalPrice(unitPriceAgorot: number, shipmentUnitHundredths: number): number {
|
||||
return Math.floor((unitPriceAgorot * shipmentUnitHundredths + 50) / 100);
|
||||
}
|
||||
|
||||
export const usernameSchema = z.string().trim().toLowerCase().min(3).max(80).regex(/^[a-z0-9._@+-]+$/);
|
||||
export const loginInput = z.object({ username: usernameSchema, password: z.string().min(12).max(128) }).strict();
|
||||
export const setupInput = loginInput.extend({ displayName: z.string().trim().min(1).max(80) }).strict();
|
||||
export const userSchema = z.object({ id: uuid, username: z.string().min(1).max(255), displayName: z.string().min(1).max(80), role: z.enum(['admin', 'staff']), authProvider: z.enum(['local', 'oidc']).default('local'), groups: z.array(z.string().min(1).max(255)).max(100).default([]) });
|
||||
export type User = z.infer<typeof userSchema>;
|
||||
export const profileInput = z.object({ displayName: z.string().trim().min(1).max(80).nullable() }).strict();
|
||||
export const sessionSchema = z.object({ user: userSchema, expiresAt: z.iso.datetime() });
|
||||
export type Session = z.infer<typeof sessionSchema>;
|
||||
export const ssoSettingsSchema = z.object({
|
||||
enabled: z.boolean(), issuer: z.string().max(512), discoveryUrl: z.string().max(1024), clientId: z.string().max(255),
|
||||
clientSecret: z.string().max(4096).default(''), redirectUri: z.string().max(1024), scopes: z.string().max(500),
|
||||
authMethod: z.enum(['client_secret_post', 'client_secret_basic', 'none']), offlineAccess: z.boolean(),
|
||||
promptConsent: z.boolean(), displayNameClaim: z.string().regex(/^[a-zA-Z0-9_]+$/).max(80),
|
||||
groupAccessEnabled: z.boolean().default(false), groupClaim: z.string().regex(/^[a-zA-Z0-9_]+$/).max(80).default('groups'),
|
||||
allowedGroups: z.array(z.string().trim().min(1).max(255)).max(100).default([]),
|
||||
}).strict().superRefine((settings, context) => {
|
||||
if (settings.groupAccessEnabled && !settings.allowedGroups.length) context.addIssue({ code: 'custom', path: ['allowedGroups'], message: 'At least one allowed group is required' });
|
||||
});
|
||||
export type SsoSettings = z.infer<typeof ssoSettingsSchema>;
|
||||
|
||||
export const directorySettingsSchema = z.object({
|
||||
enabled: z.boolean(), url: z.string().trim().max(1024), baseDn: z.string().trim().max(1024),
|
||||
domain: z.string().trim().max(15).regex(/^[a-zA-Z0-9-]*$/),
|
||||
bindDn: z.string().trim().max(1024), bindPassword: z.string().max(4096),
|
||||
caCertificate: z.string().trim().max(16000),
|
||||
}).strict();
|
||||
export type DirectorySettings = z.infer<typeof directorySettingsSchema>;
|
||||
export const directoryTestInput = z.object({ subject: z.string().trim().min(1).max(255) }).strict();
|
||||
Reference in new issue
Block a user