Refactor API Response Handling and Update Service Implementations

- Removed the TransformInterceptor to streamline response handling, ensuring that all responses are returned directly without a success envelope.
- Updated various controllers and services to utilize new action response schemas, enhancing clarity and consistency in API responses.
- Refactored error handling in the CsrfController and CheckoutController to improve logging and error management.
- Cleaned up unused imports and optimized code structure for better maintainability and clarity across the application.
This commit is contained in:
barsa 2025-12-29 11:12:20 +09:00
parent fb682c4c44
commit 2a1b4d93ed
30 changed files with 639 additions and 553 deletions

View File

@ -4,7 +4,6 @@ import { RouterModule } from "@nestjs/core";
import { ConfigModule } from "@nestjs/config";
import { ScheduleModule } from "@nestjs/schedule";
import { ZodSerializerInterceptor, ZodValidationPipe } from "nestjs-zod";
import { TransformInterceptor } from "@bff/core/http/transform.interceptor.js";
// Configuration
import { appConfig } from "@bff/core/config/app.config.js";
@ -106,10 +105,6 @@ import { HealthModule } from "@bff/modules/health/health.module.js";
provide: APP_PIPE,
useClass: ZodValidationPipe,
},
{
provide: APP_INTERCEPTOR,
useClass: TransformInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: ZodSerializerInterceptor,

View File

@ -125,7 +125,55 @@ export async function bootstrap(): Promise<INestApplication> {
const port = Number(configService.get("BFF_PORT", 4000));
await app.listen(port, "0.0.0.0");
// #region agent log
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "bootstrap.ts:before-listen",
message: "About to call app.listen",
data: { port },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "D",
}),
}).catch(() => {});
// #endregion
try {
await app.listen(port, "0.0.0.0");
// #region agent log
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "bootstrap.ts:after-listen",
message: "app.listen completed",
data: { port },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "D",
}),
}).catch(() => {});
// #endregion
} catch (listenError) {
const err = listenError as Error;
// #region agent log
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "bootstrap.ts:listen-error",
message: "app.listen failed",
data: { error: err?.message, stack: err?.stack, name: err?.name },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "D",
}),
}).catch(() => {});
// #endregion
throw listenError;
}
// Enhanced startup information
logger.log(`🚀 BFF API running on: http://localhost:${port}/api`);

View File

@ -1,75 +0,0 @@
import {
Injectable,
type NestInterceptor,
type ExecutionContext,
type CallHandler,
} from "@nestjs/common";
import { SetMetadata } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Observable, map } from "rxjs";
import type { ApiSuccessResponse } from "@customer-portal/domain/common";
export const SKIP_SUCCESS_ENVELOPE_KEY = "bff:skip-success-envelope";
/**
* Opt-out decorator for endpoints that must not be wrapped in `{ success: true, data }`,
* e.g. SSE streams or file downloads.
*/
export const SkipSuccessEnvelope = () => SetMetadata(SKIP_SUCCESS_ENVELOPE_KEY, true);
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object";
}
function isLikelyStream(value: unknown): boolean {
// Avoid wrapping Node streams (file downloads / SSE internals).
return isRecord(value) && typeof (value as { pipe?: unknown }).pipe === "function";
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiSuccessResponse<T>> {
constructor(private readonly reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler<T>): Observable<ApiSuccessResponse<T>> {
if (context.getType() !== "http") {
// Only wrap HTTP responses.
return next.handle() as unknown as Observable<ApiSuccessResponse<T>>;
}
const skip =
this.reflector.getAllAndOverride<boolean>(SKIP_SUCCESS_ENVELOPE_KEY, [
context.getHandler(),
context.getClass(),
]) ?? false;
if (skip) {
return next.handle() as unknown as Observable<ApiSuccessResponse<T>>;
}
const req = context.switchToHttp().getRequest<{ originalUrl?: string; url?: string }>();
const url = req?.originalUrl ?? req?.url ?? "";
// Only enforce success envelopes on the public API surface under `/api`.
// Keep non-API endpoints (e.g. `/health`) untouched for operational tooling.
if (!url.startsWith("/api")) {
return next.handle() as unknown as Observable<ApiSuccessResponse<T>>;
}
return next.handle().pipe(
map(data => {
// Keep already-wrapped responses as-is (ack/message/data variants).
if (isRecord(data) && "success" in data) {
return data as unknown as ApiSuccessResponse<T>;
}
// Avoid wrapping streams/buffers that are handled specially by Nest/Express.
if (isLikelyStream(data)) {
return data as unknown as ApiSuccessResponse<T>;
}
const normalized = (data === undefined ? null : data) as T;
return { success: true as const, data: normalized };
})
);
}
}

View File

@ -22,35 +22,83 @@ export class CsrfController {
@Public()
@Get("token")
getCsrfToken(@Req() req: AuthenticatedRequest, @Res() res: Response) {
const sessionId = this.extractSessionId(req) || undefined;
const userId = req.user?.id;
// #region agent log
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "csrf.controller.ts:getCsrfToken-entry",
message: "CSRF token endpoint called",
data: {},
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "A",
}),
}).catch(() => {});
// #endregion
try {
const sessionId = this.extractSessionId(req) || undefined;
const userId = req.user?.id;
// Generate new CSRF token
const tokenData = this.csrfService.generateToken(undefined, sessionId, userId);
const isProduction = this.configService.get("NODE_ENV") === "production";
const cookieName = this.csrfService.getCookieName();
// Generate new CSRF token
const tokenData = this.csrfService.generateToken(undefined, sessionId, userId);
const isProduction = this.configService.get("NODE_ENV") === "production";
const cookieName = this.csrfService.getCookieName();
// Set CSRF secret in secure cookie
res.cookie(cookieName, tokenData.secret, {
httpOnly: true,
secure: isProduction,
sameSite: "strict",
maxAge: this.csrfService.getTokenTtl(),
path: "/api",
});
// Set CSRF secret in secure cookie
res.cookie(cookieName, tokenData.secret, {
httpOnly: true,
secure: isProduction,
sameSite: "strict",
maxAge: this.csrfService.getTokenTtl(),
path: "/api",
});
this.logger.debug("CSRF token requested", {
userId,
sessionId,
userAgent: req.get("user-agent"),
ip: req.ip,
});
this.logger.debug("CSRF token requested", {
userId,
sessionId,
userAgent: req.get("user-agent"),
ip: req.ip,
});
return res.json({
success: true,
token: tokenData.token,
expiresAt: tokenData.expiresAt.toISOString(),
});
// #region agent log
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "csrf.controller.ts:getCsrfToken-success",
message: "CSRF token generated successfully",
data: { hasToken: !!tokenData.token },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "A",
}),
}).catch(() => {});
// #endregion
return res.json({
success: true,
token: tokenData.token,
expiresAt: tokenData.expiresAt.toISOString(),
});
} catch (error) {
// #region agent log
const err = error as Error;
fetch("http://127.0.0.1:7242/ingest/a683e422-cfe7-4556-a583-809fbfbeeb4a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
location: "csrf.controller.ts:getCsrfToken-error",
message: "CSRF token generation failed",
data: { error: err?.message, stack: err?.stack },
timestamp: Date.now(),
sessionId: "debug-session",
hypothesisId: "A",
}),
}).catch(() => {});
// #endregion
throw error;
}
}
@Public()

View File

@ -53,15 +53,22 @@ export class RealtimePubSubService implements OnModuleInit, OnModuleDestroy {
}
async onModuleDestroy(): Promise<void> {
if (!this.subscriber) return;
// Capture the connection reference up-front and null out the field early.
// This makes shutdown idempotent and avoids races if the hook is invoked more than once.
const subscriber = this.subscriber;
if (!subscriber) return;
this.subscriber = null;
this.handlers.clear();
try {
await this.subscriber.unsubscribe(this.CHANNEL);
await this.subscriber.quit();
await subscriber.unsubscribe(this.CHANNEL);
await subscriber.quit();
} catch {
this.subscriber.disconnect();
} finally {
this.subscriber = null;
this.handlers.clear();
// Best-effort immediate close if graceful shutdown fails.
try {
subscriber.disconnect();
} catch {
// ignore
}
}
}

View File

@ -34,6 +34,17 @@ for (const signal of signals) {
});
}
process.on("uncaughtException", error => {
logger.error(`Uncaught Exception: ${error.message}`, error.stack);
process.exit(1);
});
process.on("unhandledRejection", reason => {
const message = reason instanceof Error ? reason.message : String(reason);
const stack = reason instanceof Error ? reason.stack : undefined;
logger.error(`Unhandled Rejection: ${message}`, stack);
});
void bootstrap()
.then(startedApp => {
app = startedApp;

View File

@ -15,10 +15,7 @@ import {
type NotificationListResponse,
} from "@customer-portal/domain/notifications";
import { notificationQuerySchema } from "@customer-portal/domain/notifications";
import {
apiSuccessAckResponseSchema,
type ApiSuccessAckResponse,
} from "@customer-portal/domain/common";
import { actionAckResponseSchema, type ActionAckResponse } from "@customer-portal/domain/common";
import { createZodDto, ZodResponse } from "nestjs-zod";
class NotificationQueryDto extends createZodDto(notificationQuerySchema) {}
@ -27,7 +24,7 @@ class NotificationListResponseDto extends createZodDto(notificationListResponseS
class NotificationUnreadCountResponseDto extends createZodDto(
notificationUnreadCountResponseSchema
) {}
class ApiSuccessAckResponseDto extends createZodDto(apiSuccessAckResponseSchema) {}
class ActionAckResponseDto extends createZodDto(actionAckResponseSchema) {}
@Controller("notifications")
@UseGuards(RateLimitGuard)
@ -69,13 +66,13 @@ export class NotificationsController {
*/
@Post(":id/read")
@RateLimit({ limit: 60, ttl: 60 })
@ZodResponse({ description: "Mark as read", type: ApiSuccessAckResponseDto })
@ZodResponse({ description: "Mark as read", type: ActionAckResponseDto })
async markAsRead(
@Req() req: RequestWithUser,
@Param() params: NotificationIdParamDto
): Promise<ApiSuccessAckResponse> {
): Promise<ActionAckResponse> {
await this.notificationService.markAsRead(params.id, req.user.id);
return { success: true };
return {};
}
/**
@ -83,10 +80,10 @@ export class NotificationsController {
*/
@Post("read-all")
@RateLimit({ limit: 10, ttl: 60 })
@ZodResponse({ description: "Mark all as read", type: ApiSuccessAckResponseDto })
async markAllAsRead(@Req() req: RequestWithUser): Promise<ApiSuccessAckResponse> {
@ZodResponse({ description: "Mark all as read", type: ActionAckResponseDto })
async markAllAsRead(@Req() req: RequestWithUser): Promise<ActionAckResponse> {
await this.notificationService.markAllAsRead(req.user.id);
return { success: true };
return {};
}
/**
@ -94,12 +91,12 @@ export class NotificationsController {
*/
@Post(":id/dismiss")
@RateLimit({ limit: 60, ttl: 60 })
@ZodResponse({ description: "Dismiss notification", type: ApiSuccessAckResponseDto })
@ZodResponse({ description: "Dismiss notification", type: ActionAckResponseDto })
async dismiss(
@Req() req: RequestWithUser,
@Param() params: NotificationIdParamDto
): Promise<ApiSuccessAckResponse> {
): Promise<ActionAckResponse> {
await this.notificationService.dismiss(params.id, req.user.id);
return { success: true };
return {};
}
}

View File

@ -79,24 +79,33 @@ export class CheckoutController {
orderType: body.orderType,
});
const cart = await this.checkoutService.buildCart(
body.orderType,
body.selections,
body.configuration,
req.user?.id
);
try {
const cart = await this.checkoutService.buildCart(
body.orderType,
body.selections,
body.configuration,
req.user?.id
);
const session = await this.checkoutSessions.createSession(body, cart);
const session = await this.checkoutSessions.createSession(body, cart);
return {
sessionId: session.sessionId,
expiresAt: session.expiresAt,
orderType: body.orderType,
cart: {
items: cart.items,
totals: cart.totals,
},
};
return {
sessionId: session.sessionId,
expiresAt: session.expiresAt,
orderType: body.orderType,
cart: {
items: cart.items,
totals: cart.totals,
},
};
} catch (error) {
this.logger.error("Failed to create checkout session", {
error: error instanceof Error ? error.message : String(error),
userId: req.user?.id,
orderType: body.orderType,
});
throw error;
}
}
@Get("session/:sessionId")

View File

@ -31,7 +31,6 @@ import { SalesforceReadThrottleGuard } from "@bff/integrations/salesforce/guards
import { SalesforceWriteThrottleGuard } from "@bff/integrations/salesforce/guards/salesforce-write-throttle.guard.js";
import { CheckoutService } from "./services/checkout.service.js";
import { CheckoutSessionService } from "./services/checkout-session.service.js";
import { SkipSuccessEnvelope } from "@bff/core/http/transform.interceptor.js";
class CreateOrderRequestDto extends createZodDto(createOrderRequestSchema) {}
class CheckoutSessionCreateOrderDto extends createZodDto(checkoutSessionCreateOrderRequestSchema) {}
@ -156,7 +155,6 @@ export class OrdersController {
@Sse(":sfOrderId/events")
@UseGuards(SalesforceReadThrottleGuard)
@SkipSuccessEnvelope()
async streamOrderUpdates(
@Request() req: RequestWithUser,
@Param() params: SfOrderIdParamDto

View File

@ -35,6 +35,29 @@ export class CheckoutService {
private readonly vpnCatalogService: VpnServicesService
) {}
/**
* Produce a safe-to-log summary of user selections.
* Avoid logging PII/sensitive identifiers (EID, MNP reservation numbers, names, etc.).
*/
private summarizeSelectionsForLog(selections: OrderSelections): Record<string, unknown> {
const addons = this.collectAddonRefs(selections);
const normalizeBool = (value?: string) =>
value === "true" ? true : value === "false" ? false : undefined;
return {
planSku: selections.planSku,
installationSku: selections.installationSku,
addonSku: selections.addonSku,
addonsCount: addons.length,
activationType: selections.activationType,
simType: selections.simType,
isMnp: normalizeBool(selections.isMnp),
hasEid: typeof selections.eid === "string" && selections.eid.trim().length > 0,
hasMnpNumber:
typeof selections.mnpNumber === "string" && selections.mnpNumber.trim().length > 0,
};
}
/**
* Build checkout cart from order type and selections
*/
@ -44,7 +67,11 @@ export class CheckoutService {
configuration?: OrderConfigurations,
userId?: string
): Promise<CheckoutCart> {
this.logger.log("Building checkout cart", { orderType, selections });
this.logger.log("Building checkout cart", {
userId,
orderType,
selections: this.summarizeSelectionsForLog(selections),
});
try {
const items: CheckoutItem[] = [];
@ -93,8 +120,9 @@ export class CheckoutService {
} catch (error) {
this.logger.error("Failed to build checkout cart", {
error: getErrorMessage(error),
userId,
orderType,
selections,
selections: this.summarizeSelectionsForLog(selections),
});
throw error;
}

View File

@ -18,7 +18,6 @@ import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { Logger } from "nestjs-pino";
import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js";
import { RealtimeConnectionLimiterService } from "./realtime-connection-limiter.service.js";
import { SkipSuccessEnvelope } from "@bff/core/http/transform.interceptor.js";
@Controller("events")
export class RealtimeController {
@ -40,7 +39,6 @@ export class RealtimeController {
@RateLimit({ limit: 30, ttl: 60 }) // protect against reconnect storms / refresh spam
@Header("Cache-Control", "private, no-store")
@Header("X-Accel-Buffering", "no") // nginx: disable response buffering for SSE
@SkipSuccessEnvelope()
async stream(@Request() req: RequestWithUser): Promise<Observable<MessageEvent>> {
if (!this.limiter.tryAcquire(req.user.id)) {
throw new HttpException(

View File

@ -57,7 +57,6 @@ export class InternetController {
): Promise<SubscriptionActionResponse> {
await this.internetCancellationService.submitCancellation(req.user.id, params.id, body);
return {
success: true,
message: `Internet cancellation scheduled for end of ${body.cancellationMonth}`,
};
}

View File

@ -158,7 +158,7 @@ export class SimController {
@Body() body: SimTopupRequestDto
): Promise<SimActionResponse> {
await this.simManagementService.topUpSim(req.user.id, params.id, body);
return { success: true, message: "SIM top-up completed successfully" };
return { message: "SIM top-up completed successfully" };
}
@Post(":id/sim/change-plan")
@ -170,7 +170,6 @@ export class SimController {
): Promise<SimPlanChangeResult> {
const result = await this.simManagementService.changeSimPlan(req.user.id, params.id, body);
return {
success: true,
message: "SIM plan change completed successfully",
...result,
};
@ -184,7 +183,7 @@ export class SimController {
@Body() body: SimCancelRequestDto
): Promise<SimActionResponse> {
await this.simManagementService.cancelSim(req.user.id, params.id, body);
return { success: true, message: "SIM cancellation completed successfully" };
return { message: "SIM cancellation completed successfully" };
}
@Post(":id/sim/reissue-esim")
@ -196,7 +195,7 @@ export class SimController {
): Promise<SimActionResponse> {
const parsedBody = simReissueEsimRequestSchema.parse(body as unknown);
await this.simManagementService.reissueEsimProfile(req.user.id, params.id, parsedBody.newEid);
return { success: true, message: "eSIM profile reissue completed successfully" };
return { message: "eSIM profile reissue completed successfully" };
}
@Post(":id/sim/features")
@ -207,7 +206,7 @@ export class SimController {
@Body() body: SimFeaturesRequestDto
): Promise<SimActionResponse> {
await this.simManagementService.updateSimFeatures(req.user.id, params.id, body);
return { success: true, message: "SIM features updated successfully" };
return { message: "SIM features updated successfully" };
}
// ==================== Enhanced SIM Management Endpoints ====================
@ -232,7 +231,6 @@ export class SimController {
): Promise<SimPlanChangeResult> {
const result = await this.simPlanService.changeSimPlanFull(req.user.id, params.id, body);
return {
success: true,
message: `SIM plan change scheduled for ${result.scheduledAt}`,
...result,
};
@ -264,7 +262,6 @@ export class SimController {
): Promise<SimActionResponse> {
await this.simCancellationService.cancelSimFull(req.user.id, params.id, body);
return {
success: true,
message: `SIM cancellation scheduled for end of ${body.cancellationMonth}`,
};
}
@ -279,10 +276,9 @@ export class SimController {
await this.esimManagementService.reissueSim(req.user.id, params.id, body);
if (body.simType === "esim") {
return { success: true, message: "eSIM profile reissue request submitted" };
return { message: "eSIM profile reissue request submitted" };
} else {
return {
success: true,
message: "Physical SIM reissue request submitted. You will be contacted shortly.",
};
}

View File

@ -27,8 +27,8 @@ import {
} from "@customer-portal/domain/support";
import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
import { hashEmailForLogs } from "./support.logging.js";
import type { ApiSuccessMessageResponse } from "@customer-portal/domain/common";
import { apiSuccessMessageResponseSchema } from "@customer-portal/domain/common";
import type { ActionMessageResponse } from "@customer-portal/domain/common";
import { actionMessageResponseSchema } from "@customer-portal/domain/common";
class SupportCaseFilterDto extends createZodDto(supportCaseFilterSchema) {}
class CreateCaseRequestDto extends createZodDto(createCaseRequestSchema) {}
@ -36,7 +36,7 @@ class PublicContactRequestDto extends createZodDto(publicContactRequestSchema) {
class SupportCaseListDto extends createZodDto(supportCaseListSchema) {}
class SupportCaseDto extends createZodDto(supportCaseSchema) {}
class CreateCaseResponseDto extends createZodDto(createCaseResponseSchema) {}
class ApiSuccessMessageResponseDto extends createZodDto(apiSuccessMessageResponseSchema) {}
class ActionMessageResponseDto extends createZodDto(actionMessageResponseSchema) {}
@Controller("support")
export class SupportController {
@ -84,16 +84,15 @@ export class SupportController {
@RateLimit({ limit: 5, ttl: 300 }) // 5 requests per 5 minutes
@ZodResponse({
description: "Public contact form submission",
type: ApiSuccessMessageResponseDto,
type: ActionMessageResponseDto,
})
async publicContact(@Body() body: PublicContactRequestDto): Promise<ApiSuccessMessageResponse> {
async publicContact(@Body() body: PublicContactRequestDto): Promise<ActionMessageResponse> {
this.logger.log("Public contact form submission", { emailHash: hashEmailForLogs(body.email) });
try {
await this.supportService.createPublicContactRequest(body);
return {
success: true as const,
message: "Your message has been received. We will get back to you within 24 hours.",
};
} catch (error) {

View File

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -20,26 +20,26 @@
"dependencies": {
"@customer-portal/domain": "workspace:*",
"@heroicons/react": "^2.2.0",
"@tanstack/react-query": "^5.90.12",
"@tanstack/react-query": "^5.90.14",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.562.0",
"next": "16.0.10",
"react": "19.2.1",
"react-dom": "19.2.1",
"next": "16.1.1",
"react": "19.2.3",
"react-dom": "19.2.3",
"tailwind-merge": "^3.4.0",
"world-countries": "^5.1.0",
"zod": "catalog:",
"zod": "^4.2.1",
"zustand": "^5.0.9"
},
"devDependencies": {
"@next/bundle-analyzer": "^16.0.9",
"@tailwindcss/postcss": "^4.1.17",
"@tanstack/react-query-devtools": "^5.91.1",
"@next/bundle-analyzer": "^16.1.1",
"@tailwindcss/postcss": "^4.1.18",
"@tanstack/react-query-devtools": "^5.91.2",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.1.17",
"tailwindcss": "^4.1.18",
"tailwindcss-animate": "^1.0.7",
"typescript": "catalog:"
}

View File

@ -5,7 +5,6 @@ import type {
OrderSelections,
OrderTypeValue,
} from "@customer-portal/domain/orders";
import type { ApiSuccessResponse } from "@customer-portal/domain/common";
type CheckoutCartSummary = { items: CheckoutCart["items"]; totals: CheckoutCart["totals"] };
@ -25,7 +24,7 @@ export const checkoutService = {
selections: OrderSelections,
configuration?: OrderConfigurations
): Promise<CheckoutCart> {
const response = await apiClient.POST<ApiSuccessResponse<CheckoutCart>>("/api/checkout/cart", {
const response = await apiClient.POST<CheckoutCart>("/api/checkout/cart", {
body: {
orderType,
selections,
@ -33,11 +32,7 @@ export const checkoutService = {
},
});
const wrappedResponse = getDataOrThrow(response, "Failed to build checkout cart");
if (!wrappedResponse.success) {
throw new Error("Failed to build checkout cart");
}
return wrappedResponse.data;
return getDataOrThrow(response, "Failed to build checkout cart");
},
async createSession(
@ -45,33 +40,20 @@ export const checkoutService = {
selections: OrderSelections,
configuration?: OrderConfigurations
): Promise<CheckoutSessionResponse> {
const response = await apiClient.POST<ApiSuccessResponse<CheckoutSessionResponse>>(
"/api/checkout/session",
{
body: { orderType, selections, configuration },
}
);
const wrappedResponse = getDataOrThrow(response, "Failed to create checkout session");
if (!wrappedResponse.success) {
throw new Error("Failed to create checkout session");
}
return wrappedResponse.data;
const response = await apiClient.POST<CheckoutSessionResponse>("/api/checkout/session", {
body: { orderType, selections, configuration },
});
return getDataOrThrow(response, "Failed to create checkout session");
},
async getSession(sessionId: string): Promise<CheckoutSessionResponse> {
const response = await apiClient.GET<ApiSuccessResponse<CheckoutSessionResponse>>(
const response = await apiClient.GET<CheckoutSessionResponse>(
"/api/checkout/session/{sessionId}",
{
params: { path: { sessionId } },
}
);
const wrappedResponse = getDataOrThrow(response, "Failed to load checkout session");
if (!wrappedResponse.success) {
throw new Error("Failed to load checkout session");
}
return wrappedResponse.data;
return getDataOrThrow(response, "Failed to load checkout session");
},
/**

View File

@ -1,4 +1,4 @@
import { apiClient } from "@/lib/api";
import { apiClient, getDataOrThrow } from "@/lib/api";
import { log } from "@/lib/logger";
import {
orderDetailsSchema,
@ -7,7 +7,12 @@ import {
type OrderDetails,
type OrderSummary,
} from "@customer-portal/domain/orders";
import { assertSuccess, type DomainApiResponse } from "@/lib/api/response-helpers";
interface CreateOrderResponse {
sfOrderId: string;
status: string;
message: string;
}
async function createOrder(payload: CreateOrderRequest): Promise<{ sfOrderId: string }> {
const body: CreateOrderRequest = {
@ -23,12 +28,9 @@ async function createOrder(payload: CreateOrderRequest): Promise<{ sfOrderId: st
});
try {
const response = await apiClient.POST("/api/orders", { body });
const parsed = assertSuccess<{ sfOrderId: string; status: string; message: string }>(
response.data as DomainApiResponse<{ sfOrderId: string; status: string; message: string }>
);
return { sfOrderId: parsed.data.sfOrderId };
const response = await apiClient.POST<CreateOrderResponse>("/api/orders", { body });
const data = getDataOrThrow(response, "Failed to create order");
return { sfOrderId: data.sfOrderId };
} catch (error) {
log.error("Order creation failed", error instanceof Error ? error : undefined, {
orderType: body.orderType,
@ -41,14 +43,11 @@ async function createOrder(payload: CreateOrderRequest): Promise<{ sfOrderId: st
async function createOrderFromCheckoutSession(
checkoutSessionId: string
): Promise<{ sfOrderId: string }> {
const response = await apiClient.POST("/api/orders/from-checkout-session", {
const response = await apiClient.POST<CreateOrderResponse>("/api/orders/from-checkout-session", {
body: { checkoutSessionId },
});
const parsed = assertSuccess<{ sfOrderId: string; status: string; message: string }>(
response.data as DomainApiResponse<{ sfOrderId: string; status: string; message: string }>
);
return { sfOrderId: parsed.data.sfOrderId };
const data = getDataOrThrow(response, "Failed to create order");
return { sfOrderId: data.sfOrderId };
}
async function getMyOrders(): Promise<OrderSummary[]> {

View File

@ -1,4 +1,4 @@
import { apiClient, getDataOrDefault, getDataOrThrow } from "@/lib/api";
import { apiClient, getDataOrThrow, getDataOrDefault } from "@/lib/api";
import {
EMPTY_SIM_CATALOG,
EMPTY_VPN_CATALOG,
@ -29,11 +29,7 @@ export const servicesService = {
const response = await apiClient.GET<InternetCatalogCollection>(
"/api/public/services/internet/plans"
);
const data = getDataOrThrow<InternetCatalogCollection>(
response,
"Failed to load internet services"
);
return data; // BFF already validated
return getDataOrThrow(response, "Failed to load internet services");
},
/**
@ -45,14 +41,12 @@ export const servicesService = {
async getPublicSimCatalog(): Promise<SimCatalogCollection> {
const response = await apiClient.GET<SimCatalogCollection>("/api/public/services/sim/plans");
const data = getDataOrDefault<SimCatalogCollection>(response, EMPTY_SIM_CATALOG);
return data; // BFF already validated
return getDataOrDefault(response, EMPTY_SIM_CATALOG);
},
async getPublicVpnCatalog(): Promise<VpnCatalogCollection> {
const response = await apiClient.GET<VpnCatalogCollection>("/api/public/services/vpn/plans");
const data = getDataOrDefault<VpnCatalogCollection>(response, EMPTY_VPN_CATALOG);
return data; // BFF already validated
return getDataOrDefault(response, EMPTY_VPN_CATALOG);
},
// ============================================================================
@ -63,30 +57,24 @@ export const servicesService = {
const response = await apiClient.GET<InternetCatalogCollection>(
"/api/account/services/internet/plans"
);
const data = getDataOrThrow<InternetCatalogCollection>(
response,
"Failed to load internet services"
);
return data; // BFF already validated
return getDataOrThrow(response, "Failed to load internet services");
},
async getAccountSimCatalog(): Promise<SimCatalogCollection> {
const response = await apiClient.GET<SimCatalogCollection>("/api/account/services/sim/plans");
const data = getDataOrDefault<SimCatalogCollection>(response, EMPTY_SIM_CATALOG);
return data; // BFF already validated
return getDataOrDefault(response, EMPTY_SIM_CATALOG);
},
async getAccountVpnCatalog(): Promise<VpnCatalogCollection> {
const response = await apiClient.GET<VpnCatalogCollection>("/api/account/services/vpn/plans");
const data = getDataOrDefault<VpnCatalogCollection>(response, EMPTY_VPN_CATALOG);
return data; // BFF already validated
return getDataOrDefault(response, EMPTY_VPN_CATALOG);
},
async getInternetInstallations(): Promise<InternetInstallationCatalogItem[]> {
const response = await apiClient.GET<InternetInstallationCatalogItem[]>(
"/api/services/internet/installations"
);
const data = getDataOrDefault<InternetInstallationCatalogItem[]>(response, []);
const data = getDataOrDefault(response, []);
return internetInstallationCatalogItemSchema.array().parse(data);
},
@ -94,7 +82,7 @@ export const servicesService = {
const response = await apiClient.GET<InternetAddonCatalogItem[]>(
"/api/services/internet/addons"
);
const data = getDataOrDefault<InternetAddonCatalogItem[]>(response, []);
const data = getDataOrDefault(response, []);
return internetAddonCatalogItemSchema.array().parse(data);
},
@ -109,13 +97,13 @@ export const servicesService = {
const response = await apiClient.GET<SimActivationFeeCatalogItem[]>(
"/api/services/sim/activation-fees"
);
const data = getDataOrDefault<SimActivationFeeCatalogItem[]>(response, []);
const data = getDataOrDefault(response, []);
return simActivationFeeCatalogItemSchema.array().parse(data);
},
async getSimAddons(): Promise<SimCatalogProduct[]> {
const response = await apiClient.GET<SimCatalogProduct[]>("/api/services/sim/addons");
const data = getDataOrDefault<SimCatalogProduct[]>(response, []);
const data = getDataOrDefault(response, []);
return simCatalogProductSchema.array().parse(data);
},
@ -128,7 +116,7 @@ export const servicesService = {
async getVpnActivationFees(): Promise<VpnCatalogProduct[]> {
const response = await apiClient.GET<VpnCatalogProduct[]>("/api/services/vpn/activation-fees");
const data = getDataOrDefault<VpnCatalogProduct[]>(response, []);
const data = getDataOrDefault(response, []);
return vpnCatalogProductSchema.array().parse(data);
},

View File

@ -3,14 +3,14 @@ import type {
InternetCancelRequest,
InternetCancellationPreview,
} from "@customer-portal/domain/subscriptions";
import type { ApiSuccessResponse } from "@customer-portal/domain/common";
import { internetCancellationPreviewSchema } from "@customer-portal/domain/subscriptions";
export const internetActionsService = {
/**
* Get cancellation preview (available months, service details)
*/
async getCancellationPreview(subscriptionId: string): Promise<InternetCancellationPreview> {
const response = await apiClient.GET<ApiSuccessResponse<InternetCancellationPreview>>(
const response = await apiClient.GET<InternetCancellationPreview>(
"/api/subscriptions/{subscriptionId}/internet/cancellation-preview",
{
params: { path: { subscriptionId } },
@ -18,11 +18,7 @@ export const internetActionsService = {
);
const payload = getDataOrThrow(response, "Failed to load cancellation information");
if (!payload.data) {
throw new Error("Failed to load cancellation information");
}
return payload.data;
return internetCancellationPreviewSchema.parse(payload);
},
/**

View File

@ -1,7 +1,12 @@
import { apiClient } from "@/lib/api";
import type { ApiSuccessResponse } from "@customer-portal/domain/common";
import { apiClient, getDataOrDefault } from "@/lib/api";
import {
simInfoSchema,
simAvailablePlanArraySchema,
simCancellationPreviewSchema,
simDomesticCallHistoryResponseSchema,
simInternationalCallHistoryResponseSchema,
simSmsHistoryResponseSchema,
simHistoryAvailableMonthsSchema,
type SimAvailablePlan,
type SimInfo,
type SimCancelFullRequest,
@ -81,23 +86,23 @@ export const simActionsService = {
},
async getAvailablePlans(subscriptionId: string): Promise<SimAvailablePlan[]> {
const response = await apiClient.GET<ApiSuccessResponse<SimAvailablePlan[]>>(
const response = await apiClient.GET<SimAvailablePlan[]>(
"/api/subscriptions/{subscriptionId}/sim/available-plans",
{
params: { path: { subscriptionId } },
}
{ params: { path: { subscriptionId } } }
);
return response.data?.data ?? [];
const data = getDataOrDefault(response, []);
return simAvailablePlanArraySchema.parse(data);
},
async getCancellationPreview(subscriptionId: string): Promise<SimCancellationPreview | null> {
const response = await apiClient.GET<ApiSuccessResponse<SimCancellationPreview>>(
const response = await apiClient.GET<SimCancellationPreview>(
"/api/subscriptions/{subscriptionId}/sim/cancellation-preview",
{
params: { path: { subscriptionId } },
}
);
return response.data?.data ?? null;
const data = getDataOrDefault(response, null);
return data ? simCancellationPreviewSchema.parse(data) : null;
},
async reissueSim(subscriptionId: string, request: SimReissueFullRequest): Promise<void> {
@ -120,13 +125,14 @@ export const simActionsService = {
params.page = String(page);
params.limit = String(limit);
const response = await apiClient.GET<ApiSuccessResponse<SimDomesticCallHistoryResponse>>(
const response = await apiClient.GET<SimDomesticCallHistoryResponse>(
"/api/subscriptions/{subscriptionId}/sim/call-history/domestic",
{
params: { path: { subscriptionId }, query: params },
}
);
return response.data?.data ?? null;
const data = getDataOrDefault(response, null);
return data ? simDomesticCallHistoryResponseSchema.parse(data) : null;
},
async getInternationalCallHistory(
@ -140,13 +146,14 @@ export const simActionsService = {
params.page = String(page);
params.limit = String(limit);
const response = await apiClient.GET<ApiSuccessResponse<SimInternationalCallHistoryResponse>>(
const response = await apiClient.GET<SimInternationalCallHistoryResponse>(
"/api/subscriptions/{subscriptionId}/sim/call-history/international",
{
params: { path: { subscriptionId }, query: params },
}
);
return response.data?.data ?? null;
const data = getDataOrDefault(response, null);
return data ? simInternationalCallHistoryResponseSchema.parse(data) : null;
},
async getSmsHistory(
@ -160,20 +167,21 @@ export const simActionsService = {
params.page = String(page);
params.limit = String(limit);
const response = await apiClient.GET<ApiSuccessResponse<SimSmsHistoryResponse>>(
const response = await apiClient.GET<SimSmsHistoryResponse>(
"/api/subscriptions/{subscriptionId}/sim/sms-history",
{
params: { path: { subscriptionId }, query: params },
}
);
return response.data?.data ?? null;
const data = getDataOrDefault(response, null);
return data ? simSmsHistoryResponseSchema.parse(data) : null;
},
async getAvailableHistoryMonths(): Promise<string[]> {
const response = await apiClient.GET<ApiSuccessResponse<string[]>>(
"/api/subscriptions/sim/call-history/available-months",
{}
const response = await apiClient.GET<string[]>(
"/api/subscriptions/sim/call-history/available-months"
);
return response.data?.data ?? [];
const data = getDataOrDefault(response, []);
return simHistoryAvailableMonthsSchema.parse(data);
},
};

View File

@ -1,18 +1,18 @@
import {
apiErrorResponseSchema,
type ApiErrorResponse,
type ApiSuccessResponse,
} from "@customer-portal/domain/common";
import type { ZodTypeAny, infer as ZodInfer } from "zod";
import { apiErrorResponseSchema, type ApiErrorResponse } from "@customer-portal/domain/common";
/**
* API Response Helper Types and Functions
*
* Generic utilities for working with API responses
* Generic utilities for working with API responses.
*
* Note: Success responses from the BFF return data directly (no `{ success: true, data }` envelope).
* Error responses are returned as `{ success: false, error: { code, message, details? } }` and
* are surfaced via the API client error handler.
*/
/**
* Generic API response wrapper
* Generic API response wrapper from the client.
* After client unwrapping, `data` contains the actual payload (not the BFF envelope).
*/
export type ApiResponse<T> = {
data?: T;
@ -20,8 +20,8 @@ export type ApiResponse<T> = {
};
/**
* Extract data from API response or return null
* Useful for optional data handling
* Extract data from API response or return null.
* Useful for optional data handling.
*/
export function getNullableData<T>(response: ApiResponse<T>): T | null {
if (response.error || response.data === undefined) {
@ -31,7 +31,7 @@ export function getNullableData<T>(response: ApiResponse<T>): T | null {
}
/**
* Extract data from API response or throw error
* Extract data from API response or throw error.
*/
export function getDataOrThrow<T>(response: ApiResponse<T>, errorMessage?: string): T {
if (response.error || response.data === undefined) {
@ -41,54 +41,30 @@ export function getDataOrThrow<T>(response: ApiResponse<T>, errorMessage?: strin
}
/**
* Extract data from API response or return default value
* Extract data from API response or return default value.
*/
export function getDataOrDefault<T>(response: ApiResponse<T>, defaultValue: T): T {
return response.data ?? defaultValue;
}
/**
* Check if response has an error
* Check if response has an error.
*/
export function hasError<T>(response: ApiResponse<T>): boolean {
return !!response.error;
}
/**
* Check if response has data
* Check if response has data.
*/
export function hasData<T>(response: ApiResponse<T>): boolean {
return response.data !== undefined && !response.error;
}
export type DomainApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;
export function assertSuccess<T>(response: DomainApiResponse<T>): ApiSuccessResponse<T> {
if (response.success === true) {
return response;
}
throw new Error(response.error.message);
}
export function parseDomainResponse<T>(
response: DomainApiResponse<T>,
parser?: (payload: T) => T
): T {
const success = assertSuccess(response);
return parser ? parser(success.data) : success.data;
}
/**
* Parse an error payload into a structured API error response.
*/
export function parseDomainError(payload: unknown): ApiErrorResponse | null {
const parsed = apiErrorResponseSchema.safeParse(payload);
return parsed.success ? parsed.data : null;
}
export function buildSuccessResponse<T extends ZodTypeAny>(
data: ZodInfer<T>,
schema: T
): ApiSuccessResponse<ZodInfer<T>> {
return {
success: true,
data: schema.parse(data),
};
}

View File

@ -176,6 +176,12 @@ async function defaultHandleError(response: Response) {
throw new ApiError(message, response, body);
}
/**
* Parse response body from the BFF.
*
* The BFF returns data directly without any wrapper envelope.
* Errors are handled via HTTP status codes (4xx/5xx) and caught by `handleError`.
*/
const parseResponseBody = async (response: Response): Promise<unknown> => {
if (response.status === 204) {
return null;

View File

@ -9,7 +9,6 @@ import { NextRequest, NextResponse } from "next/server";
* @see https://nextjs.org/docs/app/guides/content-security-policy
* @see https://nextjs.org/docs/messages/middleware-to-proxy
*/
export function proxy(request: NextRequest) {
// Generate a random nonce for this request
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");

View File

@ -92,32 +92,30 @@ export const soqlFieldNameSchema = z
// ============================================================================
/**
* Schema for successful API responses
* Usage: apiSuccessResponseSchema(yourDataSchema)
* Schema for action acknowledgement responses (no payload).
* Used for endpoints that confirm an operation succeeded without returning data.
*
* @example
* // Controller returns:
* return {}; // Empty object as acknowledgement
*/
export const apiSuccessResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
z.object({
success: z.literal(true),
data: dataSchema,
});
export const actionAckResponseSchema = z.object({});
/**
* Schema for successful API acknowledgements (no payload)
* Schema for action responses with a human-readable message.
* Used for endpoints that return a confirmation message.
*
* @example
* // Controller returns:
* return { message: "SIM top-up completed successfully" };
*/
export const apiSuccessAckResponseSchema = z.object({
success: z.literal(true),
});
/**
* Schema for successful API responses with a human-readable message (no data payload)
*/
export const apiSuccessMessageResponseSchema = z.object({
success: z.literal(true),
export const actionMessageResponseSchema = z.object({
message: z.string(),
});
/**
* Schema for error API responses
* Schema for error API responses.
* Used by the exception filter to return structured errors.
*/
export const apiErrorResponseSchema = z.object({
success: z.literal(false),
@ -128,12 +126,25 @@ export const apiErrorResponseSchema = z.object({
}),
});
// ============================================================================
// Legacy API Response Schemas (Deprecated)
// ============================================================================
/**
* Discriminated union schema for API responses
* Usage: apiResponseSchema(yourDataSchema)
* @deprecated Use direct data schemas instead. The success envelope has been removed.
* Kept for backwards compatibility during migration.
*/
export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
z.discriminatedUnion("success", [apiSuccessResponseSchema(dataSchema), apiErrorResponseSchema]);
export const apiSuccessResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) => dataSchema;
/**
* @deprecated Use actionAckResponseSchema instead.
*/
export const apiSuccessAckResponseSchema = actionAckResponseSchema;
/**
* @deprecated Use actionMessageResponseSchema instead.
*/
export const apiSuccessMessageResponseSchema = actionMessageResponseSchema;
// ============================================================================
// Pagination Schemas

View File

@ -40,31 +40,27 @@ export type SalesforceCaseId = string & { readonly __brand: "SalesforceCaseId" }
// ============================================================================
// ============================================================================
// API Response Wrappers
// API Response Types
// ============================================================================
export interface ApiSuccessResponse<T> {
success: true;
data: T;
}
/**
* Action acknowledgement response (empty object).
* Used for endpoints that confirm an operation succeeded without returning data.
*/
export type ActionAckResponse = Record<string, never>;
/**
* Success acknowledgement response (no payload)
* Used for endpoints that simply confirm the operation succeeded.
* Action message response.
* Used for endpoints that return a confirmation message.
*/
export interface ApiSuccessAckResponse {
success: true;
}
/**
* Success message response (no data payload)
* Used for endpoints that return a human-readable message (e.g., actions).
*/
export interface ApiSuccessMessageResponse {
success: true;
export interface ActionMessageResponse {
message: string;
}
/**
* Error response from the BFF.
* All errors include a code, message, and optional details.
*/
export interface ApiErrorResponse {
success: false;
error: {
@ -74,6 +70,36 @@ export interface ApiErrorResponse {
};
}
// ============================================================================
// Legacy API Response Types (Deprecated)
// ============================================================================
/**
* @deprecated The success envelope has been removed. Use data types directly.
*/
export interface ApiSuccessResponse<T> {
success: true;
data: T;
}
/**
* @deprecated Use ActionAckResponse instead.
*/
export interface ApiSuccessAckResponse {
success: true;
}
/**
* @deprecated Use ActionMessageResponse instead.
*/
export interface ApiSuccessMessageResponse {
success: true;
message: string;
}
/**
* @deprecated Use data types directly for success, ApiErrorResponse for errors.
*/
export type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;
// ============================================================================

View File

@ -5,7 +5,6 @@
*/
import { z } from "zod";
import { apiSuccessResponseSchema } from "../common/index.js";
// ============================================================================
// Enum Value Arrays (for Zod schemas)
@ -316,7 +315,7 @@ export const checkoutBuildCartRequestSchema = z.object({
configuration: orderConfigurationsSchema.optional(),
});
export const checkoutBuildCartResponseSchema = apiSuccessResponseSchema(checkoutCartSchema);
export const checkoutBuildCartResponseSchema = checkoutCartSchema;
// ============================================================================
// BFF endpoint request/param schemas (DTO inputs)
@ -352,12 +351,10 @@ export const checkoutSessionDataSchema = z.object({
cart: checkoutCartSummarySchema,
});
export const checkoutSessionResponseSchema = apiSuccessResponseSchema(checkoutSessionDataSchema);
export const checkoutSessionResponseSchema = checkoutSessionDataSchema;
export const checkoutValidateCartDataSchema = z.object({ valid: z.boolean() });
export const checkoutValidateCartResponseSchema = apiSuccessResponseSchema(
checkoutValidateCartDataSchema
);
export const checkoutValidateCartResponseSchema = checkoutValidateCartDataSchema;
/**
* Schema for order creation response

View File

@ -124,10 +124,10 @@
"typecheck": "pnpm run type-check"
},
"peerDependencies": {
"zod": "4.1.13"
"zod": "^4.2.1"
},
"devDependencies": {
"typescript": "catalog:",
"zod": "catalog:"
"zod": "^4.2.1"
}
}

View File

@ -5,7 +5,6 @@
*/
import { z } from "zod";
import { apiSuccessMessageResponseSchema } from "../common/schema.js";
// Subscription Status Schema
export const subscriptionStatusSchema = z.enum([
@ -104,14 +103,16 @@ export const subscriptionStatsSchema = z.object({
/**
* Schema for SIM action responses (top-up, cancellation, feature updates)
*/
export const simActionResponseSchema = apiSuccessMessageResponseSchema.extend({
export const simActionResponseSchema = z.object({
message: z.string(),
data: z.unknown().optional(),
});
/**
* Schema for SIM plan change result with IP addresses
*/
export const simPlanChangeResultSchema = apiSuccessMessageResponseSchema.extend({
export const simPlanChangeResultSchema = z.object({
message: z.string(),
ipv4: z.string().optional(),
ipv6: z.string().optional(),
scheduledAt: z

431
pnpm-lock.yaml generated
View File

@ -64,7 +64,7 @@ importers:
dependencies:
"@customer-portal/domain":
specifier: workspace:*
version: link:../../packages/domain
version: file:packages/domain(zod@4.1.13)
"@nestjs/bullmq":
specifier: ^11.0.4
version: 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.65.1)
@ -91,7 +91,7 @@ importers:
version: 7.1.0
"@prisma/client":
specifier: ^7.1.0
version: 7.1.0(prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3))(typescript@5.9.3)
version: 7.1.0(prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3)
"@sendgrid/mail":
specifier: ^8.1.6
version: 8.1.6
@ -130,7 +130,7 @@ importers:
version: 8.16.3
prisma:
specifier: ^7.1.0
version: 7.1.0(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3)
version: 7.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
rate-limiter-flexible:
specifier: ^9.0.0
version: 9.0.0
@ -191,10 +191,10 @@ importers:
version: link:../../packages/domain
"@heroicons/react":
specifier: ^2.2.0
version: 2.2.0(react@19.2.1)
version: 2.2.0(react@19.2.3)
"@tanstack/react-query":
specifier: ^5.90.12
version: 5.90.12(react@19.2.1)
specifier: ^5.90.14
version: 5.90.14(react@19.2.3)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@ -206,16 +206,16 @@ importers:
version: 4.1.0
lucide-react:
specifier: ^0.562.0
version: 0.562.0(react@19.2.1)
version: 0.562.0(react@19.2.3)
next:
specifier: 16.0.10
version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
specifier: 16.1.1
version: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react:
specifier: 19.2.1
version: 19.2.1
specifier: 19.2.3
version: 19.2.3
react-dom:
specifier: 19.2.1
version: 19.2.1(react@19.2.1)
specifier: 19.2.3
version: 19.2.3(react@19.2.3)
tailwind-merge:
specifier: ^3.4.0
version: 3.4.0
@ -223,21 +223,21 @@ importers:
specifier: ^5.1.0
version: 5.1.0
zod:
specifier: "catalog:"
version: 4.1.13
specifier: ^4.2.1
version: 4.2.1
zustand:
specifier: ^5.0.9
version: 5.0.9(@types/react@19.2.7)(react@19.2.1)
version: 5.0.9(@types/react@19.2.7)(react@19.2.3)
devDependencies:
"@next/bundle-analyzer":
specifier: ^16.0.9
version: 16.0.10
specifier: ^16.1.1
version: 16.1.1
"@tailwindcss/postcss":
specifier: ^4.1.17
version: 4.1.17
specifier: ^4.1.18
version: 4.1.18
"@tanstack/react-query-devtools":
specifier: ^5.91.1
version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1)
specifier: ^5.91.2
version: 5.91.2(@tanstack/react-query@5.90.14(react@19.2.3))(react@19.2.3)
"@types/react":
specifier: ^19.2.7
version: 19.2.7
@ -245,11 +245,11 @@ importers:
specifier: ^19.2.3
version: 19.2.3(@types/react@19.2.7)
tailwindcss:
specifier: ^4.1.17
version: 4.1.17
specifier: ^4.1.18
version: 4.1.18
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@4.1.17)
version: 1.0.7(tailwindcss@4.1.18)
typescript:
specifier: "catalog:"
version: 5.9.3
@ -260,8 +260,8 @@ importers:
specifier: "catalog:"
version: 5.9.3
zod:
specifier: "catalog:"
version: 4.1.13
specifier: ^4.2.1
version: 4.2.1
packages:
"@alloc/quick-lru@5.2.0":
@ -508,6 +508,11 @@ packages:
}
engines: { node: ">=0.1.90" }
"@customer-portal/domain@file:packages/domain":
resolution: { directory: packages/domain, type: directory }
peerDependencies:
zod: 4.1.13
"@discoveryjs/json-ext@0.5.7":
resolution:
{
@ -1765,16 +1770,16 @@ packages:
"@nestjs/platform-express":
optional: true
"@next/bundle-analyzer@16.0.10":
"@next/bundle-analyzer@16.1.1":
resolution:
{
integrity: sha512-AHA6ZomhQuRsJtkoRvsq+hIuwA6F26mQzQT8ICcc2dL3BvHRcWOA+EiFr+BgWFY++EE957xVDqMIJjLApyxnwA==,
integrity: sha512-aNJy301GGH8k36rDgrYdnyYEdjRQg6csMi1njzqHo+3qyZvOOWMHSv+p7SztNTzP5RU2KRwX0pPwYBtDcE+vVA==,
}
"@next/env@16.0.10":
"@next/env@16.1.1":
resolution:
{
integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==,
integrity: sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==,
}
"@next/eslint-plugin-next@16.0.9":
@ -1783,73 +1788,73 @@ packages:
integrity: sha512-ea6F0Towc70S+5y0HfkmMeNvWXHH+5yQUhovmed5qHu9WxJRW0oE26+OU6z4u0hR5WHYec7KwwHZCyWlnwdpOg==,
}
"@next/swc-darwin-arm64@16.0.10":
"@next/swc-darwin-arm64@16.1.1":
resolution:
{
integrity: sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==,
integrity: sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==,
}
engines: { node: ">= 10" }
cpu: [arm64]
os: [darwin]
"@next/swc-darwin-x64@16.0.10":
"@next/swc-darwin-x64@16.1.1":
resolution:
{
integrity: sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==,
integrity: sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==,
}
engines: { node: ">= 10" }
cpu: [x64]
os: [darwin]
"@next/swc-linux-arm64-gnu@16.0.10":
"@next/swc-linux-arm64-gnu@16.1.1":
resolution:
{
integrity: sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==,
integrity: sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==,
}
engines: { node: ">= 10" }
cpu: [arm64]
os: [linux]
"@next/swc-linux-arm64-musl@16.0.10":
"@next/swc-linux-arm64-musl@16.1.1":
resolution:
{
integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==,
integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==,
}
engines: { node: ">= 10" }
cpu: [arm64]
os: [linux]
"@next/swc-linux-x64-gnu@16.0.10":
"@next/swc-linux-x64-gnu@16.1.1":
resolution:
{
integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==,
integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==,
}
engines: { node: ">= 10" }
cpu: [x64]
os: [linux]
"@next/swc-linux-x64-musl@16.0.10":
"@next/swc-linux-x64-musl@16.1.1":
resolution:
{
integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==,
integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==,
}
engines: { node: ">= 10" }
cpu: [x64]
os: [linux]
"@next/swc-win32-arm64-msvc@16.0.10":
"@next/swc-win32-arm64-msvc@16.1.1":
resolution:
{
integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==,
integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==,
}
engines: { node: ">= 10" }
cpu: [arm64]
os: [win32]
"@next/swc-win32-x64-msvc@16.0.10":
"@next/swc-win32-x64-msvc@16.1.1":
resolution:
{
integrity: sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==,
integrity: sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==,
}
engines: { node: ">= 10" }
cpu: [x64]
@ -2254,97 +2259,97 @@ packages:
}
engines: { node: ">=14.16" }
"@tailwindcss/node@4.1.17":
"@tailwindcss/node@4.1.18":
resolution:
{
integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==,
integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==,
}
"@tailwindcss/oxide-android-arm64@4.1.17":
"@tailwindcss/oxide-android-arm64@4.1.18":
resolution:
{
integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==,
integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==,
}
engines: { node: ">= 10" }
cpu: [arm64]
os: [android]
"@tailwindcss/oxide-darwin-arm64@4.1.17":
"@tailwindcss/oxide-darwin-arm64@4.1.18":
resolution:
{
integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==,
integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==,
}
engines: { node: ">= 10" }
cpu: [arm64]
os: [darwin]
"@tailwindcss/oxide-darwin-x64@4.1.17":
"@tailwindcss/oxide-darwin-x64@4.1.18":
resolution:
{
integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==,
integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==,
}
engines: { node: ">= 10" }
cpu: [x64]
os: [darwin]
"@tailwindcss/oxide-freebsd-x64@4.1.17":
"@tailwindcss/oxide-freebsd-x64@4.1.18":
resolution:
{
integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==,
integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==,
}
engines: { node: ">= 10" }
cpu: [x64]
os: [freebsd]
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17":
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18":
resolution:
{
integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==,
integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==,
}
engines: { node: ">= 10" }
cpu: [arm]
os: [linux]
"@tailwindcss/oxide-linux-arm64-gnu@4.1.17":
"@tailwindcss/oxide-linux-arm64-gnu@4.1.18":
resolution:
{
integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==,
integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==,
}
engines: { node: ">= 10" }
cpu: [arm64]
os: [linux]
"@tailwindcss/oxide-linux-arm64-musl@4.1.17":
"@tailwindcss/oxide-linux-arm64-musl@4.1.18":
resolution:
{
integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==,
integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==,
}
engines: { node: ">= 10" }
cpu: [arm64]
os: [linux]
"@tailwindcss/oxide-linux-x64-gnu@4.1.17":
"@tailwindcss/oxide-linux-x64-gnu@4.1.18":
resolution:
{
integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==,
integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==,
}
engines: { node: ">= 10" }
cpu: [x64]
os: [linux]
"@tailwindcss/oxide-linux-x64-musl@4.1.17":
"@tailwindcss/oxide-linux-x64-musl@4.1.18":
resolution:
{
integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==,
integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==,
}
engines: { node: ">= 10" }
cpu: [x64]
os: [linux]
"@tailwindcss/oxide-wasm32-wasi@4.1.17":
"@tailwindcss/oxide-wasm32-wasi@4.1.18":
resolution:
{
integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==,
integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==,
}
engines: { node: ">=14.0.0" }
cpu: [wasm32]
@ -2356,62 +2361,62 @@ packages:
- "@emnapi/wasi-threads"
- tslib
"@tailwindcss/oxide-win32-arm64-msvc@4.1.17":
"@tailwindcss/oxide-win32-arm64-msvc@4.1.18":
resolution:
{
integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==,
integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==,
}
engines: { node: ">= 10" }
cpu: [arm64]
os: [win32]
"@tailwindcss/oxide-win32-x64-msvc@4.1.17":
"@tailwindcss/oxide-win32-x64-msvc@4.1.18":
resolution:
{
integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==,
integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==,
}
engines: { node: ">= 10" }
cpu: [x64]
os: [win32]
"@tailwindcss/oxide@4.1.17":
"@tailwindcss/oxide@4.1.18":
resolution:
{
integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==,
integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==,
}
engines: { node: ">= 10" }
"@tailwindcss/postcss@4.1.17":
"@tailwindcss/postcss@4.1.18":
resolution:
{
integrity: sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==,
integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==,
}
"@tanstack/query-core@5.90.12":
"@tanstack/query-core@5.90.14":
resolution:
{
integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==,
integrity: sha512-/6di2yNI+YxpVrH9Ig74Q+puKnkCE+D0LGyagJEGndJHJc6ahkcc/UqirHKy8zCYE/N9KLggxcQvzYCsUBWgdw==,
}
"@tanstack/query-devtools@5.91.1":
"@tanstack/query-devtools@5.92.0":
resolution:
{
integrity: sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==,
integrity: sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==,
}
"@tanstack/react-query-devtools@5.91.1":
"@tanstack/react-query-devtools@5.91.2":
resolution:
{
integrity: sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==,
integrity: sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg==,
}
peerDependencies:
"@tanstack/react-query": ^5.90.10
"@tanstack/react-query": ^5.90.14
react: ^18 || ^19
"@tanstack/react-query@5.90.12":
"@tanstack/react-query@5.90.14":
resolution:
{
integrity: sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==,
integrity: sha512-JAMuULej09hrZ14W9+mxoRZ44rR2BuZfCd6oKTQVNfynQxCN3muH3jh3W46gqZNw5ZqY0ZVaS43Imb3dMr6tgw==,
}
peerDependencies:
react: ^18 || ^19
@ -3127,6 +3132,13 @@ packages:
}
engines: { node: ">=6.0.0" }
baseline-browser-mapping@2.9.11:
resolution:
{
integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==,
}
hasBin: true
baseline-browser-mapping@2.9.5:
resolution:
{
@ -3298,6 +3310,12 @@ packages:
integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==,
}
caniuse-lite@1.0.30001761:
resolution:
{
integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==,
}
chalk@4.1.2:
resolution:
{
@ -3919,6 +3937,13 @@ packages:
}
engines: { node: ">=10.13.0" }
enhanced-resolve@5.18.4:
resolution:
{
integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==,
}
engines: { node: ">=10.13.0" }
environment@1.1.0:
resolution:
{
@ -5013,10 +5038,10 @@ packages:
}
engines: { node: ">= 0.8.0" }
libphonenumber-js@1.12.31:
libphonenumber-js@1.12.33:
resolution:
{
integrity: sha512-Z3IhgVgrqO1S5xPYM3K5XwbkDasU67/Vys4heW+lfSBALcUZjeIIzI8zCLifY+OCzSq+fpDdywMDa7z+4srJPQ==,
integrity: sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw==,
}
lightningcss-android-arm64@1.30.2:
@ -5568,10 +5593,10 @@ packages:
"@nestjs/swagger":
optional: true
next@16.0.10:
next@16.1.1:
resolution:
{
integrity: sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==,
integrity: sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==,
}
engines: { node: ">=20.9.0" }
hasBin: true
@ -6228,18 +6253,18 @@ packages:
integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==,
}
react-dom@19.2.1:
react-dom@19.2.3:
resolution:
{
integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==,
integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==,
}
peerDependencies:
react: ^19.2.1
react: ^19.2.3
react@19.2.1:
react@19.2.3:
resolution:
{
integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==,
integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==,
}
engines: { node: ">=0.10.0" }
@ -6899,10 +6924,10 @@ packages:
peerDependencies:
tailwindcss: ">=3.0.0 || insiders"
tailwindcss@4.1.17:
tailwindcss@4.1.18:
resolution:
{
integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==,
integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==,
}
tapable@2.3.0:
@ -7245,10 +7270,10 @@ packages:
typescript:
optional: true
validator@13.15.23:
validator@13.15.26:
resolution:
{
integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==,
integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==,
}
engines: { node: ">= 0.10" }
@ -7494,10 +7519,10 @@ packages:
integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==,
}
zod@4.2.0:
zod@4.2.1:
resolution:
{
integrity: sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw==,
integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==,
}
zustand@5.0.9:
@ -7704,6 +7729,10 @@ snapshots:
"@colors/colors@1.5.0":
optional: true
"@customer-portal/domain@file:packages/domain(zod@4.1.13)":
dependencies:
zod: 4.1.13
"@discoveryjs/json-ext@0.5.7": {}
"@electric-sql/pglite-socket@0.0.6(@electric-sql/pglite@0.3.2)":
@ -7859,9 +7888,9 @@ snapshots:
protobufjs: 7.5.4
yargs: 17.7.2
"@heroicons/react@2.2.0(react@19.2.1)":
"@heroicons/react@2.2.0(react@19.2.3)":
dependencies:
react: 19.2.1
react: 19.2.3
"@hono/node-server@1.19.6(hono@4.10.6)":
dependencies:
@ -8388,41 +8417,41 @@ snapshots:
optionalDependencies:
"@nestjs/platform-express": 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)
"@next/bundle-analyzer@16.0.10":
"@next/bundle-analyzer@16.1.1":
dependencies:
webpack-bundle-analyzer: 4.10.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
"@next/env@16.0.10": {}
"@next/env@16.1.1": {}
"@next/eslint-plugin-next@16.0.9":
dependencies:
fast-glob: 3.3.1
"@next/swc-darwin-arm64@16.0.10":
"@next/swc-darwin-arm64@16.1.1":
optional: true
"@next/swc-darwin-x64@16.0.10":
"@next/swc-darwin-x64@16.1.1":
optional: true
"@next/swc-linux-arm64-gnu@16.0.10":
"@next/swc-linux-arm64-gnu@16.1.1":
optional: true
"@next/swc-linux-arm64-musl@16.0.10":
"@next/swc-linux-arm64-musl@16.1.1":
optional: true
"@next/swc-linux-x64-gnu@16.0.10":
"@next/swc-linux-x64-gnu@16.1.1":
optional: true
"@next/swc-linux-x64-musl@16.0.10":
"@next/swc-linux-x64-musl@16.1.1":
optional: true
"@next/swc-win32-arm64-msvc@16.0.10":
"@next/swc-win32-arm64-msvc@16.1.1":
optional: true
"@next/swc-win32-x64-msvc@16.0.10":
"@next/swc-win32-x64-msvc@16.1.1":
optional: true
"@nodelib/fs.scandir@2.1.5":
@ -8457,11 +8486,11 @@ snapshots:
"@prisma/client-runtime-utils@7.1.0": {}
"@prisma/client@7.1.0(prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3))(typescript@5.9.3)":
"@prisma/client@7.1.0(prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3)":
dependencies:
"@prisma/client-runtime-utils": 7.1.0
optionalDependencies:
prisma: 7.1.0(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3)
prisma: 7.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
typescript: 5.9.3
"@prisma/config@7.1.0":
@ -8528,11 +8557,11 @@ snapshots:
"@prisma/query-plan-executor@6.18.0": {}
"@prisma/studio-core@0.8.2(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)":
"@prisma/studio-core@0.8.2(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
dependencies:
"@types/react": 19.2.7
react: 19.2.1
react-dom: 19.2.1(react@19.2.1)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
"@protobufjs/aspromise@1.1.2": {}
@ -8668,89 +8697,89 @@ snapshots:
defer-to-connect: 2.0.1
optional: true
"@tailwindcss/node@4.1.17":
"@tailwindcss/node@4.1.18":
dependencies:
"@jridgewell/remapping": 2.3.5
enhanced-resolve: 5.18.3
enhanced-resolve: 5.18.4
jiti: 2.6.1
lightningcss: 1.30.2
magic-string: 0.30.21
source-map-js: 1.2.1
tailwindcss: 4.1.17
tailwindcss: 4.1.18
"@tailwindcss/oxide-android-arm64@4.1.17":
"@tailwindcss/oxide-android-arm64@4.1.18":
optional: true
"@tailwindcss/oxide-darwin-arm64@4.1.17":
"@tailwindcss/oxide-darwin-arm64@4.1.18":
optional: true
"@tailwindcss/oxide-darwin-x64@4.1.17":
"@tailwindcss/oxide-darwin-x64@4.1.18":
optional: true
"@tailwindcss/oxide-freebsd-x64@4.1.17":
"@tailwindcss/oxide-freebsd-x64@4.1.18":
optional: true
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17":
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18":
optional: true
"@tailwindcss/oxide-linux-arm64-gnu@4.1.17":
"@tailwindcss/oxide-linux-arm64-gnu@4.1.18":
optional: true
"@tailwindcss/oxide-linux-arm64-musl@4.1.17":
"@tailwindcss/oxide-linux-arm64-musl@4.1.18":
optional: true
"@tailwindcss/oxide-linux-x64-gnu@4.1.17":
"@tailwindcss/oxide-linux-x64-gnu@4.1.18":
optional: true
"@tailwindcss/oxide-linux-x64-musl@4.1.17":
"@tailwindcss/oxide-linux-x64-musl@4.1.18":
optional: true
"@tailwindcss/oxide-wasm32-wasi@4.1.17":
"@tailwindcss/oxide-wasm32-wasi@4.1.18":
optional: true
"@tailwindcss/oxide-win32-arm64-msvc@4.1.17":
"@tailwindcss/oxide-win32-arm64-msvc@4.1.18":
optional: true
"@tailwindcss/oxide-win32-x64-msvc@4.1.17":
"@tailwindcss/oxide-win32-x64-msvc@4.1.18":
optional: true
"@tailwindcss/oxide@4.1.17":
"@tailwindcss/oxide@4.1.18":
optionalDependencies:
"@tailwindcss/oxide-android-arm64": 4.1.17
"@tailwindcss/oxide-darwin-arm64": 4.1.17
"@tailwindcss/oxide-darwin-x64": 4.1.17
"@tailwindcss/oxide-freebsd-x64": 4.1.17
"@tailwindcss/oxide-linux-arm-gnueabihf": 4.1.17
"@tailwindcss/oxide-linux-arm64-gnu": 4.1.17
"@tailwindcss/oxide-linux-arm64-musl": 4.1.17
"@tailwindcss/oxide-linux-x64-gnu": 4.1.17
"@tailwindcss/oxide-linux-x64-musl": 4.1.17
"@tailwindcss/oxide-wasm32-wasi": 4.1.17
"@tailwindcss/oxide-win32-arm64-msvc": 4.1.17
"@tailwindcss/oxide-win32-x64-msvc": 4.1.17
"@tailwindcss/oxide-android-arm64": 4.1.18
"@tailwindcss/oxide-darwin-arm64": 4.1.18
"@tailwindcss/oxide-darwin-x64": 4.1.18
"@tailwindcss/oxide-freebsd-x64": 4.1.18
"@tailwindcss/oxide-linux-arm-gnueabihf": 4.1.18
"@tailwindcss/oxide-linux-arm64-gnu": 4.1.18
"@tailwindcss/oxide-linux-arm64-musl": 4.1.18
"@tailwindcss/oxide-linux-x64-gnu": 4.1.18
"@tailwindcss/oxide-linux-x64-musl": 4.1.18
"@tailwindcss/oxide-wasm32-wasi": 4.1.18
"@tailwindcss/oxide-win32-arm64-msvc": 4.1.18
"@tailwindcss/oxide-win32-x64-msvc": 4.1.18
"@tailwindcss/postcss@4.1.17":
"@tailwindcss/postcss@4.1.18":
dependencies:
"@alloc/quick-lru": 5.2.0
"@tailwindcss/node": 4.1.17
"@tailwindcss/oxide": 4.1.17
"@tailwindcss/node": 4.1.18
"@tailwindcss/oxide": 4.1.18
postcss: 8.5.6
tailwindcss: 4.1.17
tailwindcss: 4.1.18
"@tanstack/query-core@5.90.12": {}
"@tanstack/query-core@5.90.14": {}
"@tanstack/query-devtools@5.91.1": {}
"@tanstack/query-devtools@5.92.0": {}
"@tanstack/react-query-devtools@5.91.1(@tanstack/react-query@5.90.12(react@19.2.1))(react@19.2.1)":
"@tanstack/react-query-devtools@5.91.2(@tanstack/react-query@5.90.14(react@19.2.3))(react@19.2.3)":
dependencies:
"@tanstack/query-devtools": 5.91.1
"@tanstack/react-query": 5.90.12(react@19.2.1)
react: 19.2.1
"@tanstack/query-devtools": 5.92.0
"@tanstack/react-query": 5.90.14(react@19.2.3)
react: 19.2.3
"@tanstack/react-query@5.90.12(react@19.2.1)":
"@tanstack/react-query@5.90.14(react@19.2.3)":
dependencies:
"@tanstack/query-core": 5.90.12
react: 19.2.1
"@tanstack/query-core": 5.90.14
react: 19.2.3
"@tokenizer/inflate@0.2.7":
dependencies:
@ -9277,6 +9306,8 @@ snapshots:
base64url@3.0.1: {}
baseline-browser-mapping@2.9.11: {}
baseline-browser-mapping@2.9.5: {}
bcrypt-pbkdf@1.0.2:
@ -9413,6 +9444,8 @@ snapshots:
caniuse-lite@1.0.30001760: {}
caniuse-lite@1.0.30001761: {}
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
@ -9457,8 +9490,8 @@ snapshots:
class-validator@0.14.2:
dependencies:
"@types/validator": 13.15.10
libphonenumber-js: 1.12.31
validator: 13.15.23
libphonenumber-js: 1.12.33
validator: 13.15.26
optional: true
class-variance-authority@0.7.1:
@ -9718,6 +9751,11 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
enhanced-resolve@5.18.4:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.0
environment@1.1.0: {}
error-ex@1.3.4:
@ -9784,8 +9822,8 @@ snapshots:
"@babel/parser": 7.28.5
eslint: 9.39.2(jiti@2.6.1)
hermes-parser: 0.25.1
zod: 4.2.0
zod-validation-error: 4.0.2(zod@4.2.0)
zod: 4.2.1
zod-validation-error: 4.0.2(zod@4.2.1)
transitivePeerDependencies:
- supports-color
@ -10446,7 +10484,7 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
libphonenumber-js@1.12.31:
libphonenumber-js@1.12.33:
optional: true
lightningcss-android-arm64@1.30.2:
@ -10565,9 +10603,9 @@ snapshots:
lru.min@1.1.3: {}
lucide-react@0.562.0(react@19.2.1):
lucide-react@0.562.0(react@19.2.3):
dependencies:
react: 19.2.1
react: 19.2.3
luxon@3.7.2: {}
@ -10728,24 +10766,25 @@ snapshots:
optionalDependencies:
"@nestjs/swagger": 11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1):
next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
"@next/env": 16.0.10
"@next/env": 16.1.1
"@swc/helpers": 0.5.15
caniuse-lite: 1.0.30001760
baseline-browser-mapping: 2.9.11
caniuse-lite: 1.0.30001761
postcss: 8.4.31
react: 19.2.1
react-dom: 19.2.1(react@19.2.1)
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.1)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
optionalDependencies:
"@next/swc-darwin-arm64": 16.0.10
"@next/swc-darwin-x64": 16.0.10
"@next/swc-linux-arm64-gnu": 16.0.10
"@next/swc-linux-arm64-musl": 16.0.10
"@next/swc-linux-x64-gnu": 16.0.10
"@next/swc-linux-x64-musl": 16.0.10
"@next/swc-win32-arm64-msvc": 16.0.10
"@next/swc-win32-x64-msvc": 16.0.10
"@next/swc-darwin-arm64": 16.1.1
"@next/swc-darwin-x64": 16.1.1
"@next/swc-linux-arm64-gnu": 16.1.1
"@next/swc-linux-arm64-musl": 16.1.1
"@next/swc-linux-x64-gnu": 16.1.1
"@next/swc-linux-x64-musl": 16.1.1
"@next/swc-win32-arm64-msvc": 16.1.1
"@next/swc-win32-x64-msvc": 16.1.1
sharp: 0.34.5
transitivePeerDependencies:
- "@babel/core"
@ -11034,12 +11073,12 @@ snapshots:
prettier@3.7.4: {}
prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3):
prisma@7.1.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3):
dependencies:
"@prisma/config": 7.1.0
"@prisma/dev": 0.15.0(typescript@5.9.3)
"@prisma/engines": 7.1.0
"@prisma/studio-core": 0.8.2(@types/react@19.2.7)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
"@prisma/studio-core": 0.8.2(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
mysql2: 3.15.3
postgres: 3.4.7
optionalDependencies:
@ -11122,12 +11161,12 @@ snapshots:
defu: 6.1.4
destr: 2.0.5
react-dom@19.2.1(react@19.2.1):
react-dom@19.2.3(react@19.2.3):
dependencies:
react: 19.2.1
react: 19.2.3
scheduler: 0.27.0
react@19.2.1: {}
react@19.2.3: {}
readable-stream@3.6.2:
dependencies:
@ -11498,10 +11537,10 @@ snapshots:
dependencies:
"@tokenizer/token": 0.3.0
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.1):
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3):
dependencies:
client-only: 0.0.1
react: 19.2.1
react: 19.2.3
optionalDependencies:
"@babel/core": 7.28.5
@ -11530,11 +11569,11 @@ snapshots:
tailwind-merge@3.4.0: {}
tailwindcss-animate@1.0.7(tailwindcss@4.1.17):
tailwindcss-animate@1.0.7(tailwindcss@4.1.18):
dependencies:
tailwindcss: 4.1.17
tailwindcss: 4.1.18
tailwindcss@4.1.17: {}
tailwindcss@4.1.18: {}
tapable@2.3.0: {}
@ -11730,7 +11769,7 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
validator@13.15.23:
validator@13.15.26:
optional: true
vary@1.1.2: {}
@ -11885,15 +11924,15 @@ snapshots:
dependencies:
grammex: 3.1.12
zod-validation-error@4.0.2(zod@4.2.0):
zod-validation-error@4.0.2(zod@4.2.1):
dependencies:
zod: 4.2.0
zod: 4.2.1
zod@4.1.13: {}
zod@4.2.0: {}
zod@4.2.1: {}
zustand@5.0.9(@types/react@19.2.7)(react@19.2.1):
zustand@5.0.9(@types/react@19.2.7)(react@19.2.3):
optionalDependencies:
"@types/react": 19.2.7
react: 19.2.1
react: 19.2.3