feat(contracts): shared api-contracts adoption, zod schemas, ProblemDetails codes (#222)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* 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.
* fix(web): install shared api-contracts deps via postinstall
Web Frontend Check failed on PR #222: tsc compiles
shared/api-contracts/src/schemas.ts through the tsconfig path alias,
and module resolution for its zod import walks up from shared/, never
reaching web/node_modules. CI only ran npm ci in web/, so the shared
package's deps were absent. A postinstall hook installs them wherever
web's deps are installed (CI typecheck, web-test, deploy bundling).
Passed locally only because a stray repo-root node_modules/zod
satisfied the lookup.
2026-07-13 19:28:33 -04:00
|
|
|
// 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,
|
2026-07-14 01:18:30 -04:00
|
|
|
ProposalVersionRequest,
|
|
|
|
|
ConcurrencyConflict,
|
feat(contracts): shared api-contracts adoption, zod schemas, ProblemDetails codes (#222)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* 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.
* fix(web): install shared api-contracts deps via postinstall
Web Frontend Check failed on PR #222: tsc compiles
shared/api-contracts/src/schemas.ts through the tsconfig path alias,
and module resolution for its zod import walks up from shared/, never
reaching web/node_modules. CI only ran npm ci in web/, so the shared
package's deps were absent. A postinstall hook installs them wherever
web's deps are installed (CI typecheck, web-test, deploy bundling).
Passed locally only because a stray repo-root node_modules/zod
satisfied the lookup.
2026-07-13 19:28:33 -04:00
|
|
|
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(),
|
2026-07-14 01:18:30 -04:00
|
|
|
rowVersion: z.string(),
|
feat(contracts): shared api-contracts adoption, zod schemas, ProblemDetails codes (#222)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* 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.
* fix(web): install shared api-contracts deps via postinstall
Web Frontend Check failed on PR #222: tsc compiles
shared/api-contracts/src/schemas.ts through the tsconfig path alias,
and module resolution for its zod import walks up from shared/, never
reaching web/node_modules. CI only ran npm ci in web/, so the shared
package's deps were absent. A postinstall hook installs them wherever
web's deps are installed (CI typecheck, web-test, deploy bundling).
Passed locally only because a stray repo-root node_modules/zod
satisfied the lookup.
2026-07-13 19:28:33 -04:00
|
|
|
}) 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(),
|
2026-07-14 01:18:30 -04:00
|
|
|
rowVersion: z.string(),
|
feat(contracts): shared api-contracts adoption, zod schemas, ProblemDetails codes (#222)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* 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.
* fix(web): install shared api-contracts deps via postinstall
Web Frontend Check failed on PR #222: tsc compiles
shared/api-contracts/src/schemas.ts through the tsconfig path alias,
and module resolution for its zod import walks up from shared/, never
reaching web/node_modules. CI only ran npm ci in web/, so the shared
package's deps were absent. A postinstall hook installs them wherever
web's deps are installed (CI typecheck, web-test, deploy bundling).
Passed locally only because a stray repo-root node_modules/zod
satisfied the lookup.
2026-07-13 19:28:33 -04:00
|
|
|
}) 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(),
|
2026-07-14 01:18:30 -04:00
|
|
|
proposalVersion: z.string().optional(),
|
feat(contracts): shared api-contracts adoption, zod schemas, ProblemDetails codes (#222)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* 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.
* fix(web): install shared api-contracts deps via postinstall
Web Frontend Check failed on PR #222: tsc compiles
shared/api-contracts/src/schemas.ts through the tsconfig path alias,
and module resolution for its zod import walks up from shared/, never
reaching web/node_modules. CI only ran npm ci in web/, so the shared
package's deps were absent. A postinstall hook installs them wherever
web's deps are installed (CI typecheck, web-test, deploy bundling).
Passed locally only because a stray repo-root node_modules/zod
satisfied the lookup.
2026-07-13 19:28:33 -04:00
|
|
|
}) satisfies z.ZodType<UpdateProposalRequest>;
|
|
|
|
|
|
2026-07-14 01:18:30 -04:00
|
|
|
export const proposalVersionRequestSchema = z.object({
|
|
|
|
|
proposalVersion: z.string().optional(),
|
|
|
|
|
}) satisfies z.ZodType<ProposalVersionRequest>;
|
|
|
|
|
|
feat(contracts): shared api-contracts adoption, zod schemas, ProblemDetails codes (#222)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* 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.
* fix(web): install shared api-contracts deps via postinstall
Web Frontend Check failed on PR #222: tsc compiles
shared/api-contracts/src/schemas.ts through the tsconfig path alias,
and module resolution for its zod import walks up from shared/, never
reaching web/node_modules. CI only ran npm ci in web/, so the shared
package's deps were absent. A postinstall hook installs them wherever
web's deps are installed (CI typecheck, web-test, deploy bundling).
Passed locally only because a stray repo-root node_modules/zod
satisfied the lookup.
2026-07-13 19:28:33 -04:00
|
|
|
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(),
|
2026-07-14 01:18:30 -04:00
|
|
|
rowVersion: z.string(),
|
feat(contracts): shared api-contracts adoption, zod schemas, ProblemDetails codes (#222)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* 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.
* fix(web): install shared api-contracts deps via postinstall
Web Frontend Check failed on PR #222: tsc compiles
shared/api-contracts/src/schemas.ts through the tsconfig path alias,
and module resolution for its zod import walks up from shared/, never
reaching web/node_modules. CI only ran npm ci in web/, so the shared
package's deps were absent. A postinstall hook installs them wherever
web's deps are installed (CI typecheck, web-test, deploy bundling).
Passed locally only because a stray repo-root node_modules/zod
satisfied the lookup.
2026-07-13 19:28:33 -04:00
|
|
|
}) 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),
|
2026-07-14 01:18:30 -04:00
|
|
|
proposalVersion: z.string().optional(),
|
feat(contracts): shared api-contracts adoption, zod schemas, ProblemDetails codes (#222)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* 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.
* fix(web): install shared api-contracts deps via postinstall
Web Frontend Check failed on PR #222: tsc compiles
shared/api-contracts/src/schemas.ts through the tsconfig path alias,
and module resolution for its zod import walks up from shared/, never
reaching web/node_modules. CI only ran npm ci in web/, so the shared
package's deps were absent. A postinstall hook installs them wherever
web's deps are installed (CI typecheck, web-test, deploy bundling).
Passed locally only because a stray repo-root node_modules/zod
satisfied the lookup.
2026-07-13 19:28:33 -04:00
|
|
|
}) 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>;
|
2026-07-14 01:18:30 -04:00
|
|
|
|
|
|
|
|
// 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>>;
|