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 { SalesforceOrderRecord } from "@customer-portal/domain/orders/providers";
|
||||||
import type { ContactIdentityData } from "./sim-fulfillment.service.js";
|
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.
|
* Configuration fields extracted from checkout payload and Salesforce order records.
|
||||||
* Used during fulfillment for SIM activation, MNP porting, and address data.
|
* Used during fulfillment for SIM activation, MNP porting, and address data.
|
||||||
@ -81,7 +87,7 @@ export class FulfillmentContextMapper {
|
|||||||
config["activationType"] = sfOrder.Activation_Type__c;
|
config["activationType"] = sfOrder.Activation_Type__c;
|
||||||
}
|
}
|
||||||
if (!config["scheduledAt"] && sfOrder.Activation_Scheduled_At__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) {
|
if (!config["mnpPhone"] && sfOrder.MNP_Phone_Number__c) {
|
||||||
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;
|
config["mnpNumber"] = sfOrder.MNP_Reservation_Number__c;
|
||||||
}
|
}
|
||||||
if (!config["mnpExpiry"] && sfOrder.MNP_Expiry_Date__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) {
|
if (!config["mvnoAccountNumber"] && sfOrder.MVNO_Account_Number__c) {
|
||||||
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;
|
config["portingGender"] = sfOrder.Porting_Gender__c;
|
||||||
}
|
}
|
||||||
if (!config["portingDateOfBirth"] && sfOrder.Porting_DateOfBirth__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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate gender (must be M or F)
|
// Map gender to M/F for PA05-05 (accepts picklist values from SF)
|
||||||
const gender = genderRaw === "M" || genderRaw === "F" ? genderRaw : undefined;
|
const gender = this.mapGenderToCode(genderRaw);
|
||||||
if (!gender) {
|
if (!gender) {
|
||||||
this.logger.debug("Invalid or missing gender for contact identity", { genderRaw });
|
this.logger.debug("Invalid or missing gender for contact identity", { genderRaw });
|
||||||
return undefined;
|
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
|
* Format birthday from various formats to YYYYMMDD
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -84,12 +84,15 @@ interface MnpConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map Salesforce gender code to Freebit gender code.
|
* Map Salesforce gender value to Freebit gender code.
|
||||||
* Salesforce: 'M' (Male), 'F' (Female)
|
* Salesforce picklist: "Male", "Female", "Corporate/Other" (or legacy "M", "F")
|
||||||
* Freebit: 'M' (Male), 'W' (Weiblich/Female), 'C' (Corporation)
|
* Freebit codes: "M" (Male), "W" (Weiblich/Female), "C" (Corporation)
|
||||||
*/
|
*/
|
||||||
function mapGenderToFreebit(gender: string): string {
|
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;
|
return gender;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -96,7 +96,7 @@ export const salesforceOrderRecordSchema = z.object({
|
|||||||
Assign_Physical_SIM__c: z.string().nullable().optional(), // Lookup to SIM_Inventory__c
|
Assign_Physical_SIM__c: z.string().nullable().optional(), // Lookup to SIM_Inventory__c
|
||||||
|
|
||||||
// MNP (Mobile Number Portability) fields
|
// 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_Reservation_Number__c: z.string().nullable().optional(),
|
||||||
MNP_Expiry_Date__c: z.string().nullable().optional(),
|
MNP_Expiry_Date__c: z.string().nullable().optional(),
|
||||||
MNP_Phone_Number__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")
|
.regex(/^\d{32}$/, "EID must be exactly 32 digits")
|
||||||
.optional(),
|
.optional(),
|
||||||
isMnp: z.string().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(),
|
mnpExpiry: z.string().optional(),
|
||||||
mnpPhone: z.string().max(11, "MNP phone number must be 11 digits or fewer").optional(),
|
mnpPhone: z.string().max(11, "MNP phone number must be 11 digits or fewer").optional(),
|
||||||
mvnoAccountNumber: z.string().optional(),
|
mvnoAccountNumber: z.string().max(255).optional(),
|
||||||
portingLastName: z.string().optional(),
|
portingLastName: z.string().max(255).optional(),
|
||||||
portingFirstName: z.string().optional(),
|
portingFirstName: z.string().max(255).optional(),
|
||||||
portingLastNameKatakana: z.string().optional(),
|
portingLastNameKatakana: z.string().max(255).optional(),
|
||||||
portingFirstNameKatakana: z.string().optional(),
|
portingFirstNameKatakana: z.string().max(255).optional(),
|
||||||
portingGender: z.enum(PORTING_GENDER_VALUES).optional(),
|
portingGender: z.enum(PORTING_GENDER_VALUES).optional(),
|
||||||
portingDateOfBirth: z.string().optional(),
|
portingDateOfBirth: z.string().optional(),
|
||||||
address: orderConfigurationsAddressSchema.optional(),
|
address: orderConfigurationsAddressSchema.optional(),
|
||||||
@ -172,14 +172,14 @@ export const orderSelectionsSchema = z
|
|||||||
.regex(/^\d{32}$/, "EID must be exactly 32 digits")
|
.regex(/^\d{32}$/, "EID must be exactly 32 digits")
|
||||||
.optional(),
|
.optional(),
|
||||||
isMnp: z.string().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(),
|
mnpExpiry: z.string().optional(),
|
||||||
mnpPhone: z.string().max(11, "MNP phone number must be 11 digits or fewer").optional(),
|
mnpPhone: z.string().max(11, "MNP phone number must be 11 digits or fewer").optional(),
|
||||||
mvnoAccountNumber: z.string().optional(),
|
mvnoAccountNumber: z.string().max(255).optional(),
|
||||||
portingLastName: z.string().optional(),
|
portingLastName: z.string().max(255).optional(),
|
||||||
portingFirstName: z.string().optional(),
|
portingFirstName: z.string().max(255).optional(),
|
||||||
portingLastNameKatakana: z.string().optional(),
|
portingLastNameKatakana: z.string().max(255).optional(),
|
||||||
portingFirstNameKatakana: z.string().optional(),
|
portingFirstNameKatakana: z.string().max(255).optional(),
|
||||||
portingGender: z.enum(PORTING_GENDER_VALUES).optional(),
|
portingGender: z.enum(PORTING_GENDER_VALUES).optional(),
|
||||||
portingDateOfBirth: z.string().optional(),
|
portingDateOfBirth: z.string().optional(),
|
||||||
address: z
|
address: z
|
||||||
|
|||||||
@ -399,7 +399,10 @@ export const simChangePlanFullRequestSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const simMnpFormSchema = 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"),
|
expiryDate: z.string().regex(/^\d{8}$/, "Expiry date must be in YYYYMMDD format"),
|
||||||
phoneNumber: z
|
phoneNumber: z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user