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 <noreply@anthropic.com>
This commit is contained in:
parent
9ae3d5e9c7
commit
1ef2c5e125
@ -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
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user