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.
This commit is contained in:
barsa 2025-12-25 19:01:00 +09:00
parent 84a11f7efc
commit b1ff1e8fd3
24 changed files with 147 additions and 160 deletions

View File

@ -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

View File

@ -21,14 +21,8 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup pnpm - name: Setup Node & pnpm
uses: pnpm/action-setup@v4 uses: ./.github/actions/setup-node-pnpm
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View File

@ -24,16 +24,8 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup pnpm - name: Setup Node & pnpm
uses: pnpm/action-setup@v4 uses: ./.github/actions/setup-node-pnpm
with:
version: 10.25.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "pnpm"
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

View File

@ -14,6 +14,10 @@ on:
# Allow manual trigger # Allow manual trigger
workflow_dispatch: workflow_dispatch:
concurrency:
group: security-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
security-audit: security-audit:
name: Security Vulnerability Audit name: Security Vulnerability Audit
@ -23,47 +27,18 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node & pnpm
uses: actions/setup-node@v4 uses: ./.github/actions/setup-node-pnpm
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: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Run security audit - name: Run security audit
id: audit run: pnpm security:check
run: |
# Run audit and capture exit code
pnpm audit --audit-level=high || echo "AUDIT_FAILED=true" >> $GITHUB_OUTPUT
# Generate detailed report - name: Generate audit report (JSON)
pnpm audit --json > audit-report.json || true if: always()
run: 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: Upload audit report - name: Upload audit report
if: always() if: always()
@ -107,6 +82,15 @@ jobs:
languages: javascript-typescript languages: javascript-typescript
queries: security-and-quality 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 - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v3
with: with:
@ -122,13 +106,8 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node & pnpm
uses: actions/setup-node@v4 uses: ./.github/actions/setup-node-pnpm
with:
node-version: "22"
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Check for outdated dependencies - name: Check for outdated dependencies
run: | run: |

View File

@ -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<RequestWithUserRole>();
const role = request?.user?.role;
if (role !== "ADMIN") {
throw new ForbiddenException("Admin access required");
}
return true;
}
}

View File

@ -5,12 +5,13 @@ import { SecureErrorMapperService } from "./services/secure-error-mapper.service
import { CsrfService } from "./services/csrf.service.js"; import { CsrfService } from "./services/csrf.service.js";
import { CsrfMiddleware } from "./middleware/csrf.middleware.js"; import { CsrfMiddleware } from "./middleware/csrf.middleware.js";
import { CsrfController } from "./controllers/csrf.controller.js"; import { CsrfController } from "./controllers/csrf.controller.js";
import { AdminGuard } from "./guards/admin.guard.js";
@Module({ @Module({
imports: [ConfigModule], imports: [ConfigModule],
controllers: [CsrfController], controllers: [CsrfController],
providers: [SecureErrorMapperService, CsrfService, CsrfMiddleware], providers: [SecureErrorMapperService, CsrfService, CsrfMiddleware, AdminGuard],
exports: [SecureErrorMapperService, CsrfService], exports: [SecureErrorMapperService, CsrfService, AdminGuard],
}) })
export class SecurityModule implements NestModule { export class SecurityModule implements NestModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {

View File

@ -413,7 +413,8 @@ export class SimCallHistoryService {
): Promise<DomesticCallHistoryResponse> { ): Promise<DomesticCallHistoryResponse> {
// Validate subscription ownership // Validate subscription ownership
await this.simValidation.validateSimSubscription(userId, subscriptionId); 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"; const account = "08077052946";
// Default to available month if not specified // Default to available month if not specified
@ -477,7 +478,8 @@ export class SimCallHistoryService {
): Promise<InternationalCallHistoryResponse> { ): Promise<InternationalCallHistoryResponse> {
// Validate subscription ownership // Validate subscription ownership
await this.simValidation.validateSimSubscription(userId, subscriptionId); 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"; const account = "08077052946";
// Default to available month if not specified // Default to available month if not specified
@ -543,7 +545,8 @@ export class SimCallHistoryService {
): Promise<SmsHistoryResponse> { ): Promise<SmsHistoryResponse> {
// Validate subscription ownership // Validate subscription ownership
await this.simValidation.validateSimSubscription(userId, subscriptionId); 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"; const account = "08077052946";
// Default to available month if not specified // Default to available month if not specified

View File

@ -10,6 +10,7 @@ import {
BadRequestException, BadRequestException,
UsePipes, UsePipes,
Header, Header,
UseGuards,
} from "@nestjs/common"; } from "@nestjs/common";
import { Public } from "@bff/modules/auth/decorators/public.decorator.js"; import { Public } from "@bff/modules/auth/decorators/public.decorator.js";
import { SubscriptionsService } from "./subscriptions.service.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 type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
import { SimPlanService } from "./sim-management/services/sim-plan.service.js"; import { SimPlanService } from "./sim-management/services/sim-plan.service.js";
import { SimCancellationService } from "./sim-management/services/sim-cancellation.service.js"; import { SimCancellationService } from "./sim-management/services/sim-cancellation.service.js";
import { AdminGuard } from "@bff/core/security/guards/admin.guard.js";
import { import {
EsimManagementService, EsimManagementService,
type ReissueSimRequest, type ReissueSimRequest,
@ -117,18 +119,20 @@ export class SubscriptionsController {
/** /**
* List available files on SFTP for debugging * List available files on SFTP for debugging
*/ */
@Public() @UseGuards(AdminGuard)
@Get("sim/call-history/sftp-files") @Get("sim/call-history/sftp-files")
async listSftpFiles(@Query("path") path: string = "/home/PASI") { 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); const files = await this.simCallHistoryService.listSftpFiles(path);
return { success: true, data: files, path }; return { success: true, data: files, path };
} }
/** /**
* Trigger manual import of call history (admin only) * Trigger manual import of call history (admin only)
* TODO: Add proper admin authentication before production
*/ */
@Public() @UseGuards(AdminGuard)
@Post("sim/call-history/import") @Post("sim/call-history/import")
async importCallHistory(@Query("month") yearMonth: string) { async importCallHistory(@Query("month") yearMonth: string) {
if (!yearMonth || !/^\d{6}$/.test(yearMonth)) { if (!yearMonth || !/^\d{6}$/.test(yearMonth)) {
@ -163,6 +167,7 @@ export class SubscriptionsController {
} }
@Get("debug/sim-details/:account") @Get("debug/sim-details/:account")
@UseGuards(AdminGuard)
async debugSimDetails(@Param("account") account: string) { async debugSimDetails(@Param("account") account: string) {
return await this.simManagementService.getSimDetailsDebug(account); return await this.simManagementService.getSimDetailsDebug(account);
} }
@ -191,6 +196,7 @@ export class SubscriptionsController {
// ==================== SIM Management Endpoints (subscription-specific) ==================== // ==================== SIM Management Endpoints (subscription-specific) ====================
@Get(":id/sim/debug") @Get(":id/sim/debug")
@UseGuards(AdminGuard)
async debugSimSubscription( async debugSimSubscription(
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Param("id", ParseIntPipe) subscriptionId: number @Param("id", ParseIntPipe) subscriptionId: number

View File

@ -5,6 +5,7 @@ import { SimManagementService } from "./sim-management.service.js";
import { SimUsageStoreService } from "./sim-usage-store.service.js"; import { SimUsageStoreService } from "./sim-usage-store.service.js";
import { SimOrdersController } from "./sim-orders.controller.js"; import { SimOrdersController } from "./sim-orders.controller.js";
import { SimOrderActivationService } from "./sim-order-activation.service.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 { WhmcsModule } from "@bff/integrations/whmcs/whmcs.module.js";
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js"; import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
import { FreebitModule } from "@bff/integrations/freebit/freebit.module.js"; import { FreebitModule } from "@bff/integrations/freebit/freebit.module.js";
@ -14,6 +15,7 @@ import { InternetManagementModule } from "./internet-management/internet-managem
@Module({ @Module({
imports: [ imports: [
SecurityModule,
WhmcsModule, WhmcsModule,
MappingsModule, MappingsModule,
FreebitModule, FreebitModule,

View File

@ -14,34 +14,19 @@ import { SupportService } from "./support.service.js";
import { ZodValidationPipe } from "nestjs-zod"; import { ZodValidationPipe } from "nestjs-zod";
import { Public } from "@bff/modules/auth/decorators/public.decorator.js"; import { Public } from "@bff/modules/auth/decorators/public.decorator.js";
import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js"; import { RateLimit, RateLimitGuard } from "@bff/core/rate-limiting/index.js";
import { z } from "zod";
import { createHash } from "node:crypto";
import { import {
supportCaseFilterSchema, supportCaseFilterSchema,
createCaseRequestSchema, createCaseRequestSchema,
publicContactRequestSchema,
type SupportCaseFilter, type SupportCaseFilter,
type SupportCaseList, type SupportCaseList,
type SupportCase, type SupportCase,
type CreateCaseRequest, type CreateCaseRequest,
type CreateCaseResponse, type CreateCaseResponse,
type PublicContactRequest,
} from "@customer-portal/domain/support"; } from "@customer-portal/domain/support";
import type { RequestWithUser } from "@bff/modules/auth/auth.types.js"; import type { RequestWithUser } from "@bff/modules/auth/auth.types.js";
import { hashEmailForLogs } from "./support.logging.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<typeof publicContactSchema>;
const hashEmailForLogs = (email: string): string => {
const normalized = email.trim().toLowerCase();
return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
};
@Controller("support") @Controller("support")
export class SupportController { export class SupportController {
@ -87,7 +72,7 @@ export class SupportController {
@UseGuards(RateLimitGuard) @UseGuards(RateLimitGuard)
@RateLimit({ limit: 5, ttl: 300 }) // 5 requests per 5 minutes @RateLimit({ limit: 5, ttl: 300 }) // 5 requests per 5 minutes
async publicContact( async publicContact(
@Body(new ZodValidationPipe(publicContactSchema)) @Body(new ZodValidationPipe(publicContactRequestSchema))
body: PublicContactRequest body: PublicContactRequest
): Promise<{ success: boolean; message: string }> { ): Promise<{ success: boolean; message: string }> {
this.logger.log("Public contact form submission", { emailHash: hashEmailForLogs(body.email) }); this.logger.log("Public contact form submission", { emailHash: hashEmailForLogs(body.email) });

View File

@ -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);
};

View File

@ -8,10 +8,12 @@ import {
type SupportCaseList, type SupportCaseList,
type CreateCaseRequest, type CreateCaseRequest,
type CreateCaseResponse, type CreateCaseResponse,
type PublicContactRequest,
} from "@customer-portal/domain/support"; } from "@customer-portal/domain/support";
import { SalesforceCaseService } from "@bff/integrations/salesforce/services/salesforce-case.service.js"; import { SalesforceCaseService } from "@bff/integrations/salesforce/services/salesforce-case.service.js";
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js"; import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
import { getErrorMessage } from "@bff/core/utils/error.util.js"; import { getErrorMessage } from "@bff/core/utils/error.util.js";
import { hashEmailForLogs } from "./support.logging.js";
/** /**
* Status values that indicate an open/active case * 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) * Create a contact request from public form (no authentication required)
* Creates a Web-to-Case in Salesforce or sends an email notification * Creates a Web-to-Case in Salesforce or sends an email notification
*/ */
async createPublicContactRequest(request: { async createPublicContactRequest(request: PublicContactRequest): Promise<void> {
name: string; const emailHash = hashEmailForLogs(request.email);
email: string; this.logger.log("Creating public contact request", { emailHash });
phone?: string;
subject: string;
message: string;
}): Promise<void> {
this.logger.log("Creating public contact request", { email: request.email });
try { try {
// Create a case without account association (Web-to-Case style) // 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", { this.logger.log("Public contact request created successfully", {
email: request.email, emailHash,
}); });
} catch (error) { } catch (error) {
this.logger.error("Failed to create public contact request", { this.logger.error("Failed to create public contact request", {
error: getErrorMessage(error), error: getErrorMessage(error),
email: request.email, emailHash,
}); });
// Don't throw - we don't want to expose internal errors to public users // Don't throw - we don't want to expose internal errors to public users
// In production, this should send a fallback email notification // In production, this should send a fallback email notification

View File

@ -337,7 +337,13 @@ export const useCatalogStore = create<CatalogStore>()(
// Only persist configuration state, not transient UI state // Only persist configuration state, not transient UI state
partialize: state => ({ partialize: state => ({
internet: state.internet, 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 },
},
}), }),
} }
) )

View File

@ -1,23 +1,17 @@
"use client"; "use client";
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
import { z } from "zod";
import Link from "next/link"; import Link from "next/link";
import { Button, Input } from "@/components/atoms"; import { Button, Input } from "@/components/atoms";
import { FormField } from "@/components/molecules/FormField/FormField"; import { FormField } from "@/components/molecules/FormField/FormField";
import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner"; import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { useZodForm } from "@/hooks/useZodForm"; import { useZodForm } from "@/hooks/useZodForm";
import { Mail, CheckCircle, MapPin } from "lucide-react"; import { Mail, CheckCircle, MapPin } from "lucide-react";
import {
const contactFormSchema = z.object({ publicContactRequestSchema,
name: z.string().min(1, "Name is required"), type PublicContactRequest,
email: z.string().email("Please enter a valid email address"), } from "@customer-portal/domain/support";
phone: z.string().optional(), import { apiClient, ApiError, isApiError } from "@/lib/api";
subject: z.string().min(1, "Subject is required"),
message: z.string().min(10, "Message must be at least 10 characters"),
});
type ContactFormData = z.infer<typeof contactFormSchema>;
/** /**
* PublicContactView - Contact page with form, phone, chat, and location info * PublicContactView - Contact page with form, phone, chat, and location info
@ -26,29 +20,28 @@ export function PublicContactView() {
const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null); const [submitError, setSubmitError] = useState<string | null>(null);
const handleSubmit = useCallback(async (data: ContactFormData) => { const handleSubmit = useCallback(async (data: PublicContactRequest) => {
setSubmitError(null); setSubmitError(null);
try { try {
const response = await fetch("/api/support/contact", { await apiClient.POST("/api/support/contact", { body: data });
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");
}
setIsSubmitted(true); setIsSubmitted(true);
} catch (error) { } 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"); setSubmitError(error instanceof Error ? error.message : "Failed to send message");
} }
}, []); }, []);
const form = useZodForm<ContactFormData>({ const form = useZodForm<PublicContactRequest>({
schema: contactFormSchema, schema: publicContactRequestSchema,
initialValues: { initialValues: {
name: "", name: "",
email: "", email: "",

View File

@ -11,7 +11,6 @@ import {
whmcsInvoiceRawSchema, whmcsInvoiceRawSchema,
type WhmcsInvoiceListItem, type WhmcsInvoiceListItem,
whmcsInvoiceListItemSchema, whmcsInvoiceListItemSchema,
type WhmcsInvoiceItemsRaw,
whmcsInvoiceItemsRawSchema, whmcsInvoiceItemsRawSchema,
} from "./raw.types.js"; } from "./raw.types.js";
import { parseAmount, formatDate } from "../../../providers/whmcs/utils.js"; import { parseAmount, formatDate } from "../../../providers/whmcs/utils.js";

View File

@ -5,8 +5,6 @@
* Minimal transformation - validates and normalizes only address structure. * Minimal transformation - validates and normalizes only address structure.
*/ */
import { z } from "zod";
import type { WhmcsClient, Address } from "../../schema.js"; import type { WhmcsClient, Address } from "../../schema.js";
import { whmcsClientSchema, addressSchema } from "../../schema.js"; import { whmcsClientSchema, addressSchema } from "../../schema.js";
import { import {

View File

@ -235,8 +235,6 @@ const nullableProfileFields = [
"lastlogin", "lastlogin",
] as const; ] as const;
type NullableProfileKey = (typeof nullableProfileFields)[number];
const nullableProfileOverrides = nullableProfileFields.reduce<Record<string, z.ZodTypeAny>>( const nullableProfileOverrides = nullableProfileFields.reduce<Record<string, z.ZodTypeAny>>(
(acc, field) => { (acc, field) => {
acc[field] = z.string().nullable().optional(); acc[field] = z.string().nullable().optional();

View File

@ -5,12 +5,6 @@
* These functions contain no infrastructure dependencies (no DB, no HTTP, no logging). * 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 { import type {
CreateMappingRequest, CreateMappingRequest,
UpdateMappingRequest, UpdateMappingRequest,

View File

@ -220,7 +220,6 @@ export function generateCancellationMonthOptions(
export function getCancellationEligibility(today: Date = new Date()): CancellationEligibility { export function getCancellationEligibility(today: Date = new Date()): CancellationEligibility {
const availableMonths = generateCancellationMonthOptions(today); const availableMonths = generateCancellationMonthOptions(today);
const earliestMonth = getEarliestCancellationMonth(today); const earliestMonth = getEarliestCancellationMonth(today);
const currentYearMonth = formatYearMonth(today);
const day = today.getDate(); const day = today.getDate();
// Check if current month is still available // Check if current month is still available

View File

@ -5,8 +5,6 @@
* Validated types are derived from schemas (see schema.ts). * 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 { UserIdMapping } from "../mappings/contract.js";
import type { SalesforceOrderRecord } from "./providers/salesforce/raw.types.js"; import type { SalesforceOrderRecord } from "./providers/salesforce/raw.types.js";
import type { OrderConfigurations } from "./schema.js"; import type { OrderConfigurations } from "./schema.js";

View File

@ -4,12 +4,7 @@
import type { PaymentMethod, PaymentGateway } from "../../contract.js"; import type { PaymentMethod, PaymentGateway } from "../../contract.js";
import { paymentMethodSchema, paymentGatewaySchema } from "../../schema.js"; import { paymentMethodSchema, paymentGatewaySchema } from "../../schema.js";
import { import { whmcsPaymentMethodRawSchema, whmcsPaymentGatewayRawSchema } from "./raw.types.js";
type WhmcsPaymentMethodRaw,
type WhmcsPaymentGatewayRaw,
whmcsPaymentMethodRawSchema,
whmcsPaymentGatewayRawSchema,
} from "./raw.types.js";
const PAYMENT_TYPE_MAP: Record<string, PaymentMethod["type"]> = { const PAYMENT_TYPE_MAP: Record<string, PaymentMethod["type"]> = {
creditcard: "CreditCard", creditcard: "CreditCard",

View File

@ -40,13 +40,6 @@ function coerceNumber(value: unknown): number | undefined {
return 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 // Base Product Mapper
// ============================================================================ // ============================================================================

View File

@ -5,17 +5,7 @@
import type { SimDetails, SimUsage, SimTopUpHistory, SimType, SimStatus } from "../../contract.js"; import type { SimDetails, SimUsage, SimTopUpHistory, SimType, SimStatus } from "../../contract.js";
import { simDetailsSchema, simUsageSchema, simTopUpHistorySchema } from "../../schema.js"; import { simDetailsSchema, simUsageSchema, simTopUpHistorySchema } from "../../schema.js";
import { import {
type FreebitAccountDetailsRaw,
type FreebitTrafficInfoRaw,
type FreebitQuotaHistoryRaw,
type FreebitAuthResponseRaw, type FreebitAuthResponseRaw,
type FreebitTopUpRaw,
type FreebitAddSpecRaw,
type FreebitPlanChangeRaw,
type FreebitCancelPlanRaw,
type FreebitCancelAccountRaw,
type FreebitEsimReissueRaw,
type FreebitEsimAddAccountRaw,
freebitAccountDetailsRawSchema, freebitAccountDetailsRawSchema,
freebitTrafficInfoRawSchema, freebitTrafficInfoRawSchema,
freebitQuotaHistoryRawSchema, freebitQuotaHistoryRawSchema,
@ -28,7 +18,6 @@ import {
freebitEsimReissueRawSchema, freebitEsimReissueRawSchema,
freebitEsimAddAccountRawSchema, freebitEsimAddAccountRawSchema,
} from "./raw.types.js"; } from "./raw.types.js";
import { normalizeAccount } from "./utils.js";
function asString(value: unknown): string { function asString(value: unknown): string {
if (typeof value === "string") return value; if (typeof value === "string") return value;

View File

@ -90,6 +90,17 @@ export const createCaseResponseSchema = z.object({
caseNumber: z.string(), 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<typeof supportCaseStatusSchema>; export type SupportCaseStatus = z.infer<typeof supportCaseStatusSchema>;
export type SupportCasePriority = z.infer<typeof supportCasePrioritySchema>; export type SupportCasePriority = z.infer<typeof supportCasePrioritySchema>;
export type SupportCaseCategory = z.infer<typeof supportCaseCategorySchema>; export type SupportCaseCategory = z.infer<typeof supportCaseCategorySchema>;
@ -99,3 +110,4 @@ export type SupportCaseList = z.infer<typeof supportCaseListSchema>;
export type SupportCaseFilter = z.infer<typeof supportCaseFilterSchema>; export type SupportCaseFilter = z.infer<typeof supportCaseFilterSchema>;
export type CreateCaseRequest = z.infer<typeof createCaseRequestSchema>; export type CreateCaseRequest = z.infer<typeof createCaseRequestSchema>;
export type CreateCaseResponse = z.infer<typeof createCaseResponseSchema>; export type CreateCaseResponse = z.infer<typeof createCaseResponseSchema>;
export type PublicContactRequest = z.infer<typeof publicContactRequestSchema>;