big boi refactor to customer inof from passenger info
This commit is contained in:
@@ -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;
|
||||
});
|
||||
@@ -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<
|
||||
|
||||
46
packages/logic/domains/customerinfo/data.ts
Normal file
46
packages/logic/domains/customerinfo/data.ts
Normal 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
|
||||
>;
|
||||
300
packages/logic/domains/customerinfo/repository.ts
Normal file
300
packages/logic/domains/customerinfo/repository.ts
Normal 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,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
42
packages/logic/domains/customerinfo/usecases.ts
Normal file
42
packages/logic/domains/customerinfo/usecases.ts
Normal 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));
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user