proposal-system/shared/api-contracts/src/schemas.ts

295 lines
11 KiB
TypeScript

// Runtime validation schemas coupled to the wire types in ./index.
// Separate entrypoint by design: consumers that only need types (mobile)
// never pull zod. Each schema is compile-time-checked against its type via
// `satisfies z.ZodType<T>` — if a DTO and its schema drift, tsc fails here.
import { z } from 'zod';
import type {
ProposalStatus,
ServiceCategory,
Priority,
LineItemSource,
PricingMode,
UserRole,
ProposalListItem,
ProposalDetail,
CreateProposalRequest,
UpdateProposalRequest,
ProposalVersionRequest,
ConcurrencyConflict,
LineItem,
CreateLineItemRequest,
UpdateLineItemEntry,
BulkUpdateLineItemsRequest,
Customer,
CreateCustomerRequest,
UpdateCustomerRequest,
PricingLibraryItem,
CreatePricingLibraryItemRequest,
UpdatePricingLibraryItemRequest,
DashboardStats,
AuditEntry,
Site,
AuthUser,
UserProfile,
ProposalStats,
PdfVersion,
PresignedUpload,
ApiProblem,
} from './index';
// ── Enums ─────────────────────────────────────────────────────────────────
export const proposalStatusSchema = z.enum(['Draft', 'InReview', 'Approved', 'Sent', 'Revised']) satisfies z.ZodType<ProposalStatus>;
export const serviceCategorySchema = z.enum(['HVAC', 'Plumbing', 'Electrical', 'General', 'Renovation', 'Other']) satisfies z.ZodType<ServiceCategory>;
export const prioritySchema = z.enum(['Standard', 'Urgent', 'Emergency']) satisfies z.ZodType<Priority>;
export const lineItemSourceSchema = z.enum(['AI', 'Vendor', 'Manual', 'Historical']) satisfies z.ZodType<LineItemSource>;
export const pricingModeSchema = z.enum(['UnitPrice', 'TotalPrice', 'Both']) satisfies z.ZodType<PricingMode>;
export const userRoleSchema = z.enum(['Dispatcher', 'Admin', 'SysAdmin']) satisfies z.ZodType<UserRole>;
// ── Proposals ─────────────────────────────────────────────────────────────
export const proposalListItemSchema = z.object({
id: z.string(),
proposalNumber: z.string(),
customerName: z.string(),
workOrderNumber: z.string(),
serviceCategory: serviceCategorySchema,
priority: prioritySchema,
status: proposalStatusSchema,
totalBidAmount: z.number(),
submittedAt: z.string(),
submittedByName: z.string().nullable(),
assignedAdminName: z.string().nullable(),
rowVersion: z.string(),
}) satisfies z.ZodType<ProposalListItem>;
export const proposalDetailSchema = z.object({
id: z.string(),
proposalNumber: z.string(),
workOrderNumber: z.string(),
poNumber: z.string().nullable(),
customerName: z.string(),
customerAddress: z.string(),
scopeOfWork: z.string(),
refinedScope: z.string().nullable(),
serviceCategory: serviceCategorySchema,
priority: prioritySchema,
status: proposalStatusSchema,
totalBidAmount: z.number(),
vendorTotalCost: z.number().nullable(),
notes: z.string(),
submittedById: z.string(),
submittedByName: z.string().nullable(),
submittedAt: z.string(),
assignedAdminId: z.string().nullable(),
approvedById: z.string().nullable(),
approvedAt: z.string().nullable(),
sentAt: z.string().nullable(),
currentRevision: z.number(),
parentProposalId: z.string().nullable(),
createdAt: z.string(),
updatedAt: z.string(),
rowVersion: z.string(),
}) satisfies z.ZodType<ProposalDetail>;
export const createProposalRequestSchema = z.object({
workOrderNumber: z.string().min(1),
poNumber: z.string().optional(),
customerName: z.string().min(1),
customerAddress: z.string().min(1),
scopeOfWork: z.string().min(1),
serviceCategory: serviceCategorySchema,
priority: prioritySchema,
notes: z.string().optional(),
}) satisfies z.ZodType<CreateProposalRequest>;
export const updateProposalRequestSchema = z.object({
refinedScope: z.string().optional(),
notes: z.string().optional(),
poNumber: z.string().optional(),
workOrderNumber: z.string().optional(),
assignedAdminId: z.string().optional(),
proposalVersion: z.string().optional(),
}) satisfies z.ZodType<UpdateProposalRequest>;
export const proposalVersionRequestSchema = z.object({
proposalVersion: z.string().optional(),
}) satisfies z.ZodType<ProposalVersionRequest>;
export const proposalStatsSchema = z.object({
totalCount: z.number(),
inReviewCount: z.number(),
approvedCount: z.number(),
sentCount: z.number(),
}) satisfies z.ZodType<ProposalStats>;
// ── Line items ────────────────────────────────────────────────────────────
export const lineItemSchema = z.object({
id: z.string(),
proposalId: z.string(),
description: z.string(),
quantity: z.number(),
unit: z.string(),
unitPrice: z.number().nullable(),
totalPrice: z.number(),
pricingMode: pricingModeSchema,
sortOrder: z.number(),
source: lineItemSourceSchema,
createdAt: z.string(),
updatedAt: z.string(),
rowVersion: z.string(),
}) satisfies z.ZodType<LineItem>;
export const createLineItemRequestSchema = z.object({
description: z.string().min(1),
quantity: z.number(),
unit: z.string(),
unitPrice: z.number().nullable(),
totalPrice: z.number(),
pricingMode: pricingModeSchema,
sortOrder: z.number(),
source: lineItemSourceSchema,
}) satisfies z.ZodType<CreateLineItemRequest>;
export const updateLineItemEntrySchema = z.object({
id: z.string().nullable(),
description: z.string().min(1),
quantity: z.number(),
unit: z.string(),
unitPrice: z.number().nullable(),
totalPrice: z.number(),
pricingMode: pricingModeSchema,
sortOrder: z.number(),
source: lineItemSourceSchema,
}) satisfies z.ZodType<UpdateLineItemEntry>;
export const bulkUpdateLineItemsRequestSchema = z.object({
lineItems: z.array(updateLineItemEntrySchema),
proposalVersion: z.string().optional(),
}) satisfies z.ZodType<BulkUpdateLineItemsRequest>;
// ── Customers ─────────────────────────────────────────────────────────────
export const customerSchema = z.object({
id: z.string(),
name: z.string(),
addresses: z.array(z.string()),
contactEmail: z.string().nullable(),
createdAt: z.string(),
}) satisfies z.ZodType<Customer>;
export const createCustomerRequestSchema = z.object({
name: z.string().min(1),
addresses: z.array(z.string()).optional(),
contactEmail: z.string().optional(),
}) satisfies z.ZodType<CreateCustomerRequest>;
export const updateCustomerRequestSchema = z.object({
name: z.string().optional(),
addresses: z.array(z.string()).optional(),
contactEmail: z.string().nullable().optional(),
}) satisfies z.ZodType<UpdateCustomerRequest>;
// ── Pricing library ───────────────────────────────────────────────────────
export const pricingLibraryItemSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string().nullable(),
serviceCategory: serviceCategorySchema,
unit: z.string().nullable(),
unitPrice: z.number().nullable(),
keywords: z.string().nullable(),
source: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
}) satisfies z.ZodType<PricingLibraryItem>;
export const createPricingLibraryItemRequestSchema = z.object({
title: z.string().min(1),
description: z.string().optional(),
serviceCategory: serviceCategorySchema,
unit: z.string().optional(),
unitPrice: z.number().nullable().optional(),
keywords: z.string().optional(),
source: z.string().optional(),
}) satisfies z.ZodType<CreatePricingLibraryItemRequest>;
export const updatePricingLibraryItemRequestSchema = z.object({
title: z.string().optional(),
description: z.string().optional(),
serviceCategory: serviceCategorySchema.optional(),
unit: z.string().optional(),
unitPrice: z.number().nullable().optional(),
keywords: z.string().optional(),
}) satisfies z.ZodType<UpdatePricingLibraryItemRequest>;
// ── Admin ─────────────────────────────────────────────────────────────────
export const dashboardStatsSchema = z.object({
pendingCount: z.number(),
approvedThisWeek: z.number(),
avgTurnaroundHours: z.number(),
totalProposals: z.number(),
}) satisfies z.ZodType<DashboardStats>;
export const auditEntrySchema = z.object({
id: z.string(),
proposalId: z.string().nullable(),
userId: z.string(),
userName: z.string().nullable(),
action: z.string(),
details: z.string().nullable(),
timestamp: z.string(),
ipAddress: z.string().nullable(),
}) satisfies z.ZodType<AuditEntry>;
// ── Sites ─────────────────────────────────────────────────────────────────
export const siteSchema = z.object({
siteCode: z.string(),
fullAddress: z.string().nullable(),
address: z.string().nullable(),
city: z.string().nullable(),
state: z.string().nullable(),
zip: z.string().nullable(),
}) satisfies z.ZodType<Site>;
// ── Auth / users ──────────────────────────────────────────────────────────
export const authUserSchema = z.object({
id: z.string(),
email: z.string(),
displayName: z.string(),
role: userRoleSchema,
token: z.string(),
}) satisfies z.ZodType<AuthUser>;
export const userProfileSchema = z.object({
id: z.string(),
email: z.string(),
displayName: z.string(),
role: userRoleSchema,
}) satisfies z.ZodType<UserProfile>;
// ── Files / PDFs ──────────────────────────────────────────────────────────
export const pdfVersionSchema = z.object({
revision: z.number(),
generatedAt: z.string(),
}) satisfies z.ZodType<PdfVersion>;
export const presignedUploadSchema = z.object({
uploadUrl: z.string(),
s3Key: z.string(),
expiresAt: z.string(),
vendorProposalId: z.string(),
}) satisfies z.ZodType<PresignedUpload>;
// ── Errors ────────────────────────────────────────────────────────────────
export const apiProblemSchema = z.object({
status: z.number(),
title: z.string(),
detail: z.string(),
code: z.string(),
}) satisfies z.ZodType<ApiProblem>;
// 409 concurrency-conflict envelope. The proposal aggregate is the only
// guarded resource today, so the concrete schema is coupled to ProposalDetail;
// build others via the same pattern when new aggregates gain guards.
export const proposalConcurrencyConflictSchema = z.object({
message: z.string(),
currentState: proposalDetailSchema.nullable(),
}) satisfies z.ZodType<ConcurrencyConflict<ProposalDetail>>;