proposal-system/api/src/ProposalSystem.Api/Middleware/GlobalExceptionHandler.cs

137 lines
5.4 KiB
C#
Raw Normal View History

using System.Net;
using System.Text.Json;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
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
using ProposalSystem.Application.Common;
namespace ProposalSystem.Api.Middleware;
public class GlobalExceptionHandler : IMiddleware
{
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private static string Sanitize(string? value) => (value ?? "").Replace('\r', '_').Replace('\n', '_');
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
private static ProblemDetails MakeProblem(int status, string title, string detail, string code)
{
var problem = new ProblemDetails
{
Status = status,
Title = title,
Detail = detail,
};
problem.Extensions["code"] = code; // serialized top-level via [JsonExtensionData]
return problem;
}
// Must match the MVC pipeline's serialization (camelCase + string enums):
// the 409 currentState embeds a ProposalResponse, and clients schema-validate
// it against the same shapes their 200 responses use.
private static readonly JsonSerializerOptions CamelCase = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() },
};
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
// Concurrency conflicts use SHOC's envelopes verbatim (ADR 0004), NOT
// ProblemDetails: the guarded path embeds the reloaded currentState for
// immediate client refresh; the bare-DbUpdateConcurrencyException shape
// is the safety net for any unguarded write.
if (exception is ProposalConcurrencyException concurrencyEx)
{
_logger.LogWarning("Concurrency conflict on {Method} {Path}",
Sanitize(context.Request.Method), Sanitize(context.Request.Path));
context.Response.StatusCode = StatusCodes.Status409Conflict;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize(new
{
message = concurrencyEx.Message,
currentState = concurrencyEx.CurrentState,
}, CamelCase));
return;
}
if (exception is DbUpdateConcurrencyException)
{
_logger.LogWarning("Unguarded concurrency conflict on {Method} {Path}",
Sanitize(context.Request.Method), Sanitize(context.Request.Path));
context.Response.StatusCode = StatusCodes.Status409Conflict;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize(new
{
status = "Conflict",
message = "The record was modified by another user. Refresh and retry.",
code = 409,
}, CamelCase));
return;
}
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
// Every response carries a machine-readable "code" extension (SHOC
// error-code vocabulary convention) so clients branch on codes, not
// on human-readable text.
var (statusCode, problemDetails) = exception switch
{
ValidationException validationEx => (
HttpStatusCode.BadRequest,
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
MakeProblem(400, "Validation Error",
string.Join("; ", validationEx.Errors.Select(e => e.ErrorMessage)),
"ValidationFailed")
),
BusinessRuleException businessEx => (
HttpStatusCode.UnprocessableEntity,
MakeProblem(422, "Business Rule Violation", businessEx.Message, businessEx.Code)
),
KeyNotFoundException => (
HttpStatusCode.NotFound,
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
MakeProblem(404, "Not Found", "The requested resource was not found", "NotFound")
),
UnauthorizedAccessException => (
HttpStatusCode.Unauthorized,
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
MakeProblem(401, "Unauthorized", "Authentication required", "Unauthorized")
),
InvalidOperationException => (
HttpStatusCode.BadRequest,
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
MakeProblem(400, "Invalid Operation",
"The requested operation is not valid for the current state",
"InvalidStateTransition")
),
_ => (
HttpStatusCode.InternalServerError,
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
MakeProblem(500, "Internal Server Error", "An unexpected error occurred", "InternalError")
),
};
_logger.LogError(exception, "Exception on {Method} {Path}: {Status}",
Sanitize(context.Request.Method), Sanitize(context.Request.Path), (int)statusCode);
context.Response.StatusCode = (int)statusCode;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsync(
JsonSerializer.Serialize(problemDetails, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}));
}
}