- Users page: paginated, searchable, sortable with inline roles (no N+1) - Roles page: paginated, searchable, sortable + /roles/all for dropdowns - User is_active field with migration, PATCH toggle, auth check (disabled=401) - Frontend permission checks: auth store loads permissions, sidebar/buttons conditional - Profile pictures via file storage for users and members, avatar component - Identifier images use file storage API instead of base64 - Fix TypeScript errors across admin UI - 64 API tests passing (10 new)
151 lines
5.3 KiB
TypeScript
151 lines
5.3 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
/** Coerce empty strings to undefined — solves HTML form inputs sending '' for blank optional fields */
|
|
function opt<T extends z.ZodTypeAny>(schema: T) {
|
|
return z.preprocess((v) => (v === '' ? undefined : v), schema.optional())
|
|
}
|
|
|
|
export const BillingMode = z.enum(['consolidated', 'split'])
|
|
export type BillingMode = z.infer<typeof BillingMode>
|
|
|
|
export const PaymentProcessor = z.enum(['stripe', 'global_payments'])
|
|
export type PaymentProcessor = z.infer<typeof PaymentProcessor>
|
|
|
|
export const TaxExemptStatus = z.enum(['none', 'pending', 'approved'])
|
|
export type TaxExemptStatus = z.infer<typeof TaxExemptStatus>
|
|
|
|
export const AccountCreateSchema = z.object({
|
|
name: z.string().min(1).max(255),
|
|
email: opt(z.string().email()),
|
|
phone: opt(z.string().max(50)),
|
|
address: z
|
|
.object({
|
|
street: z.string().optional(),
|
|
city: z.string().optional(),
|
|
state: z.string().optional(),
|
|
zip: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
billingMode: BillingMode.default('consolidated'),
|
|
notes: opt(z.string()),
|
|
})
|
|
export type AccountCreateInput = z.infer<typeof AccountCreateSchema>
|
|
|
|
export const AccountUpdateSchema = AccountCreateSchema.partial()
|
|
export type AccountUpdateInput = z.infer<typeof AccountUpdateSchema>
|
|
|
|
export const MemberCreateSchema = z.object({
|
|
accountId: z.string().uuid(),
|
|
firstName: z.string().min(1).max(100),
|
|
lastName: z.string().min(1).max(100),
|
|
dateOfBirth: opt(z.string().date()),
|
|
isMinor: z.boolean().optional(),
|
|
email: opt(z.string().email()),
|
|
phone: opt(z.string().max(50)),
|
|
notes: opt(z.string()),
|
|
})
|
|
export type MemberCreateInput = z.infer<typeof MemberCreateSchema>
|
|
|
|
export const MemberUpdateSchema = MemberCreateSchema.omit({ accountId: true }).partial()
|
|
export type MemberUpdateInput = z.infer<typeof MemberUpdateSchema>
|
|
|
|
export const AccountSearchSchema = z.object({
|
|
q: z.string().min(1).max(255),
|
|
})
|
|
|
|
// --- Member Identifier ---
|
|
|
|
export const IdentifierType = z.enum(['drivers_license', 'passport', 'school_id'])
|
|
export type IdentifierType = z.infer<typeof IdentifierType>
|
|
|
|
export const MemberIdentifierCreateSchema = z.object({
|
|
memberId: z.string().uuid(),
|
|
type: IdentifierType,
|
|
label: opt(z.string().max(100)),
|
|
value: z.string().min(1).max(255),
|
|
issuingAuthority: opt(z.string().max(255)),
|
|
issuedDate: opt(z.string().date()),
|
|
expiresAt: opt(z.string().date()),
|
|
imageFrontFileId: opt(z.string().uuid()),
|
|
imageBackFileId: opt(z.string().uuid()),
|
|
notes: opt(z.string()),
|
|
isPrimary: z.boolean().default(false),
|
|
})
|
|
export type MemberIdentifierCreateInput = z.infer<typeof MemberIdentifierCreateSchema>
|
|
|
|
export const MemberIdentifierUpdateSchema = MemberIdentifierCreateSchema.omit({ memberId: true }).partial()
|
|
export type MemberIdentifierUpdateInput = z.infer<typeof MemberIdentifierUpdateSchema>
|
|
|
|
// --- Account Processor Link ---
|
|
|
|
export const ProcessorLinkCreateSchema = z.object({
|
|
accountId: z.string().uuid(),
|
|
processor: PaymentProcessor,
|
|
processorCustomerId: z.string().min(1).max(255),
|
|
})
|
|
export type ProcessorLinkCreateInput = z.infer<typeof ProcessorLinkCreateSchema>
|
|
|
|
export const ProcessorLinkUpdateSchema = z.object({
|
|
isActive: z.boolean().optional(),
|
|
})
|
|
export type ProcessorLinkUpdateInput = z.infer<typeof ProcessorLinkUpdateSchema>
|
|
|
|
// --- Account Payment Method ---
|
|
|
|
export const PaymentMethodCreateSchema = z.object({
|
|
accountId: z.string().uuid(),
|
|
processor: PaymentProcessor,
|
|
processorPaymentMethodId: z.string().min(1).max(255),
|
|
cardBrand: opt(z.string().max(50)),
|
|
lastFour: opt(z.string().length(4)),
|
|
expMonth: z.number().int().min(1).max(12).optional(),
|
|
expYear: z.number().int().min(2000).max(2100).optional(),
|
|
isDefault: z.boolean().default(false),
|
|
})
|
|
export type PaymentMethodCreateInput = z.infer<typeof PaymentMethodCreateSchema>
|
|
|
|
export const PaymentMethodUpdateSchema = z.object({
|
|
isDefault: z.boolean().optional(),
|
|
requiresUpdate: z.boolean().optional(),
|
|
})
|
|
export type PaymentMethodUpdateInput = z.infer<typeof PaymentMethodUpdateSchema>
|
|
|
|
// --- Tax Exemption ---
|
|
|
|
export const TaxExemptionCreateSchema = z.object({
|
|
accountId: z.string().uuid(),
|
|
certificateNumber: z.string().min(1).max(255),
|
|
certificateType: opt(z.string().max(100)),
|
|
issuingState: opt(z.string().length(2)),
|
|
expiresAt: opt(z.string().date()),
|
|
notes: opt(z.string()),
|
|
})
|
|
export type TaxExemptionCreateInput = z.infer<typeof TaxExemptionCreateSchema>
|
|
|
|
export const TaxExemptionUpdateSchema = z.object({
|
|
certificateNumber: opt(z.string().min(1).max(255)),
|
|
certificateType: opt(z.string().max(100)),
|
|
issuingState: opt(z.string().length(2)),
|
|
expiresAt: opt(z.string().date()),
|
|
notes: opt(z.string()),
|
|
})
|
|
export type TaxExemptionUpdateInput = z.infer<typeof TaxExemptionUpdateSchema>
|
|
|
|
// --- Lookup Tables ---
|
|
|
|
export const LookupCreateSchema = z.object({
|
|
name: z.string().min(1).max(100),
|
|
slug: z.string().min(1).max(100).regex(/^[a-z0-9_]+$/, 'Slug must be lowercase alphanumeric with underscores'),
|
|
description: opt(z.string()),
|
|
sortOrder: z.number().int().default(0),
|
|
})
|
|
export type LookupCreateInput = z.infer<typeof LookupCreateSchema>
|
|
|
|
export const LookupUpdateSchema = z.object({
|
|
name: opt(z.string().min(1).max(100)),
|
|
description: opt(z.string()),
|
|
sortOrder: z.number().int().optional(),
|
|
isActive: z.boolean().optional(),
|
|
})
|
|
export type LookupUpdateInput = z.infer<typeof LookupUpdateSchema>
|