From b1ff1e8fd373003968b8512c8f5ef38fb575feb2 Mon Sep 17 00:00:00 2001 From: barsa Date: Thu, 25 Dec 2025 19:01:00 +0900 Subject: [PATCH] Refactor GitHub Workflows to Consolidate Node and pnpm Setup - Unified Node.js and pnpm setup across deploy, pr-checks, and security workflows by introducing a custom action for streamlined configuration. - Removed redundant setup steps to enhance workflow clarity and maintainability. - Updated security workflow to include concurrency control for better job management. --- .github/actions/setup-node-pnpm/action.yml | 26 ++++++++ .github/workflows/deploy.yml | 10 +-- .github/workflows/pr-checks.yml | 12 +--- .github/workflows/security.yml | 63 +++++++------------ .../src/core/security/guards/admin.guard.ts | 18 ++++++ apps/bff/src/core/security/security.module.ts | 5 +- .../services/sim-call-history.service.ts | 9 ++- .../subscriptions/subscriptions.controller.ts | 12 +++- .../subscriptions/subscriptions.module.ts | 2 + .../src/modules/support/support.controller.ts | 23 ++----- .../src/modules/support/support.logging.ts | 10 +++ .../src/modules/support/support.service.ts | 17 +++-- .../services/services/services.store.ts | 8 ++- .../support/views/PublicContactView.tsx | 41 +++++------- .../domain/billing/providers/whmcs/mapper.ts | 1 - .../domain/customer/providers/whmcs/mapper.ts | 2 - packages/domain/customer/schema.ts | 2 - packages/domain/mappings/validation.ts | 6 -- packages/domain/opportunity/helpers.ts | 1 - packages/domain/orders/contract.ts | 2 - .../domain/payments/providers/whmcs/mapper.ts | 7 +-- .../services/providers/salesforce/mapper.ts | 7 --- .../domain/sim/providers/freebit/mapper.ts | 11 ---- packages/domain/support/schema.ts | 12 ++++ 24 files changed, 147 insertions(+), 160 deletions(-) create mode 100644 .github/actions/setup-node-pnpm/action.yml create mode 100644 apps/bff/src/core/security/guards/admin.guard.ts create mode 100644 apps/bff/src/modules/support/support.logging.ts diff --git a/.github/actions/setup-node-pnpm/action.yml b/.github/actions/setup-node-pnpm/action.yml new file mode 100644 index 00000000..317754ed --- /dev/null +++ b/.github/actions/setup-node-pnpm/action.yml @@ -0,0 +1,26 @@ +name: Setup Node & pnpm +description: Setup Node.js and pnpm with pnpm store caching + +inputs: + node-version: + description: Node.js version to use + required: false + default: "22" + pnpm-version: + description: pnpm version to use + required: false + default: "10.25.0" + +runs: + using: composite + steps: + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.pnpm-version }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: pnpm diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c3941a56..5757dc22 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,14 +21,8 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: "pnpm" + - name: Setup Node & pnpm + uses: ./.github/actions/setup-node-pnpm - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 7bf8a6d0..9e5de677 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -24,16 +24,8 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10.25.0 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "pnpm" + - name: Setup Node & pnpm + uses: ./.github/actions/setup-node-pnpm - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 8bf88f33..cdc0d4d9 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -14,6 +14,10 @@ on: # Allow manual trigger workflow_dispatch: +concurrency: + group: security-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: security-audit: name: Security Vulnerability Audit @@ -23,47 +27,18 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - name: Setup pnpm cache - uses: actions/cache@v4 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- + - name: Setup Node & pnpm + uses: ./.github/actions/setup-node-pnpm - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run security audit - id: audit - run: | - # Run audit and capture exit code - pnpm audit --audit-level=high || echo "AUDIT_FAILED=true" >> $GITHUB_OUTPUT + run: pnpm security:check - # Generate detailed report - pnpm audit --json > audit-report.json || true - - - name: Parse audit results - if: steps.audit.outputs.AUDIT_FAILED == 'true' - run: | - echo "⚠️ Security vulnerabilities detected!" - echo "Please review the audit report and update vulnerable packages." - pnpm audit - exit 1 + - name: Generate audit report (JSON) + if: always() + run: pnpm audit --json > audit-report.json || true - name: Upload audit report if: always() @@ -107,6 +82,15 @@ jobs: languages: javascript-typescript queries: security-and-quality + - name: Setup Node & pnpm + uses: ./.github/actions/setup-node-pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build (for better CodeQL extraction) + run: pnpm build + - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: @@ -122,13 +106,8 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - - - name: Setup pnpm - uses: pnpm/action-setup@v4 + - name: Setup Node & pnpm + uses: ./.github/actions/setup-node-pnpm - name: Check for outdated dependencies run: | diff --git a/apps/bff/src/core/security/guards/admin.guard.ts b/apps/bff/src/core/security/guards/admin.guard.ts new file mode 100644 index 00000000..1dcfb2ae --- /dev/null +++ b/apps/bff/src/core/security/guards/admin.guard.ts @@ -0,0 +1,18 @@ +import { ForbiddenException, Injectable } from "@nestjs/common"; +import type { CanActivate, ExecutionContext } from "@nestjs/common"; + +type RequestWithUserRole = { + user?: { role?: string }; +}; + +@Injectable() +export class AdminGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const role = request?.user?.role; + if (role !== "ADMIN") { + throw new ForbiddenException("Admin access required"); + } + return true; + } +} diff --git a/apps/bff/src/core/security/security.module.ts b/apps/bff/src/core/security/security.module.ts index 88a3b5fa..06360bbe 100644 --- a/apps/bff/src/core/security/security.module.ts +++ b/apps/bff/src/core/security/security.module.ts @@ -5,12 +5,13 @@ import { SecureErrorMapperService } from "./services/secure-error-mapper.service import { CsrfService } from "./services/csrf.service.js"; import { CsrfMiddleware } from "./middleware/csrf.middleware.js"; import { CsrfController } from "./controllers/csrf.controller.js"; +import { AdminGuard } from "./guards/admin.guard.js"; @Module({ imports: [ConfigModule], controllers: [CsrfController], - providers: [SecureErrorMapperService, CsrfService, CsrfMiddleware], - exports: [SecureErrorMapperService, CsrfService], + providers: [SecureErrorMapperService, CsrfService, CsrfMiddleware, AdminGuard], + exports: [SecureErrorMapperService, CsrfService, AdminGuard], }) export class SecurityModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/apps/bff/src/modules/subscriptions/sim-management/services/sim-call-history.service.ts b/apps/bff/src/modules/subscriptions/sim-management/services/sim-call-history.service.ts index d5480de6..a4877ef0 100644 --- a/apps/bff/src/modules/subscriptions/sim-management/services/sim-call-history.service.ts +++ b/apps/bff/src/modules/subscriptions/sim-management/services/sim-call-history.service.ts @@ -413,7 +413,8 @@ export class SimCallHistoryService { ): Promise { // Validate subscription ownership await this.simValidation.validateSimSubscription(userId, subscriptionId); - // Use production phone number for call history (test number has no call data) + // Dev/testing mode: call history data is currently sourced from a fixed account. + // TODO: Replace with the validated subscription account once call history data is available per user. const account = "08077052946"; // Default to available month if not specified @@ -477,7 +478,8 @@ export class SimCallHistoryService { ): Promise { // Validate subscription ownership await this.simValidation.validateSimSubscription(userId, subscriptionId); - // Use production phone number for call history (test number has no call data) + // Dev/testing mode: call history data is currently sourced from a fixed account. + // TODO: Replace with the validated subscription account once call history data is available per user. const account = "08077052946"; // Default to available month if not specified @@ -543,7 +545,8 @@ export class SimCallHistoryService { ): Promise { // Validate subscription ownership await this.simValidation.validateSimSubscription(userId, subscriptionId); - // Use production phone number for SMS history (test number has no SMS data) + // Dev/testing mode: call history data is currently sourced from a fixed account. + // TODO: Replace with the validated subscription account once call history data is available per user. const account = "08077052946"; // Default to available month if not specified diff --git a/apps/bff/src/modules/subscriptions/subscriptions.controller.ts b/apps/bff/src/modules/subscriptions/subscriptions.controller.ts index 95558164..edff7d72 100644 --- a/apps/bff/src/modules/subscriptions/subscriptions.controller.ts +++ b/apps/bff/src/modules/subscriptions/subscriptions.controller.ts @@ -10,6 +10,7 @@ import { BadRequestException, UsePipes, Header, + UseGuards, } from "@nestjs/common"; import { Public } from "@bff/modules/auth/decorators/public.decorator.js"; import { SubscriptionsService } from "./subscriptions.service.js"; @@ -46,6 +47,7 @@ import { ZodValidationPipe } from "nestjs-zod"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import { SimPlanService } from "./sim-management/services/sim-plan.service.js"; import { SimCancellationService } from "./sim-management/services/sim-cancellation.service.js"; +import { AdminGuard } from "@bff/core/security/guards/admin.guard.js"; import { EsimManagementService, type ReissueSimRequest, @@ -117,18 +119,20 @@ export class SubscriptionsController { /** * List available files on SFTP for debugging */ - @Public() + @UseGuards(AdminGuard) @Get("sim/call-history/sftp-files") async listSftpFiles(@Query("path") path: string = "/home/PASI") { + if (!path.startsWith("/home/PASI")) { + throw new BadRequestException("Invalid path"); + } const files = await this.simCallHistoryService.listSftpFiles(path); return { success: true, data: files, path }; } /** * Trigger manual import of call history (admin only) - * TODO: Add proper admin authentication before production */ - @Public() + @UseGuards(AdminGuard) @Post("sim/call-history/import") async importCallHistory(@Query("month") yearMonth: string) { if (!yearMonth || !/^\d{6}$/.test(yearMonth)) { @@ -163,6 +167,7 @@ export class SubscriptionsController { } @Get("debug/sim-details/:account") + @UseGuards(AdminGuard) async debugSimDetails(@Param("account") account: string) { return await this.simManagementService.getSimDetailsDebug(account); } @@ -191,6 +196,7 @@ export class SubscriptionsController { // ==================== SIM Management Endpoints (subscription-specific) ==================== @Get(":id/sim/debug") + @UseGuards(AdminGuard) async debugSimSubscription( @Request() req: RequestWithUser, @Param("id", ParseIntPipe) subscriptionId: number diff --git a/apps/bff/src/modules/subscriptions/subscriptions.module.ts b/apps/bff/src/modules/subscriptions/subscriptions.module.ts index 0a0c68ca..f6cd8e55 100644 --- a/apps/bff/src/modules/subscriptions/subscriptions.module.ts +++ b/apps/bff/src/modules/subscriptions/subscriptions.module.ts @@ -5,6 +5,7 @@ import { SimManagementService } from "./sim-management.service.js"; import { SimUsageStoreService } from "./sim-usage-store.service.js"; import { SimOrdersController } from "./sim-orders.controller.js"; import { SimOrderActivationService } from "./sim-order-activation.service.js"; +import { SecurityModule } from "@bff/core/security/security.module.js"; import { WhmcsModule } from "@bff/integrations/whmcs/whmcs.module.js"; import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js"; import { FreebitModule } from "@bff/integrations/freebit/freebit.module.js"; @@ -14,6 +15,7 @@ import { InternetManagementModule } from "./internet-management/internet-managem @Module({ imports: [ + SecurityModule, WhmcsModule, MappingsModule, FreebitModule, diff --git a/apps/bff/src/modules/support/support.controller.ts b/apps/bff/src/modules/support/support.controller.ts index c3c62a37..ecbe8cf2 100644 --- a/apps/bff/src/modules/support/support.controller.ts +++ b/apps/bff/src/modules/support/support.controller.ts @@ -14,34 +14,19 @@ import { SupportService } from "./support.service.js"; import { ZodValidationPipe } from "nestjs-zod"; import { Public } from "@bff/modules/auth/decorators/public.decorator.js"; import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js"; -import { z } from "zod"; -import { createHash } from "node:crypto"; import { supportCaseFilterSchema, createCaseRequestSchema, + publicContactRequestSchema, type SupportCaseFilter, type SupportCaseList, type SupportCase, type CreateCaseRequest, type CreateCaseResponse, + type PublicContactRequest, } from "@customer-portal/domain/support"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; - -// Public contact form schema -const publicContactSchema = z.object({ - name: z.string().min(1, "Name is required"), - email: z.string().email("Valid email required"), - phone: z.string().optional(), - subject: z.string().min(1, "Subject is required"), - message: z.string().min(10, "Message must be at least 10 characters"), -}); - -type PublicContactRequest = z.infer; - -const hashEmailForLogs = (email: string): string => { - const normalized = email.trim().toLowerCase(); - return createHash("sha256").update(normalized).digest("hex").slice(0, 12); -}; +import { hashEmailForLogs } from "./support.logging.js"; @Controller("support") export class SupportController { @@ -87,7 +72,7 @@ export class SupportController { @UseGuards(RateLimitGuard) @RateLimit({ limit: 5, ttl: 300 }) // 5 requests per 5 minutes async publicContact( - @Body(new ZodValidationPipe(publicContactSchema)) + @Body(new ZodValidationPipe(publicContactRequestSchema)) body: PublicContactRequest ): Promise<{ success: boolean; message: string }> { this.logger.log("Public contact form submission", { emailHash: hashEmailForLogs(body.email) }); diff --git a/apps/bff/src/modules/support/support.logging.ts b/apps/bff/src/modules/support/support.logging.ts new file mode 100644 index 00000000..68da4fd3 --- /dev/null +++ b/apps/bff/src/modules/support/support.logging.ts @@ -0,0 +1,10 @@ +import { createHash } from "node:crypto"; + +/** + * Hash an email for logs (PII minimization). + * Use a short, stable prefix to correlate events without storing the email itself. + */ +export const hashEmailForLogs = (email: string): string => { + const normalized = email.trim().toLowerCase(); + return createHash("sha256").update(normalized).digest("hex").slice(0, 12); +}; diff --git a/apps/bff/src/modules/support/support.service.ts b/apps/bff/src/modules/support/support.service.ts index ad86a6bc..e6d3cb0d 100644 --- a/apps/bff/src/modules/support/support.service.ts +++ b/apps/bff/src/modules/support/support.service.ts @@ -8,10 +8,12 @@ import { type SupportCaseList, type CreateCaseRequest, type CreateCaseResponse, + type PublicContactRequest, } from "@customer-portal/domain/support"; import { SalesforceCaseService } from "@bff/integrations/salesforce/services/salesforce-case.service.js"; import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js"; +import { hashEmailForLogs } from "./support.logging.js"; /** * Status values that indicate an open/active case @@ -133,14 +135,9 @@ export class SupportService { * Create a contact request from public form (no authentication required) * Creates a Web-to-Case in Salesforce or sends an email notification */ - async createPublicContactRequest(request: { - name: string; - email: string; - phone?: string; - subject: string; - message: string; - }): Promise { - this.logger.log("Creating public contact request", { email: request.email }); + async createPublicContactRequest(request: PublicContactRequest): Promise { + const emailHash = hashEmailForLogs(request.email); + this.logger.log("Creating public contact request", { emailHash }); try { // Create a case without account association (Web-to-Case style) @@ -155,12 +152,12 @@ export class SupportService { }); this.logger.log("Public contact request created successfully", { - email: request.email, + emailHash, }); } catch (error) { this.logger.error("Failed to create public contact request", { error: getErrorMessage(error), - email: request.email, + emailHash, }); // Don't throw - we don't want to expose internal errors to public users // In production, this should send a fallback email notification diff --git a/apps/portal/src/features/services/services/services.store.ts b/apps/portal/src/features/services/services/services.store.ts index 1390b55c..b8fc55a5 100644 --- a/apps/portal/src/features/services/services/services.store.ts +++ b/apps/portal/src/features/services/services/services.store.ts @@ -337,7 +337,13 @@ export const useCatalogStore = create()( // Only persist configuration state, not transient UI state partialize: state => ({ internet: state.internet, - sim: state.sim, + sim: { + ...state.sim, + // Do NOT persist sensitive identifiers or MNP/identity fields in localStorage. + eid: "", + wantsMnp: false, + mnpData: { ...initialSimState.mnpData }, + }, }), } ) diff --git a/apps/portal/src/features/support/views/PublicContactView.tsx b/apps/portal/src/features/support/views/PublicContactView.tsx index ddd34e0b..af6007d3 100644 --- a/apps/portal/src/features/support/views/PublicContactView.tsx +++ b/apps/portal/src/features/support/views/PublicContactView.tsx @@ -1,23 +1,17 @@ "use client"; import { useState, useCallback } from "react"; -import { z } from "zod"; import Link from "next/link"; import { Button, Input } from "@/components/atoms"; import { FormField } from "@/components/molecules/FormField/FormField"; import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner"; import { useZodForm } from "@/hooks/useZodForm"; import { Mail, CheckCircle, MapPin } from "lucide-react"; - -const contactFormSchema = z.object({ - name: z.string().min(1, "Name is required"), - email: z.string().email("Please enter a valid email address"), - phone: z.string().optional(), - subject: z.string().min(1, "Subject is required"), - message: z.string().min(10, "Message must be at least 10 characters"), -}); - -type ContactFormData = z.infer; +import { + publicContactRequestSchema, + type PublicContactRequest, +} from "@customer-portal/domain/support"; +import { apiClient, ApiError, isApiError } from "@/lib/api"; /** * PublicContactView - Contact page with form, phone, chat, and location info @@ -26,29 +20,28 @@ export function PublicContactView() { const [isSubmitted, setIsSubmitted] = useState(false); const [submitError, setSubmitError] = useState(null); - const handleSubmit = useCallback(async (data: ContactFormData) => { + const handleSubmit = useCallback(async (data: PublicContactRequest) => { setSubmitError(null); try { - const response = await fetch("/api/support/contact", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error?.message || "Failed to send message"); - } + await apiClient.POST("/api/support/contact", { body: data }); setIsSubmitted(true); } catch (error) { + if (isApiError(error)) { + setSubmitError(error.message || "Failed to send message"); + return; + } + if (error instanceof ApiError) { + setSubmitError(error.message || "Failed to send message"); + return; + } setSubmitError(error instanceof Error ? error.message : "Failed to send message"); } }, []); - const form = useZodForm({ - schema: contactFormSchema, + const form = useZodForm({ + schema: publicContactRequestSchema, initialValues: { name: "", email: "", diff --git a/packages/domain/billing/providers/whmcs/mapper.ts b/packages/domain/billing/providers/whmcs/mapper.ts index fabb9fe0..f7f6c22e 100644 --- a/packages/domain/billing/providers/whmcs/mapper.ts +++ b/packages/domain/billing/providers/whmcs/mapper.ts @@ -11,7 +11,6 @@ import { whmcsInvoiceRawSchema, type WhmcsInvoiceListItem, whmcsInvoiceListItemSchema, - type WhmcsInvoiceItemsRaw, whmcsInvoiceItemsRawSchema, } from "./raw.types.js"; import { parseAmount, formatDate } from "../../../providers/whmcs/utils.js"; diff --git a/packages/domain/customer/providers/whmcs/mapper.ts b/packages/domain/customer/providers/whmcs/mapper.ts index bd9d73a7..4768bd46 100644 --- a/packages/domain/customer/providers/whmcs/mapper.ts +++ b/packages/domain/customer/providers/whmcs/mapper.ts @@ -5,8 +5,6 @@ * Minimal transformation - validates and normalizes only address structure. */ -import { z } from "zod"; - import type { WhmcsClient, Address } from "../../schema.js"; import { whmcsClientSchema, addressSchema } from "../../schema.js"; import { diff --git a/packages/domain/customer/schema.ts b/packages/domain/customer/schema.ts index 3eb4ed47..0a3bed99 100644 --- a/packages/domain/customer/schema.ts +++ b/packages/domain/customer/schema.ts @@ -235,8 +235,6 @@ const nullableProfileFields = [ "lastlogin", ] as const; -type NullableProfileKey = (typeof nullableProfileFields)[number]; - const nullableProfileOverrides = nullableProfileFields.reduce>( (acc, field) => { acc[field] = z.string().nullable().optional(); diff --git a/packages/domain/mappings/validation.ts b/packages/domain/mappings/validation.ts index bde7e192..13048337 100644 --- a/packages/domain/mappings/validation.ts +++ b/packages/domain/mappings/validation.ts @@ -5,12 +5,6 @@ * These functions contain no infrastructure dependencies (no DB, no HTTP, no logging). */ -import { z } from "zod"; -import { - createMappingRequestSchema, - updateMappingRequestSchema, - userIdMappingSchema, -} from "./schema.js"; import type { CreateMappingRequest, UpdateMappingRequest, diff --git a/packages/domain/opportunity/helpers.ts b/packages/domain/opportunity/helpers.ts index c0d6f547..70b845c2 100644 --- a/packages/domain/opportunity/helpers.ts +++ b/packages/domain/opportunity/helpers.ts @@ -220,7 +220,6 @@ export function generateCancellationMonthOptions( export function getCancellationEligibility(today: Date = new Date()): CancellationEligibility { const availableMonths = generateCancellationMonthOptions(today); const earliestMonth = getEarliestCancellationMonth(today); - const currentYearMonth = formatYearMonth(today); const day = today.getDate(); // Check if current month is still available diff --git a/packages/domain/orders/contract.ts b/packages/domain/orders/contract.ts index 57a8f54a..9eaf1329 100644 --- a/packages/domain/orders/contract.ts +++ b/packages/domain/orders/contract.ts @@ -5,8 +5,6 @@ * Validated types are derived from schemas (see schema.ts). */ -import type { SalesforceProductFieldMap } from "../services/contract.js"; -import type { SalesforceAccountFieldMap } from "../customer/index.js"; import type { UserIdMapping } from "../mappings/contract.js"; import type { SalesforceOrderRecord } from "./providers/salesforce/raw.types.js"; import type { OrderConfigurations } from "./schema.js"; diff --git a/packages/domain/payments/providers/whmcs/mapper.ts b/packages/domain/payments/providers/whmcs/mapper.ts index dc7ed13d..3dd11ced 100644 --- a/packages/domain/payments/providers/whmcs/mapper.ts +++ b/packages/domain/payments/providers/whmcs/mapper.ts @@ -4,12 +4,7 @@ import type { PaymentMethod, PaymentGateway } from "../../contract.js"; import { paymentMethodSchema, paymentGatewaySchema } from "../../schema.js"; -import { - type WhmcsPaymentMethodRaw, - type WhmcsPaymentGatewayRaw, - whmcsPaymentMethodRawSchema, - whmcsPaymentGatewayRawSchema, -} from "./raw.types.js"; +import { whmcsPaymentMethodRawSchema, whmcsPaymentGatewayRawSchema } from "./raw.types.js"; const PAYMENT_TYPE_MAP: Record = { creditcard: "CreditCard", diff --git a/packages/domain/services/providers/salesforce/mapper.ts b/packages/domain/services/providers/salesforce/mapper.ts index 09654b7b..78c20c57 100644 --- a/packages/domain/services/providers/salesforce/mapper.ts +++ b/packages/domain/services/providers/salesforce/mapper.ts @@ -40,13 +40,6 @@ function coerceNumber(value: unknown): number | undefined { return undefined; } -function inferInstallationTypeFromSku(sku: string): "One-time" | "12-Month" | "24-Month" { - const normalized = sku.toLowerCase(); - if (normalized.includes("24")) return "24-Month"; - if (normalized.includes("12")) return "12-Month"; - return "One-time"; -} - // ============================================================================ // Base Product Mapper // ============================================================================ diff --git a/packages/domain/sim/providers/freebit/mapper.ts b/packages/domain/sim/providers/freebit/mapper.ts index 8da233c3..54e50f0c 100644 --- a/packages/domain/sim/providers/freebit/mapper.ts +++ b/packages/domain/sim/providers/freebit/mapper.ts @@ -5,17 +5,7 @@ import type { SimDetails, SimUsage, SimTopUpHistory, SimType, SimStatus } from "../../contract.js"; import { simDetailsSchema, simUsageSchema, simTopUpHistorySchema } from "../../schema.js"; import { - type FreebitAccountDetailsRaw, - type FreebitTrafficInfoRaw, - type FreebitQuotaHistoryRaw, type FreebitAuthResponseRaw, - type FreebitTopUpRaw, - type FreebitAddSpecRaw, - type FreebitPlanChangeRaw, - type FreebitCancelPlanRaw, - type FreebitCancelAccountRaw, - type FreebitEsimReissueRaw, - type FreebitEsimAddAccountRaw, freebitAccountDetailsRawSchema, freebitTrafficInfoRawSchema, freebitQuotaHistoryRawSchema, @@ -28,7 +18,6 @@ import { freebitEsimReissueRawSchema, freebitEsimAddAccountRawSchema, } from "./raw.types.js"; -import { normalizeAccount } from "./utils.js"; function asString(value: unknown): string { if (typeof value === "string") return value; diff --git a/packages/domain/support/schema.ts b/packages/domain/support/schema.ts index 51d4f9a4..f608cec5 100644 --- a/packages/domain/support/schema.ts +++ b/packages/domain/support/schema.ts @@ -90,6 +90,17 @@ export const createCaseResponseSchema = z.object({ caseNumber: z.string(), }); +/** + * Public contact form schema (unauthenticated) + */ +export const publicContactRequestSchema = z.object({ + name: z.string().min(1, "Name is required"), + email: z.string().email("Valid email required"), + phone: z.string().optional(), + subject: z.string().min(1, "Subject is required"), + message: z.string().min(10, "Message must be at least 10 characters"), +}); + export type SupportCaseStatus = z.infer; export type SupportCasePriority = z.infer; export type SupportCaseCategory = z.infer; @@ -99,3 +110,4 @@ export type SupportCaseList = z.infer; export type SupportCaseFilter = z.infer; export type CreateCaseRequest = z.infer; export type CreateCaseResponse = z.infer; +export type PublicContactRequest = z.infer;