fix: reset order config state when selecting a new plan and validate MNP phone length
Order wizard was skipping steps (jumping to add-ons) due to stale currentStep persisting in localStorage from previous orders. Reset store on plan selection and exclude currentStep from persistence. Also add max(11) validation on MNP phone number to prevent Salesforce STRING_TOO_LONG errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1610e436a5
commit
454fb29c85
@ -158,8 +158,9 @@ export class FreebitEsimService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Note: authKey is injected by makeAuthenticatedJsonRequest
|
// Note: authKey is injected by makeAuthenticatedRequest
|
||||||
// createType is not required when addKind is 'R' (reissue)
|
// createType is not required when addKind is 'R' (reissue)
|
||||||
|
// Only include defined optional fields — Freebit rejects unknown/null values
|
||||||
const payload: Omit<FreebitEsimAccountActivationRequest, "authKey"> = {
|
const payload: Omit<FreebitEsimAccountActivationRequest, "authKey"> = {
|
||||||
aladinOperated,
|
aladinOperated,
|
||||||
...(finalAddKind !== "R" && { createType: "new" }),
|
...(finalAddKind !== "R" && { createType: "new" }),
|
||||||
@ -167,12 +168,12 @@ export class FreebitEsimService {
|
|||||||
account,
|
account,
|
||||||
simkind: simKind || "E0",
|
simkind: simKind || "E0",
|
||||||
addKind: finalAddKind,
|
addKind: finalAddKind,
|
||||||
planCode,
|
...(planCode && { planCode }),
|
||||||
contractLine,
|
...(contractLine && { contractLine }),
|
||||||
shipDate,
|
...(shipDate && { shipDate }),
|
||||||
repAccount,
|
...(repAccount && { repAccount }),
|
||||||
deliveryCode,
|
...(deliveryCode && { deliveryCode }),
|
||||||
globalIp,
|
...(globalIp && { globalIp }),
|
||||||
// MNP object includes both reservation info AND identity fields (all Level 2 per PA05-41 spec)
|
// MNP object includes both reservation info AND identity fields (all Level 2 per PA05-41 spec)
|
||||||
...(mnp ? { mnp } : {}),
|
...(mnp ? { mnp } : {}),
|
||||||
} as Omit<FreebitEsimAccountActivationRequest, "authKey">;
|
} as Omit<FreebitEsimAccountActivationRequest, "authKey">;
|
||||||
@ -204,7 +205,7 @@ export class FreebitEsimService {
|
|||||||
: "no-mnp",
|
: "no-mnp",
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.client.makeAuthenticatedJsonRequest<
|
await this.client.makeAuthenticatedRequest<
|
||||||
FreebitEsimAccountActivationResponse,
|
FreebitEsimAccountActivationResponse,
|
||||||
Omit<FreebitEsimAccountActivationRequest, "authKey">
|
Omit<FreebitEsimAccountActivationRequest, "authKey">
|
||||||
>("/mvno/esim/addAcnt/", payload);
|
>("/mvno/esim/addAcnt/", payload);
|
||||||
|
|||||||
@ -345,6 +345,10 @@ export class FulfillmentStepExecutors {
|
|||||||
/**
|
/**
|
||||||
* Update Salesforce with WHMCS registration info
|
* Update Salesforce with WHMCS registration info
|
||||||
* This is the final step that sets Status to "Processed"
|
* This is the final step that sets Status to "Processed"
|
||||||
|
*
|
||||||
|
* If the order is already in an activated status (e.g. auto-approved by SF Flow),
|
||||||
|
* the integration user may lack "Edit Activated Orders" permission. In that case,
|
||||||
|
* we retry with only custom field updates (no Status change).
|
||||||
*/
|
*/
|
||||||
async executeSfRegistrationComplete(
|
async executeSfRegistrationComplete(
|
||||||
ctx: OrderFulfillmentContext,
|
ctx: OrderFulfillmentContext,
|
||||||
@ -361,7 +365,31 @@ export class FulfillmentStepExecutors {
|
|||||||
WHMCS_Order_ID__c: whmcsCreateResult?.orderId?.toString(),
|
WHMCS_Order_ID__c: whmcsCreateResult?.orderId?.toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.salesforceFacade.updateOrder(updatePayload);
|
try {
|
||||||
|
await this.salesforceFacade.updateOrder(updatePayload);
|
||||||
|
} catch (error) {
|
||||||
|
// SF returns permission error when order is already in an activated status
|
||||||
|
// (e.g. auto-approved by a Flow). Retry with only custom fields, no Status change.
|
||||||
|
const msg = error instanceof Error ? error.message : String(error);
|
||||||
|
if (
|
||||||
|
msg.includes("有効注文") ||
|
||||||
|
msg.includes("Edit Activated Orders") ||
|
||||||
|
msg.includes("FIELD_INTEGRITY_EXCEPTION")
|
||||||
|
) {
|
||||||
|
this.logger.warn("SF order already activated, retrying without Status change", {
|
||||||
|
sfOrderId: ctx.sfOrderId,
|
||||||
|
originalError: msg,
|
||||||
|
});
|
||||||
|
await this.salesforceFacade.updateOrder({
|
||||||
|
Id: ctx.sfOrderId,
|
||||||
|
Activation_Status__c: "Activated",
|
||||||
|
WHMCS_Order_ID__c: whmcsCreateResult?.orderId?.toString(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.sideEffects.publishStatusUpdate(ctx.sfOrderId, {
|
this.sideEffects.publishStatusUpdate(ctx.sfOrderId, {
|
||||||
status: "Processed",
|
status: "Processed",
|
||||||
activationStatus: "Activated",
|
activationStatus: "Activated",
|
||||||
@ -375,7 +403,6 @@ export class FulfillmentStepExecutors {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await this.sideEffects.notifyOrderActivated(ctx.sfOrderId, ctx.validation?.sfOrder?.AccountId);
|
await this.sideEffects.notifyOrderActivated(ctx.sfOrderId, ctx.validation?.sfOrder?.AccountId);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -18,6 +18,16 @@ function assignIfString(target: SalesforceOrderFields, key: string, value: unkno
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Convert YYYYMMDD to YYYY-MM-DD for Salesforce Date fields */
|
||||||
|
function toSalesforceDate(yyyymmdd: unknown): string | undefined {
|
||||||
|
if (typeof yyyymmdd !== "string") return undefined;
|
||||||
|
const d = yyyymmdd.trim().replace(/-/g, "");
|
||||||
|
if (/^\d{8}$/.test(d)) {
|
||||||
|
return `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
|
||||||
|
}
|
||||||
|
return yyyymmdd;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds Salesforce Order records from domain order requests
|
* Builds Salesforce Order records from domain order requests
|
||||||
*/
|
*/
|
||||||
@ -74,7 +84,11 @@ export class OrderBuilder {
|
|||||||
const config = body.configurations || {};
|
const config = body.configurations || {};
|
||||||
|
|
||||||
assignIfString(orderFields, fieldNames.activationType, config.activationType);
|
assignIfString(orderFields, fieldNames.activationType, config.activationType);
|
||||||
assignIfString(orderFields, fieldNames.activationScheduledAt, config.scheduledAt);
|
assignIfString(
|
||||||
|
orderFields,
|
||||||
|
fieldNames.activationScheduledAt,
|
||||||
|
toSalesforceDate(config.scheduledAt)
|
||||||
|
);
|
||||||
orderFields[fieldNames.activationStatus] = "Not Started";
|
orderFields[fieldNames.activationStatus] = "Not Started";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +113,7 @@ export class OrderBuilder {
|
|||||||
if (config.isMnp === "true") {
|
if (config.isMnp === "true") {
|
||||||
orderFields[fieldNames.mnpApplication] = true;
|
orderFields[fieldNames.mnpApplication] = true;
|
||||||
assignIfString(orderFields, fieldNames.mnpReservation, config.mnpNumber);
|
assignIfString(orderFields, fieldNames.mnpReservation, config.mnpNumber);
|
||||||
assignIfString(orderFields, fieldNames.mnpExpiry, config.mnpExpiry);
|
assignIfString(orderFields, fieldNames.mnpExpiry, toSalesforceDate(config.mnpExpiry));
|
||||||
assignIfString(orderFields, fieldNames.mnpPhone, config.mnpPhone);
|
assignIfString(orderFields, fieldNames.mnpPhone, config.mnpPhone);
|
||||||
assignIfString(orderFields, fieldNames.mvnoAccountNumber, config.mvnoAccountNumber);
|
assignIfString(orderFields, fieldNames.mvnoAccountNumber, config.mvnoAccountNumber);
|
||||||
assignIfString(orderFields, fieldNames.portingLastName, config.portingLastName);
|
assignIfString(orderFields, fieldNames.portingLastName, config.portingLastName);
|
||||||
@ -115,7 +129,11 @@ export class OrderBuilder {
|
|||||||
config.portingFirstNameKatakana
|
config.portingFirstNameKatakana
|
||||||
);
|
);
|
||||||
assignIfString(orderFields, fieldNames.portingGender, config.portingGender);
|
assignIfString(orderFields, fieldNames.portingGender, config.portingGender);
|
||||||
assignIfString(orderFields, fieldNames.portingDateOfBirth, config.portingDateOfBirth);
|
assignIfString(
|
||||||
|
orderFields,
|
||||||
|
fieldNames.portingDateOfBirth,
|
||||||
|
toSalesforceDate(config.portingDateOfBirth)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -313,16 +313,17 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update Salesforce with failure status
|
// Update Salesforce with failure status
|
||||||
|
const errorShortCode = (
|
||||||
|
this.orderFulfillmentErrorService.getShortCode(error) || String(errorCode)
|
||||||
|
)
|
||||||
|
.toString()
|
||||||
|
.slice(0, 60);
|
||||||
try {
|
try {
|
||||||
await this.salesforceFacade.updateOrder({
|
await this.salesforceFacade.updateOrder({
|
||||||
Id: sfOrderId,
|
Id: sfOrderId,
|
||||||
Status: "Pending Review",
|
Status: "Pending Review",
|
||||||
Activation_Status__c: "Failed",
|
Activation_Status__c: "Failed",
|
||||||
Activation_Error_Code__c: (
|
Activation_Error_Code__c: errorShortCode,
|
||||||
this.orderFulfillmentErrorService.getShortCode(error) || String(errorCode)
|
|
||||||
)
|
|
||||||
.toString()
|
|
||||||
.slice(0, 60),
|
|
||||||
Activation_Error_Message__c: userMessage?.slice(0, 255),
|
Activation_Error_Message__c: userMessage?.slice(0, 255),
|
||||||
});
|
});
|
||||||
this.logger.log("Salesforce updated with failure status", {
|
this.logger.log("Salesforce updated with failure status", {
|
||||||
@ -330,10 +331,33 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
errorCode,
|
errorCode,
|
||||||
});
|
});
|
||||||
} catch (updateError) {
|
} catch (updateError) {
|
||||||
this.logger.error("Failed to update Salesforce with error status", {
|
// If the order is already activated (e.g. auto-approved by SF Flow),
|
||||||
sfOrderId: context.sfOrderId,
|
// retry without Status change — only update custom error fields
|
||||||
updateError: updateError instanceof Error ? updateError.message : String(updateError),
|
const updateMsg = updateError instanceof Error ? updateError.message : String(updateError);
|
||||||
});
|
if (updateMsg.includes("有効注文") || updateMsg.includes("Edit Activated Orders")) {
|
||||||
|
try {
|
||||||
|
await this.salesforceFacade.updateOrder({
|
||||||
|
Id: sfOrderId,
|
||||||
|
Activation_Status__c: "Failed",
|
||||||
|
Activation_Error_Code__c: errorShortCode,
|
||||||
|
Activation_Error_Message__c: userMessage?.slice(0, 255),
|
||||||
|
});
|
||||||
|
this.logger.log("Salesforce updated with failure status (without Status change)", {
|
||||||
|
sfOrderId: context.sfOrderId,
|
||||||
|
errorCode,
|
||||||
|
});
|
||||||
|
} catch (retryError) {
|
||||||
|
this.logger.error("Failed to update Salesforce with error status (retry)", {
|
||||||
|
sfOrderId: context.sfOrderId,
|
||||||
|
retryError: retryError instanceof Error ? retryError.message : String(retryError),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.error("Failed to update Salesforce with error status", {
|
||||||
|
sfOrderId: context.sfOrderId,
|
||||||
|
updateError: updateMsg,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish failure events and notifications
|
// Publish failure events and notifications
|
||||||
|
|||||||
@ -64,8 +64,10 @@ export function useInternetConfigure(): UseInternetConfigureResult {
|
|||||||
|
|
||||||
// Initialize/restore state on mount
|
// Initialize/restore state on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If URL has plan param but store doesn't, this is a fresh entry
|
// If URL has a different plan than stored, reset config for fresh start
|
||||||
if (urlPlanSku && configState.planSku !== urlPlanSku) {
|
if (urlPlanSku && configState.planSku !== urlPlanSku) {
|
||||||
|
const resetInternetConfig = useCatalogStore.getState().resetInternetConfig;
|
||||||
|
resetInternetConfig();
|
||||||
setConfig({ planSku: urlPlanSku });
|
setConfig({ planSku: urlPlanSku });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,11 @@ function trimOptional(value?: string | null) {
|
|||||||
return trimmed.length > 0 ? trimmed : undefined;
|
return trimmed.length > 0 ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Convert YYYY-MM-DD (from date input) to YYYYMMDD (FreeBit API format) */
|
||||||
|
function toYYYYMMDD(value: string): string {
|
||||||
|
return value.replace(/-/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
function buildFormData(configState: SimConfigState): SimConfigureFormData {
|
function buildFormData(configState: SimConfigState): SimConfigureFormData {
|
||||||
return {
|
return {
|
||||||
simType: configState.simType,
|
simType: configState.simType,
|
||||||
@ -69,13 +74,13 @@ function buildFormData(configState: SimConfigState): SimConfigureFormData {
|
|||||||
activationType: configState.activationType,
|
activationType: configState.activationType,
|
||||||
scheduledActivationDate:
|
scheduledActivationDate:
|
||||||
configState.activationType === "Scheduled"
|
configState.activationType === "Scheduled"
|
||||||
? trimOptional(configState.scheduledActivationDate)
|
? trimOptional(configState.scheduledActivationDate)?.replace(/-/g, "")
|
||||||
: undefined,
|
: undefined,
|
||||||
wantsMnp: configState.wantsMnp,
|
wantsMnp: configState.wantsMnp,
|
||||||
mnpData: configState.wantsMnp
|
mnpData: configState.wantsMnp
|
||||||
? {
|
? {
|
||||||
reservationNumber: configState.mnpData.reservationNumber.trim(),
|
reservationNumber: configState.mnpData.reservationNumber.trim(),
|
||||||
expiryDate: configState.mnpData.expiryDate.trim(),
|
expiryDate: toYYYYMMDD(configState.mnpData.expiryDate.trim()),
|
||||||
phoneNumber: configState.mnpData.phoneNumber.trim(),
|
phoneNumber: configState.mnpData.phoneNumber.trim(),
|
||||||
mvnoAccountNumber: trimOptional(configState.mnpData.mvnoAccountNumber),
|
mvnoAccountNumber: trimOptional(configState.mnpData.mvnoAccountNumber),
|
||||||
portingLastName: trimOptional(configState.mnpData.portingLastName),
|
portingLastName: trimOptional(configState.mnpData.portingLastName),
|
||||||
@ -83,7 +88,10 @@ function buildFormData(configState: SimConfigState): SimConfigureFormData {
|
|||||||
portingLastNameKatakana: trimOptional(configState.mnpData.portingLastNameKatakana),
|
portingLastNameKatakana: trimOptional(configState.mnpData.portingLastNameKatakana),
|
||||||
portingFirstNameKatakana: trimOptional(configState.mnpData.portingFirstNameKatakana),
|
portingFirstNameKatakana: trimOptional(configState.mnpData.portingFirstNameKatakana),
|
||||||
portingGender: trimOptional(configState.mnpData.portingGender),
|
portingGender: trimOptional(configState.mnpData.portingGender),
|
||||||
portingDateOfBirth: trimOptional(configState.mnpData.portingDateOfBirth),
|
portingDateOfBirth: trimOptional(configState.mnpData.portingDateOfBirth)?.replace(
|
||||||
|
/-/g,
|
||||||
|
""
|
||||||
|
),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
@ -149,6 +157,8 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const effectivePlanSku = urlPlanSku || planId;
|
const effectivePlanSku = urlPlanSku || planId;
|
||||||
if (effectivePlanSku && configState.planSku !== effectivePlanSku) {
|
if (effectivePlanSku && configState.planSku !== effectivePlanSku) {
|
||||||
|
const resetSimConfig = useCatalogStore.getState().resetSimConfig;
|
||||||
|
resetSimConfig();
|
||||||
setConfig({ planSku: effectivePlanSku });
|
setConfig({ planSku: effectivePlanSku });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -329,9 +329,13 @@ export const useCatalogStore = create<CatalogStore>()(
|
|||||||
name: "services-config-store",
|
name: "services-config-store",
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
partialize: state => ({
|
partialize: state => ({
|
||||||
internet: state.internet,
|
internet: {
|
||||||
|
...state.internet,
|
||||||
|
currentStep: 1,
|
||||||
|
},
|
||||||
sim: {
|
sim: {
|
||||||
...state.sim,
|
...state.sim,
|
||||||
|
currentStep: 1,
|
||||||
eid: "",
|
eid: "",
|
||||||
wantsMnp: false,
|
wantsMnp: false,
|
||||||
mnpData: { ...initialSimState.mnpData },
|
mnpData: { ...initialSimState.mnpData },
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import type { SimCatalogProduct } from "@customer-portal/domain/services";
|
import type { SimCatalogProduct } from "@customer-portal/domain/services";
|
||||||
import { usePublicSimCatalog } from "@/features/services/hooks";
|
import { usePublicSimCatalog } from "@/features/services/hooks";
|
||||||
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
||||||
|
import { useCatalogStore } from "@/features/services/stores/services.store";
|
||||||
import {
|
import {
|
||||||
SimPlansContent,
|
SimPlansContent,
|
||||||
type SimPlansTab,
|
type SimPlansTab,
|
||||||
@ -26,6 +27,9 @@ export function PublicSimPlansView() {
|
|||||||
const [activeTab, setActiveTab] = useState<SimPlansTab>("data-voice");
|
const [activeTab, setActiveTab] = useState<SimPlansTab>("data-voice");
|
||||||
|
|
||||||
const handleSelectPlan = (planSku: string) => {
|
const handleSelectPlan = (planSku: string) => {
|
||||||
|
const { resetSimConfig, setSimConfig } = useCatalogStore.getState();
|
||||||
|
resetSimConfig();
|
||||||
|
setSimConfig({ planSku, currentStep: 1 });
|
||||||
router.push(`${servicesBasePath}/sim/configure?planSku=${encodeURIComponent(planSku)}`);
|
router.push(`${servicesBasePath}/sim/configure?planSku=${encodeURIComponent(planSku)}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import type { SimCatalogProduct } from "@customer-portal/domain/services";
|
import type { SimCatalogProduct } from "@customer-portal/domain/services";
|
||||||
import { useAccountSimCatalog } from "@/features/services/hooks";
|
import { useAccountSimCatalog } from "@/features/services/hooks";
|
||||||
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
||||||
|
import { useCatalogStore } from "@/features/services/stores/services.store";
|
||||||
import {
|
import {
|
||||||
SimPlansContent,
|
SimPlansContent,
|
||||||
type SimPlansTab,
|
type SimPlansTab,
|
||||||
@ -26,6 +27,9 @@ export function SimPlansContainer() {
|
|||||||
const [activeTab, setActiveTab] = useState<SimPlansTab>("data-voice");
|
const [activeTab, setActiveTab] = useState<SimPlansTab>("data-voice");
|
||||||
|
|
||||||
const handleSelectPlan = (planSku: string) => {
|
const handleSelectPlan = (planSku: string) => {
|
||||||
|
const { resetSimConfig, setSimConfig } = useCatalogStore.getState();
|
||||||
|
resetSimConfig();
|
||||||
|
setSimConfig({ planSku, currentStep: 1 });
|
||||||
router.push(`${servicesBasePath}/sim/configure?planSku=${encodeURIComponent(planSku)}`);
|
router.push(`${servicesBasePath}/sim/configure?planSku=${encodeURIComponent(planSku)}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -135,7 +135,7 @@ export const orderConfigurationsSchema = z.object({
|
|||||||
isMnp: z.string().optional(),
|
isMnp: z.string().optional(),
|
||||||
mnpNumber: z.string().optional(),
|
mnpNumber: z.string().optional(),
|
||||||
mnpExpiry: z.string().optional(),
|
mnpExpiry: z.string().optional(),
|
||||||
mnpPhone: z.string().optional(),
|
mnpPhone: z.string().max(11, "MNP phone number must be 11 digits or fewer").optional(),
|
||||||
mvnoAccountNumber: z.string().optional(),
|
mvnoAccountNumber: z.string().optional(),
|
||||||
portingLastName: z.string().optional(),
|
portingLastName: z.string().optional(),
|
||||||
portingFirstName: z.string().optional(),
|
portingFirstName: z.string().optional(),
|
||||||
@ -168,7 +168,7 @@ export const orderSelectionsSchema = z
|
|||||||
isMnp: z.string().optional(),
|
isMnp: z.string().optional(),
|
||||||
mnpNumber: z.string().optional(),
|
mnpNumber: z.string().optional(),
|
||||||
mnpExpiry: z.string().optional(),
|
mnpExpiry: z.string().optional(),
|
||||||
mnpPhone: z.string().optional(),
|
mnpPhone: z.string().max(11, "MNP phone number must be 11 digits or fewer").optional(),
|
||||||
mvnoAccountNumber: z.string().optional(),
|
mvnoAccountNumber: z.string().optional(),
|
||||||
portingLastName: z.string().optional(),
|
portingLastName: z.string().optional(),
|
||||||
portingFirstName: z.string().optional(),
|
portingFirstName: z.string().optional(),
|
||||||
|
|||||||
@ -401,7 +401,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"),
|
||||||
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.string().min(1, "Phone number is required"),
|
phoneNumber: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Phone number is required")
|
||||||
|
.max(11, "Phone number must be 11 digits or fewer"),
|
||||||
mvnoAccountNumber: z.string().optional(),
|
mvnoAccountNumber: z.string().optional(),
|
||||||
portingLastName: z.string().optional(),
|
portingLastName: z.string().optional(),
|
||||||
portingFirstName: z.string().optional(),
|
portingFirstName: z.string().optional(),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user