diff --git a/apps/bff/src/modules/subscriptions/sim-management.service.ts b/apps/bff/src/modules/subscriptions/sim-management.service.ts
index 716a3cba..cb019fc3 100644
--- a/apps/bff/src/modules/subscriptions/sim-management.service.ts
+++ b/apps/bff/src/modules/subscriptions/sim-management.service.ts
@@ -1,7 +1,7 @@
import { Injectable } from "@nestjs/common";
import { SimOrchestratorService } from "./sim-management/services/sim-orchestrator.service";
import { SimNotificationService } from "./sim-management/services/sim-notification.service";
-import type { SimDetails, SimTopUpHistory, SimUsage } from "@customer-portal/domain/sim";
+import type { SimDetails, SimInfo, SimTopUpHistory, SimUsage } from "@customer-portal/domain/sim";
import type {
SimTopUpRequest,
SimPlanChangeRequest,
@@ -123,10 +123,7 @@ export class SimManagementService {
async getSimInfo(
userId: string,
subscriptionId: number
- ): Promise<{
- details: SimDetails;
- usage: SimUsage;
- }> {
+ ): Promise {
return this.simOrchestrator.getSimInfo(userId, subscriptionId);
}
diff --git a/apps/bff/src/modules/subscriptions/sim-management/services/sim-orchestrator.service.ts b/apps/bff/src/modules/subscriptions/sim-management/services/sim-orchestrator.service.ts
index e3f6121a..0cbbf0c0 100644
--- a/apps/bff/src/modules/subscriptions/sim-management/services/sim-orchestrator.service.ts
+++ b/apps/bff/src/modules/subscriptions/sim-management/services/sim-orchestrator.service.ts
@@ -8,7 +8,8 @@ import { SimCancellationService } from "./sim-cancellation.service";
import { EsimManagementService } from "./esim-management.service";
import { SimValidationService } from "./sim-validation.service";
import { getErrorMessage } from "@bff/core/utils/error.util";
-import type { SimDetails, SimTopUpHistory, SimUsage } from "@customer-portal/domain/sim";
+import { simInfoSchema } from "@customer-portal/domain/sim";
+import type { SimInfo, SimDetails, SimTopUpHistory, SimUsage } from "@customer-portal/domain/sim";
import type {
SimTopUpRequest,
SimPlanChangeRequest,
@@ -113,10 +114,7 @@ export class SimOrchestratorService {
async getSimInfo(
userId: string,
subscriptionId: number
- ): Promise<{
- details: SimDetails;
- usage: SimUsage;
- }> {
+ ): Promise {
try {
const [details, usage] = await Promise.all([
this.getSimDetails(userId, subscriptionId),
@@ -141,7 +139,7 @@ export class SimOrchestratorService {
}
}
- return { details, usage };
+ return simInfoSchema.parse({ details, usage });
} catch (error) {
const sanitizedError = getErrorMessage(error);
this.logger.error(`Failed to get comprehensive SIM info for subscription ${subscriptionId}`, {
diff --git a/apps/bff/src/modules/subscriptions/subscriptions.controller.ts b/apps/bff/src/modules/subscriptions/subscriptions.controller.ts
index de427f27..a6fe8d4b 100644
--- a/apps/bff/src/modules/subscriptions/subscriptions.controller.ts
+++ b/apps/bff/src/modules/subscriptions/subscriptions.controller.ts
@@ -33,6 +33,7 @@ import {
simCancelRequestSchema,
simFeaturesRequestSchema,
simReissueRequestSchema,
+ type SimInfo,
type SimTopupRequest,
type SimChangePlanRequest,
type SimCancelRequest,
@@ -109,7 +110,7 @@ export class SubscriptionsController {
async getSimInfo(
@Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number
- ) {
+ ): Promise {
return this.simManagementService.getSimInfo(req.user.id, subscriptionId);
}
diff --git a/apps/bff/src/modules/subscriptions/subscriptions.service.ts b/apps/bff/src/modules/subscriptions/subscriptions.service.ts
index e9312a2f..77a42487 100644
--- a/apps/bff/src/modules/subscriptions/subscriptions.service.ts
+++ b/apps/bff/src/modules/subscriptions/subscriptions.service.ts
@@ -5,6 +5,7 @@ import {
SubscriptionList,
subscriptionListSchema,
subscriptionStatusSchema,
+ subscriptionStatsSchema,
type SubscriptionStatus,
} from "@customer-portal/domain/subscriptions";
import type { Invoice, InvoiceItem, InvoiceList } from "@customer-portal/domain/billing";
@@ -201,7 +202,7 @@ export class SubscriptionsService {
this.logger.log(`Generated subscription stats for user ${userId}`, stats);
- return stats;
+ return subscriptionStatsSchema.parse(stats);
} catch (error) {
this.logger.error(`Failed to generate subscription stats for user ${userId}`, {
error: getErrorMessage(error),
diff --git a/apps/bff/src/modules/users/users.service.ts b/apps/bff/src/modules/users/users.service.ts
index 83a2df27..213ca623 100644
--- a/apps/bff/src/modules/users/users.service.ts
+++ b/apps/bff/src/modules/users/users.service.ts
@@ -17,6 +17,7 @@ import {
import type { Subscription } from "@customer-portal/domain/subscriptions";
import type { Invoice } from "@customer-portal/domain/billing";
import type { Activity, DashboardSummary, NextInvoice } from "@customer-portal/domain/dashboard";
+import { dashboardSummarySchema } from "@customer-portal/domain/dashboard";
import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service";
import { SalesforceService } from "@bff/integrations/salesforce/salesforce.service";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service";
@@ -525,7 +526,7 @@ export class UsersService {
nextInvoice,
recentActivity,
};
- return summary;
+ return dashboardSummarySchema.parse(summary);
} catch (error) {
this.logger.error(`Failed to get user summary for ${userId}`, {
error: getErrorMessage(error),
diff --git a/apps/portal/src/features/account/components/AddressCard.tsx b/apps/portal/src/features/account/components/AddressCard.tsx
index 18331d52..215b1865 100644
--- a/apps/portal/src/features/account/components/AddressCard.tsx
+++ b/apps/portal/src/features/account/components/AddressCard.tsx
@@ -4,6 +4,7 @@ import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { MapPinIcon, PencilIcon, CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { AddressForm, type AddressFormProps } from "@/features/catalog/components";
import type { Address } from "@customer-portal/domain/customer";
+import { getCountryName } from "@/lib/constants/countries";
interface AddressCardProps {
address: Address;
@@ -26,6 +27,9 @@ export function AddressCard({
onSave,
onAddressChange,
}: AddressCardProps) {
+ const countryLabel =
+ address.country ? getCountryName(address.country) ?? address.country : null;
+
return (
@@ -88,7 +92,7 @@ export function AddressCard({
{[address.city, address.state, address.postcode].filter(Boolean).join(", ")}
)}
- {address.country &&
{address.country}
}
+ {countryLabel &&
{countryLabel}
}
)}
diff --git a/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx b/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx
index 1dade10c..ae1e459e 100644
--- a/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx
+++ b/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx
@@ -11,29 +11,7 @@ import { FormField } from "@/components/molecules/FormField/FormField";
import type { FormErrors, FormTouched, UseZodFormReturn } from "@customer-portal/validation";
import type { SignupFormValues } from "./SignupForm";
import type { Address } from "@customer-portal/domain/customer";
-
-const COUNTRIES = [
- { code: "US", name: "United States" },
- { code: "CA", name: "Canada" },
- { code: "GB", name: "United Kingdom" },
- { code: "AU", name: "Australia" },
- { code: "DE", name: "Germany" },
- { code: "FR", name: "France" },
- { code: "IT", name: "Italy" },
- { code: "ES", name: "Spain" },
- { code: "NL", name: "Netherlands" },
- { code: "SE", name: "Sweden" },
- { code: "NO", name: "Norway" },
- { code: "DK", name: "Denmark" },
- { code: "FI", name: "Finland" },
- { code: "CH", name: "Switzerland" },
- { code: "AT", name: "Austria" },
- { code: "BE", name: "Belgium" },
- { code: "IE", name: "Ireland" },
- { code: "PT", name: "Portugal" },
- { code: "GR", name: "Greece" },
- { code: "JP", name: "Japan" },
-];
+import { COUNTRY_OPTIONS } from "@/lib/constants/countries";
interface AddressStepProps {
address: SignupFormValues["address"];
@@ -53,7 +31,19 @@ export function AddressStep({
// Use domain Address type directly - no type helpers needed
const updateAddressField = useCallback(
(field: keyof Address, value: string) => {
- onAddressChange({ ...address, [field]: value });
+ onAddressChange({ ...(address ?? {}), [field]: value });
+ },
+ [address, onAddressChange]
+ );
+
+ const handleCountryChange = useCallback(
+ (code: string) => {
+ const normalized = code || "";
+ onAddressChange({
+ ...(address ?? {}),
+ country: normalized,
+ countryCode: normalized,
+ });
},
[address, onAddressChange]
);
@@ -139,13 +129,13 @@ export function AddressStep({
- {address.country}
+ {countryLabel && {countryLabel}
}
{/* Status message for Internet orders when pending */}
diff --git a/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts b/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts
index 70a58b7b..58b20d55 100644
--- a/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts
+++ b/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts
@@ -5,8 +5,12 @@
import { useQuery } from "@tanstack/react-query";
import { useAuthSession } from "@/features/auth/services/auth.store";
-import { apiClient, queryKeys, getDataOrThrow } from "@/lib/api";
-import type { DashboardSummary, DashboardError } from "@customer-portal/domain/dashboard";
+import { apiClient, queryKeys } from "@/lib/api";
+import {
+ dashboardSummarySchema,
+ type DashboardSummary,
+ type DashboardError,
+} from "@customer-portal/domain/dashboard";
class DashboardDataError extends Error {
constructor(
@@ -37,7 +41,21 @@ export function useDashboardSummary() {
try {
const response = await apiClient.GET("/api/me/summary");
- return getDataOrThrow(response, "Dashboard summary response was empty");
+ if (!response.data) {
+ throw new DashboardDataError(
+ "FETCH_ERROR",
+ "Dashboard summary response was empty"
+ );
+ }
+ const parsed = dashboardSummarySchema.safeParse(response.data);
+ if (!parsed.success) {
+ throw new DashboardDataError(
+ "FETCH_ERROR",
+ "Dashboard summary response failed validation",
+ { issues: parsed.error.issues }
+ );
+ }
+ return parsed.data;
} catch (error) {
// Transform API errors to DashboardError format
if (error instanceof Error) {
diff --git a/apps/portal/src/features/sim-management/components/DataUsageChart.tsx b/apps/portal/src/features/sim-management/components/DataUsageChart.tsx
index 4fe871cc..c0a8fa56 100644
--- a/apps/portal/src/features/sim-management/components/DataUsageChart.tsx
+++ b/apps/portal/src/features/sim-management/components/DataUsageChart.tsx
@@ -2,18 +2,7 @@
import React from "react";
import { ChartBarIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";
-
-export interface SimUsage {
- account: string;
- todayUsageKb: number;
- todayUsageMb: number;
- recentDaysUsage: Array<{
- date: string;
- usageKb: number;
- usageMb: number;
- }>;
- isBlacklisted: boolean;
-}
+import type { SimUsage } from "@customer-portal/domain/sim";
interface DataUsageChartProps {
usage: SimUsage;
diff --git a/apps/portal/src/features/sim-management/components/SimManagementSection.tsx b/apps/portal/src/features/sim-management/components/SimManagementSection.tsx
index ed6b18c3..79dae40c 100644
--- a/apps/portal/src/features/sim-management/components/SimManagementSection.tsx
+++ b/apps/portal/src/features/sim-management/components/SimManagementSection.tsx
@@ -7,21 +7,16 @@ import {
ArrowPathIcon,
} from "@heroicons/react/24/outline";
import { SimDetailsCard } from "./SimDetailsCard";
-import { DataUsageChart, type SimUsage } from "./DataUsageChart";
+import { DataUsageChart } from "./DataUsageChart";
import { SimActions } from "./SimActions";
import { apiClient } from "@/lib/api";
import { SimFeatureToggles } from "./SimFeatureToggles";
-import type { SimDetails } from "@customer-portal/domain/sim";
+import { simInfoSchema, type SimInfo } from "@customer-portal/domain/sim";
interface SimManagementSectionProps {
subscriptionId: number;
}
-interface SimInfo {
- details: SimDetails;
- usage: SimUsage;
-}
-
export function SimManagementSection({ subscriptionId }: SimManagementSectionProps) {
const [simInfo, setSimInfo] = useState(null);
const [loading, setLoading] = useState(true);
@@ -31,16 +26,16 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
try {
setError(null);
- const response = await apiClient.GET("/api/subscriptions/{id}/sim", {
+ const response = await apiClient.GET("/api/subscriptions/{id}/sim", {
params: { path: { id: subscriptionId } },
});
- const payload = response.data as SimInfo | undefined;
-
- if (!payload) {
+ if (!response.data) {
throw new Error("Failed to load SIM information");
}
+ const payload = simInfoSchema.parse(response.data);
+
setSimInfo(payload);
} catch (err: unknown) {
const hasStatus = (value: unknown): value is { status: number } =>
diff --git a/apps/portal/src/features/subscriptions/containers/SimCancel.tsx b/apps/portal/src/features/subscriptions/containers/SimCancel.tsx
index 65cc34cb..f96ba73f 100644
--- a/apps/portal/src/features/subscriptions/containers/SimCancel.tsx
+++ b/apps/portal/src/features/subscriptions/containers/SimCancel.tsx
@@ -48,7 +48,7 @@ export function SimCancelContainer() {
useEffect(() => {
const fetchDetails = async () => {
try {
- const info = await simActionsService.getSimInfo(subscriptionId);
+ const info = await simActionsService.getSimInfo(subscriptionId);
setDetails(info?.details || null);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to load SIM details");
diff --git a/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts b/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts
index bbef3cb6..9b5d1e97 100644
--- a/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts
+++ b/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts
@@ -8,19 +8,17 @@ import { apiClient, queryKeys, getDataOrDefault, getDataOrThrow } from "@/lib/ap
import { getNullableData } from "@/lib/api/response-helpers";
import { useAuthSession } from "@/features/auth/services";
import type { InvoiceList } from "@customer-portal/domain/billing";
-import type { Subscription, SubscriptionList } from "@customer-portal/domain/subscriptions";
+import {
+ subscriptionStatsSchema,
+ type Subscription,
+ type SubscriptionList,
+ type SubscriptionStats,
+} from "@customer-portal/domain/subscriptions";
interface UseSubscriptionsOptions {
status?: string;
}
-const emptyStats = {
- total: 0,
- active: 0,
- completed: 0,
- cancelled: 0,
-};
-
const emptyInvoiceList: InvoiceList = {
invoices: [],
pagination: {
@@ -76,11 +74,14 @@ export function useActiveSubscriptions() {
export function useSubscriptionStats() {
const { isAuthenticated } = useAuthSession();
- return useQuery({
+ return useQuery({
queryKey: queryKeys.subscriptions.stats(),
queryFn: async () => {
- const response = await apiClient.GET("/api/subscriptions/stats");
- return getDataOrThrow(response, "Failed to load subscription statistics");
+ const response = await apiClient.GET("/api/subscriptions/stats");
+ if (!response.data) {
+ throw new Error("Subscription statistics response was empty");
+ }
+ return subscriptionStatsSchema.parse(response.data);
},
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
diff --git a/apps/portal/src/features/subscriptions/services/sim-actions.service.ts b/apps/portal/src/features/subscriptions/services/sim-actions.service.ts
index c5f86564..43d7c0bf 100644
--- a/apps/portal/src/features/subscriptions/services/sim-actions.service.ts
+++ b/apps/portal/src/features/subscriptions/services/sim-actions.service.ts
@@ -1,4 +1,5 @@
-import { apiClient, getDataOrDefault } from "@/lib/api";
+import { apiClient } from "@/lib/api";
+import { simInfoSchema, type SimInfo } from "@customer-portal/domain/sim";
import type {
SimTopUpRequest,
SimPlanChangeRequest,
@@ -11,11 +12,6 @@ import type {
// - SimPlanChangeRequest: newPlanCode, assignGlobalIp, scheduledAt (YYYYMMDD format)
// - SimCancelRequest: scheduledAt (YYYYMMDD format)
-export interface SimInfo {
- details: T;
- error?: E;
-}
-
export const simActionsService = {
async topUp(subscriptionId: string, request: SimTopUpRequest): Promise {
await apiClient.POST("/api/subscriptions/{subscriptionId}/sim/top-up", {
@@ -38,13 +34,15 @@ export const simActionsService = {
});
},
- async getSimInfo(subscriptionId: string): Promise | null> {
- const response = await apiClient.GET | null>(
- "/api/subscriptions/{subscriptionId}/sim/info",
- {
- params: { path: { subscriptionId } },
- }
- );
- return getDataOrDefault | null>(response, null);
+ async getSimInfo(subscriptionId: string): Promise {
+ const response = await apiClient.GET("/api/subscriptions/{subscriptionId}/sim", {
+ params: { path: { subscriptionId } },
+ });
+
+ if (!response.data) {
+ return null;
+ }
+
+ return simInfoSchema.parse(response.data);
},
};
diff --git a/apps/portal/src/features/subscriptions/views/SimCancel.tsx b/apps/portal/src/features/subscriptions/views/SimCancel.tsx
index 70e0e140..dd38f430 100644
--- a/apps/portal/src/features/subscriptions/views/SimCancel.tsx
+++ b/apps/portal/src/features/subscriptions/views/SimCancel.tsx
@@ -48,7 +48,7 @@ export function SimCancelContainer() {
useEffect(() => {
const fetchDetails = async () => {
try {
- const info = await simActionsService.getSimInfo(subscriptionId);
+ const info = await simActionsService.getSimInfo(subscriptionId);
setDetails(info?.details || null);
} catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to load SIM details");
diff --git a/apps/portal/src/lib/constants/countries.ts b/apps/portal/src/lib/constants/countries.ts
new file mode 100644
index 00000000..af60b877
--- /dev/null
+++ b/apps/portal/src/lib/constants/countries.ts
@@ -0,0 +1,42 @@
+export interface CountryOption {
+ code: string;
+ name: string;
+}
+
+export const COUNTRY_OPTIONS: CountryOption[] = [
+ { code: "US", name: "United States" },
+ { code: "CA", name: "Canada" },
+ { code: "GB", name: "United Kingdom" },
+ { code: "AU", name: "Australia" },
+ { code: "DE", name: "Germany" },
+ { code: "FR", name: "France" },
+ { code: "IT", name: "Italy" },
+ { code: "ES", name: "Spain" },
+ { code: "NL", name: "Netherlands" },
+ { code: "SE", name: "Sweden" },
+ { code: "NO", name: "Norway" },
+ { code: "DK", name: "Denmark" },
+ { code: "FI", name: "Finland" },
+ { code: "CH", name: "Switzerland" },
+ { code: "AT", name: "Austria" },
+ { code: "BE", name: "Belgium" },
+ { code: "IE", name: "Ireland" },
+ { code: "PT", name: "Portugal" },
+ { code: "GR", name: "Greece" },
+ { code: "JP", name: "Japan" },
+];
+
+const COUNTRY_NAME_BY_CODE = new Map(COUNTRY_OPTIONS.map(option => [option.code, option.name]));
+const COUNTRY_CODE_BY_NAME = new Map(
+ COUNTRY_OPTIONS.map(option => [option.name.toLowerCase(), option.code])
+);
+
+export function getCountryName(code?: string | null): string | undefined {
+ if (!code) return undefined;
+ return COUNTRY_NAME_BY_CODE.get(code) ?? undefined;
+}
+
+export function getCountryCodeByName(name?: string | null): string | undefined {
+ if (!name) return undefined;
+ return COUNTRY_CODE_BY_NAME.get(name.toLowerCase()) ?? undefined;
+}
diff --git a/packages/domain/dashboard/contract.ts b/packages/domain/dashboard/contract.ts
index 0d4733c8..61816ae2 100644
--- a/packages/domain/dashboard/contract.ts
+++ b/packages/domain/dashboard/contract.ts
@@ -1,65 +1,22 @@
-/**
- * Dashboard Domain - Contract
- *
- * Shared types for dashboard summaries, activity feeds, and related data.
- */
+import { z } from "zod";
+import {
+ activityTypeSchema,
+ activitySchema,
+ dashboardStatsSchema,
+ nextInvoiceSchema,
+ dashboardSummarySchema,
+ dashboardErrorSchema,
+ activityFilterSchema,
+ activityFilterConfigSchema,
+ dashboardSummaryResponseSchema,
+} from "./schema";
-import type { IsoDateTimeString } from "../common/types";
-import type { Invoice } from "../billing/contract";
-
-export type ActivityType =
- | "invoice_created"
- | "invoice_paid"
- | "service_activated"
- | "case_created"
- | "case_closed";
-
-export interface Activity {
- id: string;
- type: ActivityType;
- title: string;
- description?: string;
- date: IsoDateTimeString;
- relatedId?: number;
- metadata?: Record;
-}
-
-export interface DashboardStats {
- activeSubscriptions: number;
- unpaidInvoices: number;
- openCases: number;
- recentOrders?: number;
- totalSpent?: number;
- currency: string;
-}
-
-export interface NextInvoice {
- id: number;
- dueDate: IsoDateTimeString;
- amount: number;
- currency: string;
-}
-
-export interface DashboardSummary {
- stats: DashboardStats;
- nextInvoice: NextInvoice | null;
- recentActivity: Activity[];
-}
-
-export interface DashboardError {
- code: string;
- message: string;
- details?: Record;
-}
-
-export type ActivityFilter = "all" | "billing" | "orders" | "support";
-
-export interface ActivityFilterConfig {
- key: ActivityFilter;
- label: string;
- types?: ActivityType[];
-}
-
-export interface DashboardSummaryResponse extends DashboardSummary {
- invoices?: Invoice[];
-}
+export type ActivityType = z.infer;
+export type Activity = z.infer;
+export type DashboardStats = z.infer;
+export type NextInvoice = z.infer;
+export type DashboardSummary = z.infer;
+export type DashboardError = z.infer;
+export type ActivityFilter = z.infer;
+export type ActivityFilterConfig = z.infer;
+export type DashboardSummaryResponse = z.infer;
diff --git a/packages/domain/dashboard/index.ts b/packages/domain/dashboard/index.ts
index 46ed6e9a..7dcfba10 100644
--- a/packages/domain/dashboard/index.ts
+++ b/packages/domain/dashboard/index.ts
@@ -3,3 +3,4 @@
*/
export * from "./contract";
+export * from "./schema";
diff --git a/packages/domain/dashboard/schema.ts b/packages/domain/dashboard/schema.ts
new file mode 100644
index 00000000..a8c94298
--- /dev/null
+++ b/packages/domain/dashboard/schema.ts
@@ -0,0 +1,60 @@
+import { z } from "zod";
+import { invoiceSchema } from "../billing/schema";
+
+export const activityTypeSchema = z.enum([
+ "invoice_created",
+ "invoice_paid",
+ "service_activated",
+ "case_created",
+ "case_closed",
+]);
+
+export const activitySchema = z.object({
+ id: z.string(),
+ type: activityTypeSchema,
+ title: z.string(),
+ description: z.string().optional(),
+ date: z.string(),
+ relatedId: z.number().optional(),
+ metadata: z.record(z.string(), z.unknown()).optional(),
+});
+
+export const dashboardStatsSchema = z.object({
+ activeSubscriptions: z.number().int().nonnegative(),
+ unpaidInvoices: z.number().int().nonnegative(),
+ openCases: z.number().int().nonnegative(),
+ recentOrders: z.number().int().nonnegative().optional(),
+ totalSpent: z.number().nonnegative().optional(),
+ currency: z.string(),
+});
+
+export const nextInvoiceSchema = z.object({
+ id: z.number().int().positive(),
+ dueDate: z.string(),
+ amount: z.number(),
+ currency: z.string(),
+});
+
+export const dashboardSummarySchema = z.object({
+ stats: dashboardStatsSchema,
+ nextInvoice: nextInvoiceSchema.nullable(),
+ recentActivity: z.array(activitySchema),
+});
+
+export const dashboardErrorSchema = z.object({
+ code: z.string(),
+ message: z.string(),
+ details: z.record(z.string(), z.unknown()).optional(),
+});
+
+export const activityFilterSchema = z.enum(["all", "billing", "orders", "support"]);
+
+export const activityFilterConfigSchema = z.object({
+ key: activityFilterSchema,
+ label: z.string(),
+ types: z.array(activityTypeSchema).optional(),
+});
+
+export const dashboardSummaryResponseSchema = dashboardSummarySchema.extend({
+ invoices: z.array(invoiceSchema).optional(),
+});
diff --git a/packages/domain/sim/index.ts b/packages/domain/sim/index.ts
index bf60a716..375bec19 100644
--- a/packages/domain/sim/index.ts
+++ b/packages/domain/sim/index.ts
@@ -24,6 +24,7 @@ export type {
SimUsage,
SimTopUpHistoryEntry,
SimTopUpHistory,
+ SimInfo,
// Request types
SimTopUpRequest,
SimPlanChangeRequest,
diff --git a/packages/domain/sim/schema.ts b/packages/domain/sim/schema.ts
index 9c36070c..5856ce34 100644
--- a/packages/domain/sim/schema.ts
+++ b/packages/domain/sim/schema.ts
@@ -60,6 +60,14 @@ export const simTopUpHistorySchema = z.object({
history: z.array(simTopUpHistoryEntrySchema),
});
+/**
+ * Combined SIM info payload (details + usage)
+ */
+export const simInfoSchema = z.object({
+ details: simDetailsSchema,
+ usage: simUsageSchema,
+});
+
// ============================================================================
// SIM Management Request Schemas
// ============================================================================
@@ -283,6 +291,7 @@ export type RecentDayUsage = z.infer;
export type SimUsage = z.infer;
export type SimTopUpHistoryEntry = z.infer;
export type SimTopUpHistory = z.infer;
+export type SimInfo = z.infer;
// Request types (derived from request schemas)
export type SimTopUpRequest = z.infer;