Files
lunarfront-app/packages/shared/src/schemas/account.schema.ts
Ryan Moon b9f78639e2 Add paginated users/roles, user status, frontend permissions, profile pictures, identifier file storage
- 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)
2026-03-29 08:16:34 -05:00

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>