import { z } from 'zod'; const timeSlotSchema = z.object({ start: z.string().min(1).max(20), end: z.string().min(1).max(20), }); const preferredLocationSchema = z.object({ label: z.string().min(1).max(160), city: z.string().max(120).optional(), state: z.string().max(80).optional(), latitude: z.number().min(-90).max(90).optional(), longitude: z.number().min(-180).max(180).optional(), radiusMiles: z.number().nonnegative().optional(), }); const hhmmSchema = z.string().regex(/^\d{2}:\d{2}$/, 'Time must use HH:MM format'); const isoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must use YYYY-MM-DD format'); const clockInModeSchema = z.enum(['NFC_REQUIRED', 'GEO_REQUIRED', 'EITHER']); const pushProviderSchema = z.enum(['FCM', 'APNS', 'WEB_PUSH']); const pushPlatformSchema = z.enum(['IOS', 'ANDROID', 'WEB']); const shiftPositionSchema = z.object({ roleId: z.string().uuid().optional(), roleCode: z.string().min(1).max(120).optional(), roleName: z.string().min(1).max(160).optional(), workerCount: z.number().int().positive().optional(), workersNeeded: z.number().int().positive().optional(), startTime: hhmmSchema, endTime: hhmmSchema, hourlyRateCents: z.number().int().nonnegative().optional(), payRateCents: z.number().int().nonnegative().optional(), billRateCents: z.number().int().nonnegative().optional(), lunchBreakMinutes: z.number().int().nonnegative().optional(), paidBreak: z.boolean().optional(), instantBook: z.boolean().optional(), metadata: z.record(z.any()).optional(), }).superRefine((value, ctx) => { if (!value.roleId && !value.roleCode && !value.roleName) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'roleId, roleCode, or roleName is required', path: ['roleId'], }); } }); const baseOrderCreateSchema = z.object({ hubId: z.string().uuid(), vendorId: z.string().uuid().optional(), eventName: z.string().min(2).max(160), timezone: z.string().min(1).max(80).optional(), description: z.string().max(5000).optional(), notes: z.string().max(5000).optional(), serviceType: z.enum(['EVENT', 'CATERING', 'HOTEL', 'RESTAURANT', 'OTHER']).optional(), positions: z.array(shiftPositionSchema).min(1), metadata: z.record(z.any()).optional(), }); export const hubCreateSchema = z.object({ name: z.string().min(1).max(160), fullAddress: z.string().max(300).optional(), placeId: z.string().max(255).optional(), latitude: z.number().min(-90).max(90).optional(), longitude: z.number().min(-180).max(180).optional(), street: z.string().max(160).optional(), city: z.string().max(120).optional(), state: z.string().max(80).optional(), country: z.string().max(80).optional(), zipCode: z.string().max(40).optional(), costCenterId: z.string().uuid().optional(), geofenceRadiusMeters: z.number().int().positive().optional(), nfcTagId: z.string().max(255).optional(), clockInMode: clockInModeSchema.optional(), allowClockInOverride: z.boolean().optional(), }); export const hubUpdateSchema = hubCreateSchema.extend({ hubId: z.string().uuid(), }); export const hubDeleteSchema = z.object({ hubId: z.string().uuid(), reason: z.string().max(1000).optional(), }); export const hubAssignNfcSchema = z.object({ hubId: z.string().uuid(), nfcTagId: z.string().min(1).max(255), }); export const hubAssignManagerSchema = z.object({ hubId: z.string().uuid(), businessMembershipId: z.string().uuid().optional(), managerUserId: z.string().min(1).optional(), }).refine((value) => value.businessMembershipId || value.managerUserId, { message: 'businessMembershipId or managerUserId is required', }); export const invoiceApproveSchema = z.object({ invoiceId: z.string().uuid(), }); export const invoiceDisputeSchema = z.object({ invoiceId: z.string().uuid(), reason: z.string().min(3).max(2000), }); export const coverageReviewSchema = z.object({ staffId: z.string().uuid(), assignmentId: z.string().uuid().optional(), rating: z.number().int().min(1).max(5), markAsFavorite: z.boolean().optional(), issueFlags: z.array(z.string().min(1).max(80)).max(20).optional(), feedback: z.string().max(5000).optional(), }); export const cancelLateWorkerSchema = z.object({ assignmentId: z.string().uuid(), reason: z.string().max(1000).optional(), }); export const clientOneTimeOrderSchema = baseOrderCreateSchema.extend({ orderDate: isoDateSchema, }); export const clientRecurringOrderSchema = baseOrderCreateSchema.extend({ startDate: isoDateSchema, endDate: isoDateSchema, recurrenceDays: z.array(z.number().int().min(0).max(6)).min(1), }); export const clientPermanentOrderSchema = baseOrderCreateSchema.extend({ startDate: isoDateSchema, endDate: isoDateSchema.optional(), daysOfWeek: z.array(z.number().int().min(0).max(6)).min(1).optional(), horizonDays: z.number().int().min(7).max(180).optional(), }); export const clientOrderEditSchema = z.object({ orderId: z.string().uuid(), orderType: z.enum(['ONE_TIME', 'RECURRING', 'PERMANENT']).optional(), hubId: z.string().uuid().optional(), vendorId: z.string().uuid().optional(), eventName: z.string().min(2).max(160).optional(), orderDate: isoDateSchema.optional(), startDate: isoDateSchema.optional(), endDate: isoDateSchema.optional(), recurrenceDays: z.array(z.number().int().min(0).max(6)).min(1).optional(), daysOfWeek: z.array(z.number().int().min(0).max(6)).min(1).optional(), timezone: z.string().min(1).max(80).optional(), description: z.string().max(5000).optional(), notes: z.string().max(5000).optional(), serviceType: z.enum(['EVENT', 'CATERING', 'HOTEL', 'RESTAURANT', 'OTHER']).optional(), positions: z.array(shiftPositionSchema).min(1).optional(), metadata: z.record(z.any()).optional(), }).superRefine((value, ctx) => { const keys = Object.keys(value).filter((key) => key !== 'orderId'); if (keys.length === 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'At least one field must be provided to create an edited order copy', path: [], }); } }); export const clientOrderCancelSchema = z.object({ orderId: z.string().uuid(), reason: z.string().max(1000).optional(), metadata: z.record(z.any()).optional(), }); export const availabilityDayUpdateSchema = z.object({ dayOfWeek: z.number().int().min(0).max(6), availabilityStatus: z.enum(['AVAILABLE', 'UNAVAILABLE', 'PARTIAL']), slots: z.array(timeSlotSchema).max(8).optional(), metadata: z.record(z.any()).optional(), }); export const availabilityQuickSetSchema = z.object({ startDate: z.string().datetime().optional(), endDate: z.string().datetime().optional(), quickSetType: z.enum(['all', 'weekdays', 'weekends', 'clear']), slots: z.array(timeSlotSchema).max(8).optional(), }); export const shiftApplySchema = z.object({ shiftId: z.string().uuid(), roleId: z.string().uuid().optional(), instantBook: z.boolean().optional(), }); export const shiftDecisionSchema = z.object({ shiftId: z.string().uuid(), reason: z.string().max(1000).optional(), }); export const staffClockInSchema = z.object({ assignmentId: z.string().uuid().optional(), shiftId: z.string().uuid().optional(), sourceType: z.enum(['NFC', 'GEO', 'QR', 'MANUAL', 'SYSTEM']).optional(), sourceReference: z.string().max(255).optional(), nfcTagId: z.string().max(255).optional(), deviceId: z.string().max(255).optional(), latitude: z.number().min(-90).max(90).optional(), longitude: z.number().min(-180).max(180).optional(), accuracyMeters: z.number().int().nonnegative().optional(), capturedAt: z.string().datetime().optional(), proofNonce: z.string().min(8).max(255).optional(), proofTimestamp: z.string().datetime().optional(), attestationProvider: z.enum(['PLAY_INTEGRITY', 'APP_ATTEST', 'DEVICE_CHECK']).optional(), attestationToken: z.string().min(16).max(20000).optional(), isMockLocation: z.boolean().optional(), notes: z.string().max(2000).optional(), overrideReason: z.string().max(2000).optional(), rawPayload: z.record(z.any()).optional(), }).refine((value) => value.assignmentId || value.shiftId, { message: 'assignmentId or shiftId is required', }); export const staffClockOutSchema = z.object({ assignmentId: z.string().uuid().optional(), shiftId: z.string().uuid().optional(), applicationId: z.string().uuid().optional(), sourceType: z.enum(['NFC', 'GEO', 'QR', 'MANUAL', 'SYSTEM']).optional(), sourceReference: z.string().max(255).optional(), nfcTagId: z.string().max(255).optional(), deviceId: z.string().max(255).optional(), latitude: z.number().min(-90).max(90).optional(), longitude: z.number().min(-180).max(180).optional(), accuracyMeters: z.number().int().nonnegative().optional(), capturedAt: z.string().datetime().optional(), proofNonce: z.string().min(8).max(255).optional(), proofTimestamp: z.string().datetime().optional(), attestationProvider: z.enum(['PLAY_INTEGRITY', 'APP_ATTEST', 'DEVICE_CHECK']).optional(), attestationToken: z.string().min(16).max(20000).optional(), isMockLocation: z.boolean().optional(), notes: z.string().max(2000).optional(), overrideReason: z.string().max(2000).optional(), breakMinutes: z.number().int().nonnegative().optional(), rawPayload: z.record(z.any()).optional(), }).refine((value) => value.assignmentId || value.shiftId || value.applicationId, { message: 'assignmentId, shiftId, or applicationId is required', }); const locationPointSchema = z.object({ capturedAt: z.string().datetime(), latitude: z.number().min(-90).max(90).optional(), longitude: z.number().min(-180).max(180).optional(), accuracyMeters: z.number().int().nonnegative().optional(), speedMps: z.number().nonnegative().optional(), isMocked: z.boolean().optional(), metadata: z.record(z.any()).optional(), }); export const staffLocationBatchSchema = z.object({ assignmentId: z.string().uuid().optional(), shiftId: z.string().uuid().optional(), sourceType: z.enum(['NFC', 'GEO', 'QR', 'MANUAL', 'SYSTEM']).default('GEO'), deviceId: z.string().max(255).optional(), points: z.array(locationPointSchema).min(1).max(96), metadata: z.record(z.any()).optional(), }).refine((value) => value.assignmentId || value.shiftId, { message: 'assignmentId or shiftId is required', }); export const pushTokenRegisterSchema = z.object({ provider: pushProviderSchema.default('FCM'), platform: pushPlatformSchema, pushToken: z.string().min(16).max(4096), deviceId: z.string().max(255).optional(), appVersion: z.string().max(80).optional(), appBuild: z.string().max(80).optional(), locale: z.string().max(32).optional(), timezone: z.string().max(64).optional(), notificationsEnabled: z.boolean().optional(), metadata: z.record(z.any()).optional(), }); export const pushTokenDeleteSchema = z.object({ tokenId: z.string().uuid().optional(), pushToken: z.string().min(16).max(4096).optional(), reason: z.string().max(255).optional(), }).refine((value) => value.tokenId || value.pushToken, { message: 'tokenId or pushToken is required', }); export const staffProfileSetupSchema = z.object({ fullName: z.string().min(2).max(160), bio: z.string().max(5000).optional(), email: z.string().email().optional(), phoneNumber: z.string().min(6).max(40), preferredLocations: z.array(preferredLocationSchema).max(20).optional(), maxDistanceMiles: z.number().nonnegative().max(500).optional(), industries: z.array(z.string().min(1).max(80)).max(30).optional(), skills: z.array(z.string().min(1).max(80)).max(50).optional(), primaryRole: z.string().max(120).optional(), tenantId: z.string().uuid().optional(), vendorId: z.string().uuid().optional(), }); export const personalInfoUpdateSchema = z.object({ firstName: z.string().min(1).max(80).optional(), lastName: z.string().min(1).max(80).optional(), bio: z.string().max(5000).optional(), preferredLocations: z.array(preferredLocationSchema).max(20).optional(), maxDistanceMiles: z.number().nonnegative().max(500).optional(), email: z.string().email().optional(), phone: z.string().min(6).max(40).optional(), displayName: z.string().min(2).max(160).optional(), }); export const profileExperienceSchema = z.object({ industries: z.array(z.string().min(1).max(80)).max(30).optional(), skills: z.array(z.string().min(1).max(80)).max(50).optional(), primaryRole: z.string().max(120).optional(), }); export const preferredLocationsUpdateSchema = z.object({ preferredLocations: z.array(preferredLocationSchema).max(20), maxDistanceMiles: z.number().nonnegative().max(500).optional(), }); export const emergencyContactCreateSchema = z.object({ fullName: z.string().min(2).max(160), phone: z.string().min(6).max(40), relationshipType: z.string().min(1).max(120), isPrimary: z.boolean().optional(), metadata: z.record(z.any()).optional(), }); export const emergencyContactUpdateSchema = emergencyContactCreateSchema.partial().extend({ contactId: z.string().uuid(), }); const taxFormFieldsSchema = z.record(z.any()); export const taxFormDraftSchema = z.object({ formType: z.enum(['I9', 'W4']), fields: taxFormFieldsSchema, }); export const taxFormSubmitSchema = z.object({ formType: z.enum(['I9', 'W4']), fields: taxFormFieldsSchema, }); export const bankAccountCreateSchema = z.object({ bankName: z.string().min(2).max(160), accountNumber: z.string().min(4).max(34), routingNumber: z.string().min(4).max(20), accountType: z.string() .transform((value) => value.trim().toUpperCase()) .pipe(z.enum(['CHECKING', 'SAVINGS'])), }); export const privacyUpdateSchema = z.object({ profileVisible: z.boolean(), });