proposal-system/shared/api-contracts/src/index.ts
Adam Moussa 8dc8f7814b
feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):

- shared/api-contracts: rewritten as the authoritative superset of the
  .NET DTOs (ProposalListItem/ProposalDetail with poNumber and
  submittedByName, line item requests, customers, pricing library,
  dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
  Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
  every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
  a compile error); separate entrypoint so type-only consumers (mobile)
  never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
  paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
  types so page imports stay stable; enum unions tightened
  (PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
  API silently dropped (contract is addresses: string[], CustomerDtos.cs)
  - addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
  `code` (SHOC error-code vocabulary): ValidationFailed,
  InvalidStateTransition, NotFound, Unauthorized, InternalError; new
  BusinessRuleException(code, message) maps to 422 with its code;
  GlobalExceptionHandlerTests cover the full mapping (wire contract)

Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.

Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
2026-07-13 17:19:42 -04:00

287 lines
7.9 KiB
TypeScript

// @proposal-system/api-contracts — single source of truth for API wire types.
// Fix: WEB-M5 — web and mobile both import these; shapes mirror the .NET DTOs
// in api/src/ProposalSystem.Application/DTOs/ (JsonStringEnumConverter → string
// enums, Guid → string, DateTime → ISO string, decimal → number).
// Runtime validation schemas live in ./schemas (separate entrypoint so
// consumers that only need types never pull zod).
// ── Enums (string over the wire) ──────────────────────────────────────────
export type ProposalStatus = 'Draft' | 'InReview' | 'Approved' | 'Sent' | 'Revised';
export type ServiceCategory = 'HVAC' | 'Plumbing' | 'Electrical' | 'General' | 'Renovation' | 'Other';
export type Priority = 'Standard' | 'Urgent' | 'Emergency';
export type LineItemSource = 'AI' | 'Vendor' | 'Manual' | 'Historical';
export type PricingMode = 'UnitPrice' | 'TotalPrice' | 'Both';
export type UserRole = 'Dispatcher' | 'Admin' | 'SysAdmin';
// ── Shared envelopes ──────────────────────────────────────────────────────
export interface PagedResponse<T> {
items: T[];
totalCount: number;
page: number;
pageSize: number;
}
// ── Proposals (ProposalDtos.cs) ───────────────────────────────────────────
export interface ProposalListItem {
id: string;
proposalNumber: string;
customerName: string;
workOrderNumber: string;
serviceCategory: ServiceCategory;
priority: Priority;
status: ProposalStatus;
totalBidAmount: number;
submittedAt: string;
submittedByName: string | null;
assignedAdminName: string | null;
}
export interface ProposalDetail {
id: string;
proposalNumber: string;
workOrderNumber: string;
poNumber: string | null;
customerName: string;
customerAddress: string;
scopeOfWork: string;
refinedScope: string | null;
serviceCategory: ServiceCategory;
priority: Priority;
status: ProposalStatus;
totalBidAmount: number;
vendorTotalCost: number | null;
notes: string;
submittedById: string;
submittedByName: string | null;
submittedAt: string;
assignedAdminId: string | null;
approvedById: string | null;
approvedAt: string | null;
sentAt: string | null;
currentRevision: number;
parentProposalId: string | null;
createdAt: string;
updatedAt: string;
}
export interface CreateProposalRequest {
workOrderNumber: string;
poNumber?: string;
customerName: string;
customerAddress: string;
scopeOfWork: string;
serviceCategory: ServiceCategory;
priority: Priority;
notes?: string;
}
export interface UpdateProposalRequest {
refinedScope?: string;
notes?: string;
poNumber?: string;
workOrderNumber?: string;
assignedAdminId?: string;
}
export interface ProposalFilters {
search?: string;
page?: number;
pageSize?: number;
status?: string;
serviceCategory?: string;
priority?: string;
fromDate?: string;
toDate?: string;
mine?: boolean;
}
export interface ProposalStats {
totalCount: number;
inReviewCount: number;
approvedCount: number;
sentCount: number;
}
// ── Line items (LineItemDtos.cs) ──────────────────────────────────────────
export interface LineItem {
id: string;
proposalId: string;
description: string;
quantity: number;
unit: string;
unitPrice: number | null;
totalPrice: number;
pricingMode: PricingMode;
sortOrder: number;
source: LineItemSource;
createdAt: string;
updatedAt: string;
}
export interface CreateLineItemRequest {
description: string;
quantity: number;
unit: string;
unitPrice: number | null;
totalPrice: number;
pricingMode: PricingMode;
sortOrder: number;
source: LineItemSource;
}
export interface UpdateLineItemEntry {
id: string | null;
description: string;
quantity: number;
unit: string;
unitPrice: number | null;
totalPrice: number;
pricingMode: PricingMode;
sortOrder: number;
source: LineItemSource;
}
export interface BulkUpdateLineItemsRequest {
lineItems: UpdateLineItemEntry[];
}
// ── Customers (CustomerDtos.cs) ───────────────────────────────────────────
export interface Customer {
id: string;
name: string;
addresses: string[];
contactEmail: string | null;
createdAt: string;
}
export interface CustomerListParams {
page?: number;
pageSize?: number;
search?: string;
}
export interface CreateCustomerRequest {
name: string;
addresses?: string[];
contactEmail?: string;
}
export interface UpdateCustomerRequest {
name?: string;
addresses?: string[];
contactEmail?: string | null;
}
// ── Pricing library (PricingLibraryDtos.cs) ───────────────────────────────
export interface PricingLibraryItem {
id: string;
title: string;
description: string | null;
serviceCategory: ServiceCategory;
unit: string | null;
unitPrice: number | null;
keywords: string | null;
source: string;
createdAt: string;
updatedAt: string;
}
export interface PricingLibraryListParams {
page?: number;
pageSize?: number;
}
export interface CreatePricingLibraryItemRequest {
title: string;
description?: string;
serviceCategory: ServiceCategory;
unit?: string;
unitPrice?: number | null;
keywords?: string;
source?: string;
}
export interface UpdatePricingLibraryItemRequest {
title?: string;
description?: string;
serviceCategory?: ServiceCategory;
unit?: string;
unitPrice?: number | null;
keywords?: string;
}
// ── Admin (DashboardDtos.cs, AuditDtos.cs) ────────────────────────────────
export interface DashboardStats {
pendingCount: number;
approvedThisWeek: number;
avgTurnaroundHours: number;
totalProposals: number;
}
export interface AuditEntry {
id: string;
proposalId: string | null;
userId: string;
userName: string | null;
action: string;
details: string | null;
timestamp: string;
ipAddress: string | null;
}
// ── Sites (SiteDtos.cs) ───────────────────────────────────────────────────
export interface Site {
siteCode: string;
fullAddress: string | null;
address: string | null;
city: string | null;
state: string | null;
zip: string | null;
}
// ── Auth / users (UserDtos.cs, AuthController) ────────────────────────────
export interface AuthUser {
id: string;
email: string;
displayName: string;
role: UserRole;
token: string;
}
export interface UserProfile {
id: string;
email: string;
displayName: string;
role: UserRole;
}
// ── Files / PDFs (FileDtos.cs) ────────────────────────────────────────────
export interface PdfVersion {
revision: number;
generatedAt: string;
}
export interface PresignedUpload {
uploadUrl: string;
s3Key: string;
expiresAt: string;
vendorProposalId: string;
}
// ── Errors (GlobalExceptionHandler.cs) ────────────────────────────────────
// RFC 7807 ProblemDetails + machine-readable business code (SHOC error-code
// vocabulary convention). Branch on `code`, never on title/detail text.
export type ApiProblemCode =
| 'ValidationFailed'
| 'InvalidStateTransition'
| 'NotFound'
| 'Unauthorized'
| 'InternalError'
| (string & {}); // business codes are open-ended (e.g. future CancelNotAllowed)
export interface ApiProblem {
status: number;
title: string;
detail: string;
code: ApiProblemCode;
}