From 1ef2c5e125f3ec3b745922c8ffd988c92436efad Mon Sep 17 00:00:00 2001 From: Temuulen Ankhbayar Date: Sat, 7 Mar 2026 11:32:31 +0900 Subject: [PATCH] fix: resolve SF field type mismatches, date format conversions, and gender mapping - MNP_Application__c: change Zod type from z.string() to z.coerce.boolean() to match Salesforce Checkbox field type - Date fields (mnpExpiry, portingDateOfBirth, scheduledAt): convert YYYY-MM-DD back to YYYYMMDD when reading from SF for Freebit API - Gender mapping: handle full SF picklist values (Male/Female/Corporate) not just legacy M/F codes, for both Freebit activation and PA05-05 - MNP reservation number: add max(10) per SF Text(10) field limit - Porting name fields: add max(255) to match SF Text(255) limits Co-Authored-By: Claude Opus 4.6 --- .../fulfillment-context-mapper.service.ts | 28 +++++++++++++++---- .../services/sim-fulfillment.service.ts | 11 +++++--- .../orders/providers/salesforce/raw.types.ts | 2 +- packages/domain/orders/schema.ts | 24 ++++++++-------- packages/domain/sim/schema.ts | 5 +++- 5 files changed, 47 insertions(+), 23 deletions(-) diff --git a/apps/bff/src/modules/orders/services/fulfillment-context-mapper.service.ts b/apps/bff/src/modules/orders/services/fulfillment-context-mapper.service.ts index 424dfd79..2bc7c140 100644 --- a/apps/bff/src/modules/orders/services/fulfillment-context-mapper.service.ts +++ b/apps/bff/src/modules/orders/services/fulfillment-context-mapper.service.ts @@ -3,6 +3,12 @@ import { Logger } from "nestjs-pino"; import type { SalesforceOrderRecord } from "@customer-portal/domain/orders/providers"; import type { ContactIdentityData } from "./sim-fulfillment.service.js"; +/** Strip dashes from SF date (YYYY-MM-DD → YYYYMMDD) for Freebit API */ +function sfDateToYYYYMMDD(date: string): string { + const stripped = date.replace(/-/g, ""); + return /^\d{8}$/.test(stripped) ? stripped : date; +} + /** * Configuration fields extracted from checkout payload and Salesforce order records. * Used during fulfillment for SIM activation, MNP porting, and address data. @@ -81,7 +87,7 @@ export class FulfillmentContextMapper { config["activationType"] = sfOrder.Activation_Type__c; } if (!config["scheduledAt"] && sfOrder.Activation_Scheduled_At__c) { - config["scheduledAt"] = sfOrder.Activation_Scheduled_At__c; + config["scheduledAt"] = sfDateToYYYYMMDD(sfOrder.Activation_Scheduled_At__c); } if (!config["mnpPhone"] && sfOrder.MNP_Phone_Number__c) { config["mnpPhone"] = sfOrder.MNP_Phone_Number__c; @@ -94,7 +100,7 @@ export class FulfillmentContextMapper { config["mnpNumber"] = sfOrder.MNP_Reservation_Number__c; } if (!config["mnpExpiry"] && sfOrder.MNP_Expiry_Date__c) { - config["mnpExpiry"] = sfOrder.MNP_Expiry_Date__c; + config["mnpExpiry"] = sfDateToYYYYMMDD(sfOrder.MNP_Expiry_Date__c); } if (!config["mvnoAccountNumber"] && sfOrder.MVNO_Account_Number__c) { config["mvnoAccountNumber"] = sfOrder.MVNO_Account_Number__c; @@ -115,7 +121,7 @@ export class FulfillmentContextMapper { config["portingGender"] = sfOrder.Porting_Gender__c; } if (!config["portingDateOfBirth"] && sfOrder.Porting_DateOfBirth__c) { - config["portingDateOfBirth"] = sfOrder.Porting_DateOfBirth__c; + config["portingDateOfBirth"] = sfDateToYYYYMMDD(sfOrder.Porting_DateOfBirth__c); } } @@ -207,8 +213,8 @@ export class FulfillmentContextMapper { return undefined; } - // Validate gender (must be M or F) - const gender = genderRaw === "M" || genderRaw === "F" ? genderRaw : undefined; + // Map gender to M/F for PA05-05 (accepts picklist values from SF) + const gender = this.mapGenderToCode(genderRaw); if (!gender) { this.logger.debug("Invalid or missing gender for contact identity", { genderRaw }); return undefined; @@ -231,6 +237,18 @@ export class FulfillmentContextMapper { }; } + /** + * Map Salesforce gender picklist value to single-letter code. + * SF picklist: "Male", "Female", "Corporate/Other" (or legacy "M", "F") + */ + private mapGenderToCode(gender?: string | null): "M" | "F" | undefined { + if (!gender) return undefined; + const normalized = gender.trim().toLowerCase(); + if (normalized === "male" || normalized === "m") return "M"; + if (normalized === "female" || normalized === "f") return "F"; + return undefined; + } + /** * Format birthday from various formats to YYYYMMDD */ diff --git a/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts b/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts index 0cee296f..102b9061 100644 --- a/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts +++ b/apps/bff/src/modules/orders/services/sim-fulfillment.service.ts @@ -84,12 +84,15 @@ interface MnpConfig { } /** - * Map Salesforce gender code to Freebit gender code. - * Salesforce: 'M' (Male), 'F' (Female) - * Freebit: 'M' (Male), 'W' (Weiblich/Female), 'C' (Corporation) + * Map Salesforce gender value to Freebit gender code. + * Salesforce picklist: "Male", "Female", "Corporate/Other" (or legacy "M", "F") + * Freebit codes: "M" (Male), "W" (Weiblich/Female), "C" (Corporation) */ function mapGenderToFreebit(gender: string): string { - if (gender === "F") return "W"; + const normalized = gender.trim().toLowerCase(); + if (normalized === "female" || normalized === "f") return "W"; + if (normalized === "male" || normalized === "m") return "M"; + if (normalized.startsWith("corporate") || normalized === "c") return "C"; return gender; } diff --git a/packages/domain/orders/providers/salesforce/raw.types.ts b/packages/domain/orders/providers/salesforce/raw.types.ts index 5a4a8207..0342e06a 100644 --- a/packages/domain/orders/providers/salesforce/raw.types.ts +++ b/packages/domain/orders/providers/salesforce/raw.types.ts @@ -96,7 +96,7 @@ export const salesforceOrderRecordSchema = z.object({ Assign_Physical_SIM__c: z.string().nullable().optional(), // Lookup to SIM_Inventory__c // MNP (Mobile Number Portability) fields - MNP_Application__c: z.string().nullable().optional(), + MNP_Application__c: z.coerce.boolean().nullable().optional(), MNP_Reservation_Number__c: z.string().nullable().optional(), MNP_Expiry_Date__c: z.string().nullable().optional(), MNP_Phone_Number__c: z.string().nullable().optional(), diff --git a/packages/domain/orders/schema.ts b/packages/domain/orders/schema.ts index 8add87ee..77334266 100644 --- a/packages/domain/orders/schema.ts +++ b/packages/domain/orders/schema.ts @@ -136,14 +136,14 @@ export const orderConfigurationsSchema = z.object({ .regex(/^\d{32}$/, "EID must be exactly 32 digits") .optional(), isMnp: z.string().optional(), - mnpNumber: z.string().optional(), + mnpNumber: z.string().max(10, "MNP reservation number must be 10 digits or fewer").optional(), mnpExpiry: z.string().optional(), mnpPhone: z.string().max(11, "MNP phone number must be 11 digits or fewer").optional(), - mvnoAccountNumber: z.string().optional(), - portingLastName: z.string().optional(), - portingFirstName: z.string().optional(), - portingLastNameKatakana: z.string().optional(), - portingFirstNameKatakana: z.string().optional(), + mvnoAccountNumber: z.string().max(255).optional(), + portingLastName: z.string().max(255).optional(), + portingFirstName: z.string().max(255).optional(), + portingLastNameKatakana: z.string().max(255).optional(), + portingFirstNameKatakana: z.string().max(255).optional(), portingGender: z.enum(PORTING_GENDER_VALUES).optional(), portingDateOfBirth: z.string().optional(), address: orderConfigurationsAddressSchema.optional(), @@ -172,14 +172,14 @@ export const orderSelectionsSchema = z .regex(/^\d{32}$/, "EID must be exactly 32 digits") .optional(), isMnp: z.string().optional(), - mnpNumber: z.string().optional(), + mnpNumber: z.string().max(10, "MNP reservation number must be 10 digits or fewer").optional(), mnpExpiry: z.string().optional(), mnpPhone: z.string().max(11, "MNP phone number must be 11 digits or fewer").optional(), - mvnoAccountNumber: z.string().optional(), - portingLastName: z.string().optional(), - portingFirstName: z.string().optional(), - portingLastNameKatakana: z.string().optional(), - portingFirstNameKatakana: z.string().optional(), + mvnoAccountNumber: z.string().max(255).optional(), + portingLastName: z.string().max(255).optional(), + portingFirstName: z.string().max(255).optional(), + portingLastNameKatakana: z.string().max(255).optional(), + portingFirstNameKatakana: z.string().max(255).optional(), portingGender: z.enum(PORTING_GENDER_VALUES).optional(), portingDateOfBirth: z.string().optional(), address: z diff --git a/packages/domain/sim/schema.ts b/packages/domain/sim/schema.ts index cbf22578..f06945c2 100644 --- a/packages/domain/sim/schema.ts +++ b/packages/domain/sim/schema.ts @@ -399,7 +399,10 @@ export const simChangePlanFullRequestSchema = z.object({ }); const simMnpFormSchema = z.object({ - reservationNumber: z.string().min(1, "Reservation number is required"), + reservationNumber: z + .string() + .min(1, "Reservation number is required") + .max(10, "Reservation number must be 10 digits or fewer"), expiryDate: z.string().regex(/^\d{8}$/, "Expiry date must be in YYYYMMDD format"), phoneNumber: z .string()