big boi refactor to customer inof from passenger info

This commit is contained in:
user
2025-10-20 21:46:26 +03:00
parent 2cc0ca4c51
commit 2fdb934ec9
53 changed files with 702 additions and 2068 deletions

View File

@@ -1,12 +0,0 @@
import { z } from "zod";
export const numberModel = z
.union([
z.coerce
.number()
.refine((val) => !isNaN(val), { message: "Must be a valid number" }),
z.undefined(),
])
.transform((value) => {
return value !== undefined && isNaN(value) ? undefined : value;
});

View File

@@ -1,7 +1,7 @@
import { z } from "zod";
import {
PassengerPII,
passengerPIIModel,
CustomerInfo,
customerInfoModel,
} from "../../passengerinfo/data/entities";
import {
PaymentDetailsPayload,
@@ -94,7 +94,7 @@ export const flowInfoModel = z.object({
paymentInfoLastSyncedAt: z.string().datetime().optional(),
pendingActions: pendingActionsModel.default([]),
personalInfo: z.custom<PassengerPII>().optional(),
personalInfo: z.custom<CustomerInfo>().optional(),
paymentInfo: z.custom<PaymentDetailsPayload>().optional(),
refOids: z.array(z.number()).optional(),
@@ -143,14 +143,14 @@ export type CreateCheckoutFlowPayload = z.infer<
// Step-specific payloads
export const prePaymentFlowStepPayloadModel = z.object({
initialUrl: z.string(),
personalInfo: passengerPIIModel.optional(),
personalInfo: customerInfoModel.optional(),
});
export type PrePaymentFlowStepPayload = z.infer<
typeof prePaymentFlowStepPayloadModel
>;
export const paymentFlowStepPayloadModel = z.object({
personalInfo: passengerPIIModel.optional(),
personalInfo: customerInfoModel.optional(),
paymentInfo: paymentDetailsPayloadModel.optional(),
});
export type PaymentFlowStepPayload = z.infer<

View File

@@ -0,0 +1,46 @@
import { z } from "zod";
export const customerInfoModel = z.object({
id: z.number().optional(),
firstName: z.string().min(1).max(64),
middleName: z.string().max(64).default(""),
lastName: z.string().min(1).max(64),
email: z.string().email().max(128),
phoneCountryCode: z.string().min(1).max(6),
phoneNumber: z.string().min(1).max(20),
country: z.string().min(1).max(128),
state: z.string().min(1).max(128),
city: z.string().min(1).max(128),
zipCode: z.string().min(1).max(21),
address: z.string().min(1),
address2: z.string().optional().nullable(),
orderId: z.number().optional().nullable(),
createdAt: z.coerce.string().optional(),
updatedAt: z.coerce.string().optional(),
});
export type CustomerInfoModel = z.infer<typeof customerInfoModel>;
export const createCustomerInfoPayload = customerInfoModel.omit({
id: true,
createdAt: true,
updatedAt: true,
});
export type CreateCustomerInfoPayload = z.infer<
typeof createCustomerInfoPayload
>;
export const updateCustomerInfoPayload = customerInfoModel
.omit({
createdAt: true,
updatedAt: true,
})
.partial()
.extend({
id: z.number(),
});
export type UpdateCustomerInfoPayload = z.infer<
typeof updateCustomerInfoPayload
>;

View File

@@ -0,0 +1,300 @@
import { desc, eq, type Database } from "@pkg/db";
import { customerInfo } from "@pkg/db/schema";
import { getError, Logger } from "@pkg/logger";
import { ERROR_CODES, type Result } from "@pkg/result";
import {
customerInfoModel,
type CreateCustomerInfoPayload,
type CustomerInfoModel,
type UpdateCustomerInfoPayload,
} from "./data";
export class CustomerInfoRepository {
private db: Database;
constructor(db: Database) {
this.db = db;
}
async getAllCustomerInfo(): Promise<Result<CustomerInfoModel[]>> {
try {
const results = await this.db.query.customerInfo.findMany({
orderBy: [desc(customerInfo.createdAt)],
});
const out = [] as CustomerInfoModel[];
for (const result of results) {
const parsed = customerInfoModel.safeParse(result);
if (!parsed.success) {
Logger.error("Failed to parse customer info");
Logger.error(parsed.error);
continue;
}
out.push(parsed.data);
}
return { data: out };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to fetch customer information",
detail:
"An error occurred while retrieving customer information from the database",
userHint: "Please try refreshing the page",
actionable: false,
},
e,
),
};
}
}
async getCustomerInfoById(id: number): Promise<Result<CustomerInfoModel>> {
try {
const result = await this.db.query.customerInfo.findFirst({
where: eq(customerInfo.id, id),
});
if (!result) {
return {
error: getError({
code: ERROR_CODES.NOT_FOUND_ERROR,
message: "Customer information not found",
detail: "No customer information exists with the provided ID",
userHint: "Please check the customer ID and try again",
actionable: true,
}),
};
}
const parsed = customerInfoModel.safeParse(result);
if (!parsed.success) {
Logger.error("Failed to parse customer info", result);
return {
error: getError({
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
message: "Failed to parse customer information",
userHint: "Please try again",
detail: "Failed to parse customer information",
}),
};
}
return { data: parsed.data };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to fetch customer information",
detail:
"An error occurred while retrieving the customer information from the database",
userHint: "Please try refreshing the page",
actionable: false,
},
e,
),
};
}
}
async getCustomerInfoByOrderId(
orderId: number,
): Promise<Result<CustomerInfoModel[]>> {
try {
const results = await this.db.query.customerInfo.findMany({
where: eq(customerInfo.orderId, orderId),
});
const out = [] as CustomerInfoModel[];
for (const result of results) {
const parsed = customerInfoModel.safeParse(result);
if (!parsed.success) {
Logger.error("Failed to parse customer info", result);
continue;
}
out.push(parsed.data);
}
return { data: out };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to fetch customer information",
detail:
"An error occurred while retrieving customer information for the order",
userHint: "Please try again",
actionable: false,
},
e,
),
};
}
}
async createCustomerInfo(
payload: CreateCustomerInfoPayload,
): Promise<Result<number>> {
try {
const result = await this.db
.insert(customerInfo)
.values({
firstName: payload.firstName,
middleName: payload.middleName || "",
lastName: payload.lastName,
email: payload.email,
phoneCountryCode: payload.phoneCountryCode,
phoneNumber: payload.phoneNumber,
country: payload.country,
state: payload.state,
city: payload.city,
zipCode: payload.zipCode,
address: payload.address,
address2: payload.address2 || null,
orderId: payload.orderId || null,
})
.returning({ id: customerInfo.id })
.execute();
if (!result || result.length === 0) {
throw new Error("Failed to create customer info record");
}
return { data: result[0].id };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to create customer information",
detail: "An error occurred while creating the customer information",
userHint: "Please try again",
actionable: false,
},
e,
),
};
}
}
async updateCustomerInfo(
payload: UpdateCustomerInfoPayload,
): Promise<Result<boolean>> {
try {
if (!payload.id) {
return {
error: getError({
code: ERROR_CODES.VALIDATION_ERROR,
message: "Invalid customer ID",
detail: "No customer ID was provided for the update operation",
userHint: "Please provide a valid customer ID",
actionable: true,
}),
};
}
// Check if customer info exists
const existing = await this.db.query.customerInfo.findFirst({
where: eq(customerInfo.id, payload.id),
});
if (!existing) {
return {
error: getError({
code: ERROR_CODES.NOT_FOUND_ERROR,
message: "Customer information not found",
detail: "No customer information exists with the provided ID",
userHint: "Please check the customer ID and try again",
actionable: true,
}),
};
}
// Build the update object with only the fields that are provided
const updateValues: Record<string, any> = {};
if (payload.firstName !== undefined)
updateValues.firstName = payload.firstName;
if (payload.middleName !== undefined)
updateValues.middleName = payload.middleName;
if (payload.lastName !== undefined)
updateValues.lastName = payload.lastName;
if (payload.email !== undefined) updateValues.email = payload.email;
if (payload.phoneCountryCode !== undefined)
updateValues.phoneCountryCode = payload.phoneCountryCode;
if (payload.phoneNumber !== undefined)
updateValues.phoneNumber = payload.phoneNumber;
if (payload.country !== undefined) updateValues.country = payload.country;
if (payload.state !== undefined) updateValues.state = payload.state;
if (payload.city !== undefined) updateValues.city = payload.city;
if (payload.zipCode !== undefined) updateValues.zipCode = payload.zipCode;
if (payload.address !== undefined) updateValues.address = payload.address;
if (payload.address2 !== undefined)
updateValues.address2 = payload.address2;
if (payload.orderId !== undefined) updateValues.orderId = payload.orderId;
updateValues.updatedAt = new Date();
await this.db
.update(customerInfo)
.set(updateValues)
.where(eq(customerInfo.id, payload.id))
.execute();
return { data: true };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to update customer information",
detail: "An error occurred while updating the customer information",
userHint: "Please try again",
actionable: false,
},
e,
),
};
}
}
async deleteCustomerInfo(id: number): Promise<Result<boolean>> {
try {
// Check if customer info exists
const existing = await this.db.query.customerInfo.findFirst({
where: eq(customerInfo.id, id),
});
if (!existing) {
return {
error: getError({
code: ERROR_CODES.NOT_FOUND_ERROR,
message: "Customer information not found",
detail: "No customer information exists with the provided ID",
userHint: "Please check the customer ID and try again",
actionable: true,
}),
};
}
await this.db
.delete(customerInfo)
.where(eq(customerInfo.id, id))
.execute();
return { data: true };
} catch (e) {
return {
error: getError(
{
code: ERROR_CODES.DATABASE_ERROR,
message: "Failed to delete customer information",
detail: "An error occurred while deleting the customer information",
userHint: "Please try again",
actionable: false,
},
e,
),
};
}
}
}

View File

@@ -0,0 +1,42 @@
import { db } from "@pkg/db";
import type {
CreateCustomerInfoPayload,
UpdateCustomerInfoPayload,
} from "./data";
import { CustomerInfoRepository } from "./repository";
export class CustomerInfoUseCases {
private repo: CustomerInfoRepository;
constructor(repo: CustomerInfoRepository) {
this.repo = repo;
}
async getAllCustomerInfo() {
return this.repo.getAllCustomerInfo();
}
async getCustomerInfoById(id: number) {
return this.repo.getCustomerInfoById(id);
}
async getCustomerInfoByOrderId(orderId: number) {
return this.repo.getCustomerInfoByOrderId(orderId);
}
async createCustomerInfo(payload: CreateCustomerInfoPayload) {
return this.repo.createCustomerInfo(payload);
}
async updateCustomerInfo(payload: UpdateCustomerInfoPayload) {
return this.repo.updateCustomerInfo(payload);
}
async deleteCustomerInfo(id: number) {
return this.repo.deleteCustomerInfo(id);
}
}
export function getCustomerInfoUseCases() {
return new CustomerInfoUseCases(new CustomerInfoRepository(db));
}

View File

@@ -1,18 +1,14 @@
import { z } from "zod";
import { paginationModel } from "../../../core/pagination.utils";
import { encodeCursor } from "../../../core/string.utils";
import {
emailAccountModel,
emailAccountPayloadModel,
} from "../../account/data/entities";
import { flightTicketModel } from "../../ticket/data/entities";
import { passengerInfoModel } from "../../passengerinfo/data/entities";
import { z } from "zod";
import { customerInfoModel } from "../../customerinfo/data";
import { paymentDetailsPayloadModel } from "../../paymentinfo/data/entities";
import { flightTicketModel } from "../../ticket/data/entities";
export enum OrderCreationStep {
ACCOUNT_SELECTION = 0,
TICKET_SELECTION = 1,
PASSENGER_INFO = 2,
CUSTOMER_INFO = 2,
SUMMARY = 3,
}
@@ -31,7 +27,7 @@ export const orderModel = z.object({
displayPrice: z.coerce.number().min(0),
orderPrice: z.coerce.number().min(0),
fullfilledPrice: z.coerce.number().min(0),
pricePerPassenger: z.coerce.number().min(0),
pricePerCustomer: z.coerce.number().min(0),
status: z.nativeEnum(OrderStatus),
@@ -51,7 +47,7 @@ export const limitedOrderWithTicketInfoModel = orderModel
basePrice: true,
discountAmount: true,
displayPrice: true,
pricePerPassenger: true,
pricePerCustomer: true,
fullfilledPrice: true,
status: true,
})
@@ -75,8 +71,7 @@ export type LimitedOrderWithTicketInfoModel = z.infer<
export const fullOrderModel = orderModel.merge(
z.object({
flightTicketInfo: flightTicketModel,
emailAccount: emailAccountModel.nullable().optional(),
passengerInfos: z.array(passengerInfoModel).default([]),
customerInfos: z.array(customerInfoModel).default([]),
}),
);
export type FullOrderModel = z.infer<typeof fullOrderModel>;
@@ -113,7 +108,7 @@ export const newOrderModel = orderModel.pick({
discountAmount: true,
orderPrice: true,
fullfilledPrice: true,
pricePerPassenger: true,
pricePerCustomer: true,
flightTicketInfoId: true,
paymentDetailsId: true,
emailAccountId: true,
@@ -126,8 +121,7 @@ export const createOrderPayloadModel = z.object({
refOIds: z.array(z.number()).nullable().optional(),
paymentDetails: paymentDetailsPayloadModel.optional(),
orderModel: newOrderModel,
emailAccountInfo: emailAccountPayloadModel.optional(),
passengerInfos: z.array(passengerInfoModel),
customerInfos: z.array(customerInfoModel),
flowId: z.string().optional(),
});
export type CreateOrderModel = z.infer<typeof createOrderPayloadModel>;

View File

@@ -1,8 +1,4 @@
import { z } from "zod";
import {
flightPriceDetailsModel,
allBagDetailsModel,
} from "../../ticket/data/entities";
import { paymentDetailsModel } from "../../paymentinfo/data/entities";
export enum Gender {
@@ -16,7 +12,7 @@ export enum PassengerType {
Child = "child",
}
export const passengerPIIModel = z.object({
export const customerInfoModel = z.object({
firstName: z.string().min(1).max(255),
middleName: z.string().min(0).max(255),
lastName: z.string().min(1).max(255),
@@ -43,43 +39,17 @@ export const passengerPIIModel = z.object({
address: z.string().min(1).max(128),
address2: z.string().min(0).max(128),
});
export type PassengerPII = z.infer<typeof passengerPIIModel>;
export const seatSelectionInfoModel = z.object({
id: z.string(),
row: z.string(),
number: z.number(),
seatLetter: z.string(),
available: z.boolean(),
reserved: z.boolean(),
price: flightPriceDetailsModel,
});
export type SeatSelectionInfo = z.infer<typeof seatSelectionInfoModel>;
export const flightSeatMapModel = z.object({
flightId: z.string(),
seats: z.array(z.array(seatSelectionInfoModel)),
});
export type FlightSeatMap = z.infer<typeof flightSeatMapModel>;
export const bagSelectionInfoModel = z.object({
id: z.number(),
personalBags: z.number().default(1),
handBags: z.number().default(0),
checkedBags: z.number().default(0),
pricing: allBagDetailsModel,
});
export type BagSelectionInfo = z.infer<typeof bagSelectionInfoModel>;
export type CustomerInfo = z.infer<typeof customerInfoModel>;
export const passengerInfoModel = z.object({
id: z.number(),
passengerType: z.enum([PassengerType.Adult, PassengerType.Child]),
passengerPii: passengerPIIModel,
passengerPii: customerInfoModel,
paymentDetails: paymentDetailsModel.optional(),
passengerPiiId: z.number().optional(),
paymentDetailsId: z.number().optional(),
seatSelection: seatSelectionInfoModel,
bagSelection: bagSelectionInfoModel,
seatSelection: z.any(),
bagSelection: z.any(),
agentsInfo: z.boolean().default(false).optional(),
agentId: z.coerce.string().optional(),