stashing code
This commit is contained in:
52
packages/logic/domains/account/data/entities.ts
Normal file
52
packages/logic/domains/account/data/entities.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const emailAccountPayloadModel = z.object({
|
||||
email: z.string().email().min(6).max(128),
|
||||
password: z.string().max(128),
|
||||
agentId: z.string().optional(),
|
||||
orderId: z.number().int().nullish().optional(),
|
||||
});
|
||||
export type EmailAccountPayload = z.infer<typeof emailAccountPayloadModel>;
|
||||
|
||||
export const emailAccountModel = emailAccountPayloadModel
|
||||
.pick({ email: true, agentId: true, orderId: true })
|
||||
.merge(
|
||||
z.object({
|
||||
id: z.number().int(),
|
||||
used: z.boolean().default(false),
|
||||
lastActiveCheckAt: z.coerce.string().optional(),
|
||||
createdAt: z.coerce.string(),
|
||||
updatedAt: z.coerce.string(),
|
||||
}),
|
||||
);
|
||||
export type EmailAccount = z.infer<typeof emailAccountModel>;
|
||||
|
||||
export const emailAccountFullModel = emailAccountPayloadModel.merge(
|
||||
z.object({
|
||||
id: z.number().int(),
|
||||
used: z.boolean().default(false),
|
||||
createdAt: z.coerce.string(),
|
||||
updatedAt: z.coerce.string(),
|
||||
}),
|
||||
);
|
||||
export type EmailAccountFull = z.infer<typeof emailAccountFullModel>;
|
||||
|
||||
export const inboxModel = z.object({
|
||||
id: z.number(),
|
||||
emailId: z.string().default(""),
|
||||
|
||||
from: z.string(),
|
||||
to: z.string().optional(),
|
||||
cc: z.string().optional(),
|
||||
subject: z.string(),
|
||||
body: z.string(),
|
||||
|
||||
attachments: z.any().optional(),
|
||||
emailAccountId: z.number(),
|
||||
|
||||
dated: z.coerce.string(),
|
||||
|
||||
createdAt: z.coerce.string(),
|
||||
updatedAt: z.coerce.string(),
|
||||
});
|
||||
export type InboxModel = z.infer<typeof inboxModel>;
|
||||
34
packages/logic/domains/airport/data/entities.ts
Normal file
34
packages/logic/domains/airport/data/entities.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
});
|
||||
|
||||
export const airportModel = z.object({
|
||||
id: z.coerce.number().int(),
|
||||
ident: z.string().nullable().optional(),
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
latitudeDeg: numberModel.default(0.0),
|
||||
longitudeDeg: numberModel.default(0.0),
|
||||
elevationFt: numberModel.default(0.0),
|
||||
continent: z.string(),
|
||||
isoCountry: z.string(),
|
||||
country: z.string(),
|
||||
isoRegion: z.string(),
|
||||
municipality: z.string(),
|
||||
scheduledService: z.string(),
|
||||
gpsCode: z.coerce.string().default("----"),
|
||||
iataCode: z.coerce.string().min(1),
|
||||
localCode: z.coerce.string().default("----"),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
export type Airport = z.infer<typeof airportModel>;
|
||||
21
packages/logic/domains/auth/data/entities.ts
Normal file
21
packages/logic/domains/auth/data/entities.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { authClient } from "../config/client";
|
||||
import { z } from "zod";
|
||||
|
||||
export const passwordModel = z.string().min(6).max(128);
|
||||
|
||||
export const authPayloadModel = z.object({
|
||||
username: z.string().min(4).max(128),
|
||||
password: passwordModel,
|
||||
});
|
||||
|
||||
export type AuthPayloadModel = z.infer<typeof authPayloadModel>;
|
||||
export type Session = typeof authClient.$Infer.Session;
|
||||
|
||||
export const changePasswordPayloadModel = z.object({
|
||||
oldPassword: passwordModel,
|
||||
newPassword: passwordModel,
|
||||
});
|
||||
|
||||
export type ChangePasswordPayloadModel = z.infer<
|
||||
typeof changePasswordPayloadModel
|
||||
>;
|
||||
11
packages/logic/domains/auth/session/data/entities.ts
Normal file
11
packages/logic/domains/auth/session/data/entities.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const sessionModel = z.object({
|
||||
id: z.string(),
|
||||
userId: z.coerce.number().int(),
|
||||
userAgent: z.string(),
|
||||
ipAddress: z.string(),
|
||||
expiresAt: z.coerce.date(),
|
||||
});
|
||||
export type SessionModel = z.infer<typeof sessionModel>;
|
||||
export type Session = SessionModel & { id: string };
|
||||
158
packages/logic/domains/ckflow/data/entities.ts
Normal file
158
packages/logic/domains/ckflow/data/entities.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
PassengerPII,
|
||||
passengerPIIModel,
|
||||
} from "../../passengerinfo/data/entities";
|
||||
import {
|
||||
PaymentDetailsPayload,
|
||||
paymentDetailsPayloadModel,
|
||||
} from "../../paymentinfo/data/entities";
|
||||
import { CheckoutStep } from "../../ticket/data/entities";
|
||||
|
||||
// Define action types for the checkout flow
|
||||
export enum CKActionType {
|
||||
PutInVerification = "PUT_IN_VERIFICATION",
|
||||
ShowVerificationScreen = "SHOW_VERIFICATION_SCREEN",
|
||||
RequestOTP = "REQUEST_OTP",
|
||||
CompleteOrder = "COMPLETE_ORDER",
|
||||
BackToPII = "BACK_TO_PII",
|
||||
BackToPayment = "BACK_TO_PAYMENT",
|
||||
TerminateSession = "TERMINATE_SESSION",
|
||||
}
|
||||
|
||||
export enum SessionOutcome {
|
||||
PENDING = "PENDING",
|
||||
COMPLETED = "COMPLETED",
|
||||
ABANDONED = "ABANDONED",
|
||||
TERMINATED = "TERMINATED",
|
||||
EXPIRED = "EXPIRED",
|
||||
PAYMENT_FAILED = "PAYMENT_FAILED",
|
||||
PAYMENT_SUCCESSFUL = "PAYMENT_SUCCESSFUL",
|
||||
VERIFICATION_FAILED = "VERIFICATION_FAILED",
|
||||
SYSTEM_ERROR = "SYSTEM_ERROR",
|
||||
}
|
||||
|
||||
export enum PaymentErrorType {
|
||||
DO_NOT_HONOR = "DO_NOT_HONOR",
|
||||
REFER_TO_ISSUER = "REFER_TO_ISSUER",
|
||||
TRANSACTION_DENIED = "TRANSACTION_DENIED",
|
||||
CAPTURE_CARD_ERROR = "CAPTURE_CARD_ERROR",
|
||||
CUSTOM_ERROR = "CUSTOM_ERROR",
|
||||
}
|
||||
|
||||
export interface PaymentErrorMessage {
|
||||
type: PaymentErrorType;
|
||||
message: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// Model for pending actions in the flow
|
||||
export const pendingActionModel = z.object({
|
||||
id: z.string(),
|
||||
type: z.nativeEnum(CKActionType),
|
||||
data: z.record(z.string(), z.any()).default({}),
|
||||
});
|
||||
export type PendingAction = z.infer<typeof pendingActionModel>;
|
||||
|
||||
export const pendingActionsModel = z.array(pendingActionModel);
|
||||
export type PendingActions = z.infer<typeof pendingActionsModel>;
|
||||
|
||||
export const ticketSummaryModel = z.object({
|
||||
id: z.number().optional(),
|
||||
ticketId: z.string().optional(),
|
||||
departure: z.string(),
|
||||
arrival: z.string(),
|
||||
departureDate: z.string(),
|
||||
returnDate: z.string().optional(),
|
||||
flightType: z.string(),
|
||||
cabinClass: z.string(),
|
||||
priceDetails: z.object({
|
||||
currency: z.string(),
|
||||
displayPrice: z.number(),
|
||||
basePrice: z.number().optional(),
|
||||
discountAmount: z.number().optional(),
|
||||
}),
|
||||
});
|
||||
export type TicketSummary = z.infer<typeof ticketSummaryModel>;
|
||||
|
||||
// Core flow information model - what's actually stored in Redis
|
||||
export const flowInfoModel = z.object({
|
||||
id: z.coerce.number().optional(),
|
||||
flowId: z.string(),
|
||||
domain: z.string(),
|
||||
checkoutStep: z.nativeEnum(CheckoutStep),
|
||||
showVerification: z.boolean().default(false),
|
||||
createdAt: z.string().datetime(),
|
||||
lastPinged: z.string().datetime(),
|
||||
isActive: z.boolean().default(true),
|
||||
lastSyncedAt: z.string().datetime(),
|
||||
|
||||
ticketInfo: ticketSummaryModel.optional(),
|
||||
ticketId: z.number().nullable().optional(),
|
||||
|
||||
personalInfoLastSyncedAt: z.string().datetime().optional(),
|
||||
paymentInfoLastSyncedAt: z.string().datetime().optional(),
|
||||
|
||||
pendingActions: pendingActionsModel.default([]),
|
||||
personalInfo: z.custom<PassengerPII>().optional(),
|
||||
paymentInfo: z.custom<PaymentDetailsPayload>().optional(),
|
||||
refOids: z.array(z.number()).optional(),
|
||||
|
||||
otpCode: z.coerce.string().optional(),
|
||||
otpSubmitted: z.boolean().default(false),
|
||||
partialOtpCode: z.coerce.string().optional(),
|
||||
|
||||
ipAddress: z.string().default(""),
|
||||
userAgent: z.string().default(""),
|
||||
reserved: z.boolean().default(false),
|
||||
reservedBy: z.string().nullable().optional(),
|
||||
|
||||
liveCheckoutStartedAt: z.string().datetime().optional(),
|
||||
|
||||
completedAt: z.coerce.string().datetime().nullable().optional(),
|
||||
sessionOutcome: z.string(),
|
||||
isDeleted: z.boolean().default(false),
|
||||
});
|
||||
export type FlowInfo = z.infer<typeof flowInfoModel>;
|
||||
|
||||
// Payload for frontend to create a checkout flow
|
||||
export const feCreateCheckoutFlowPayloadModel = z.object({
|
||||
domain: z.string(),
|
||||
refOIds: z.array(z.number()),
|
||||
ticketId: z.number().optional(),
|
||||
});
|
||||
export type FECreateCheckoutFlowPayload = z.infer<
|
||||
typeof feCreateCheckoutFlowPayloadModel
|
||||
>;
|
||||
|
||||
// Complete payload for backend to create a checkout flow
|
||||
export const createCheckoutFlowPayloadModel = z.object({
|
||||
flowId: z.string(),
|
||||
domain: z.string(),
|
||||
refOIds: z.array(z.number()),
|
||||
ticketId: z.number().optional(),
|
||||
ipAddress: z.string().default(""),
|
||||
userAgent: z.string().default(""),
|
||||
initialUrl: z.string().default(""),
|
||||
provider: z.string().default("kiwi"),
|
||||
});
|
||||
export type CreateCheckoutFlowPayload = z.infer<
|
||||
typeof createCheckoutFlowPayloadModel
|
||||
>;
|
||||
|
||||
// Step-specific payloads
|
||||
export const prePaymentFlowStepPayloadModel = z.object({
|
||||
initialUrl: z.string(),
|
||||
personalInfo: passengerPIIModel.optional(),
|
||||
});
|
||||
export type PrePaymentFlowStepPayload = z.infer<
|
||||
typeof prePaymentFlowStepPayloadModel
|
||||
>;
|
||||
|
||||
export const paymentFlowStepPayloadModel = z.object({
|
||||
personalInfo: passengerPIIModel.optional(),
|
||||
paymentInfo: paymentDetailsPayloadModel.optional(),
|
||||
});
|
||||
export type PaymentFlowStepPayload = z.infer<
|
||||
typeof paymentFlowStepPayloadModel
|
||||
>;
|
||||
41
packages/logic/domains/coupon/data.ts
Normal file
41
packages/logic/domains/coupon/data.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export enum DiscountType {
|
||||
PERCENTAGE = "PERCENTAGE",
|
||||
FIXED = "FIXED",
|
||||
}
|
||||
|
||||
export const couponModel = z.object({
|
||||
id: z.number().optional(),
|
||||
code: z.string().min(3).max(32),
|
||||
description: z.string().optional().nullable(),
|
||||
discountType: z.nativeEnum(DiscountType),
|
||||
discountValue: z.coerce.number().positive(),
|
||||
maxUsageCount: z.coerce.number().int().positive().optional().nullable(),
|
||||
currentUsageCount: z.coerce.number().int().nonnegative().default(0),
|
||||
minOrderValue: z.coerce.number().nonnegative().optional().nullable(),
|
||||
maxDiscountAmount: z.coerce.number().positive().optional().nullable(),
|
||||
startDate: z.coerce.string(),
|
||||
endDate: z.coerce.string().optional().nullable(),
|
||||
isActive: z.boolean().default(true),
|
||||
createdAt: z.coerce.string().optional(),
|
||||
updatedAt: z.coerce.string().optional(),
|
||||
createdBy: z.coerce.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export type CouponModel = z.infer<typeof couponModel>;
|
||||
|
||||
export const createCouponPayload = couponModel.omit({
|
||||
id: true,
|
||||
currentUsageCount: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
|
||||
export type CreateCouponPayload = z.infer<typeof createCouponPayload>;
|
||||
|
||||
export const updateCouponPayload = createCouponPayload.partial().extend({
|
||||
id: z.number(),
|
||||
});
|
||||
|
||||
export type UpdateCouponPayload = z.infer<typeof updateCouponPayload>;
|
||||
491
packages/logic/domains/coupon/repository.ts
Normal file
491
packages/logic/domains/coupon/repository.ts
Normal file
@@ -0,0 +1,491 @@
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
desc,
|
||||
eq,
|
||||
gte,
|
||||
isNull,
|
||||
lte,
|
||||
or,
|
||||
type Database,
|
||||
} from "@pkg/db";
|
||||
import { coupon } from "@pkg/db/schema";
|
||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
||||
import { getError, Logger } from "@pkg/logger";
|
||||
import {
|
||||
couponModel,
|
||||
type CouponModel,
|
||||
type CreateCouponPayload,
|
||||
type UpdateCouponPayload,
|
||||
} from "./data";
|
||||
|
||||
export class CouponRepository {
|
||||
private db: Database;
|
||||
|
||||
constructor(db: Database) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async getAllCoupons(): Promise<Result<CouponModel[]>> {
|
||||
try {
|
||||
const results = await this.db.query.coupon.findMany({
|
||||
orderBy: [desc(coupon.createdAt)],
|
||||
});
|
||||
const out = [] as CouponModel[];
|
||||
for (const result of results) {
|
||||
const parsed = couponModel.safeParse(result);
|
||||
if (!parsed.success) {
|
||||
Logger.error("Failed to parse coupon");
|
||||
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 coupons",
|
||||
detail:
|
||||
"An error occurred while retrieving coupons from the database",
|
||||
userHint: "Please try refreshing the page",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getCouponById(id: number): Promise<Result<CouponModel>> {
|
||||
try {
|
||||
const result = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.id, id),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||
message: "Coupon not found",
|
||||
detail: "No coupon exists with the provided ID",
|
||||
userHint: "Please check the coupon ID and try again",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const parsed = couponModel.safeParse(result);
|
||||
if (!parsed.success) {
|
||||
Logger.error("Failed to parse coupon", result);
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to parse coupon",
|
||||
userHint: "Please try again",
|
||||
detail: "Failed to parse coupon",
|
||||
}),
|
||||
};
|
||||
}
|
||||
return { data: parsed.data };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to fetch coupon",
|
||||
detail:
|
||||
"An error occurred while retrieving the coupon from the database",
|
||||
userHint: "Please try refreshing the page",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async createCoupon(payload: CreateCouponPayload): Promise<Result<number>> {
|
||||
try {
|
||||
// Check if coupon code already exists
|
||||
const existing = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.code, payload.code),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Coupon code already exists",
|
||||
detail: "A coupon with this code already exists in the system",
|
||||
userHint: "Please use a different coupon code",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const result = await this.db
|
||||
.insert(coupon)
|
||||
.values({
|
||||
code: payload.code,
|
||||
description: payload.description || null,
|
||||
discountType: payload.discountType,
|
||||
discountValue: payload.discountValue.toString(),
|
||||
maxUsageCount: payload.maxUsageCount,
|
||||
minOrderValue: payload.minOrderValue
|
||||
? payload.minOrderValue.toString()
|
||||
: null,
|
||||
maxDiscountAmount: payload.maxDiscountAmount
|
||||
? payload.maxDiscountAmount.toString()
|
||||
: null,
|
||||
startDate: new Date(payload.startDate),
|
||||
endDate: payload.endDate ? new Date(payload.endDate) : null,
|
||||
isActive: payload.isActive,
|
||||
createdBy: payload.createdBy || null,
|
||||
})
|
||||
.returning({ id: coupon.id })
|
||||
.execute();
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
throw new Error("Failed to create coupon record");
|
||||
}
|
||||
|
||||
return { data: result[0].id };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to create coupon",
|
||||
detail: "An error occurred while creating the coupon",
|
||||
userHint: "Please try again",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async updateCoupon(payload: UpdateCouponPayload): Promise<Result<boolean>> {
|
||||
try {
|
||||
if (!payload.id) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.VALIDATION_ERROR,
|
||||
message: "Invalid coupon ID",
|
||||
detail: "No coupon ID was provided for the update operation",
|
||||
userHint: "Please provide a valid coupon ID",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Check if coupon exists
|
||||
const existing = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.id, payload.id),
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||
message: "Coupon not found",
|
||||
detail: "No coupon exists with the provided ID",
|
||||
userHint: "Please check the coupon ID and try again",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// If changing the code, check if the new code already exists
|
||||
if (payload.code && payload.code !== existing.code) {
|
||||
const codeExists = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.code, payload.code),
|
||||
});
|
||||
|
||||
if (codeExists) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Coupon code already exists",
|
||||
detail: "A coupon with this code already exists in the system",
|
||||
userHint: "Please use a different coupon code",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Build the update object with only the fields that are provided
|
||||
const updateValues: Record<string, any> = {};
|
||||
|
||||
if (payload.code !== undefined) updateValues.code = payload.code;
|
||||
if (payload.description !== undefined)
|
||||
updateValues.description = payload.description;
|
||||
if (payload.discountType !== undefined)
|
||||
updateValues.discountType = payload.discountType;
|
||||
if (payload.discountValue !== undefined)
|
||||
updateValues.discountValue = payload.discountValue.toString();
|
||||
if (payload.maxUsageCount !== undefined)
|
||||
updateValues.maxUsageCount = payload.maxUsageCount;
|
||||
if (payload.minOrderValue !== undefined)
|
||||
updateValues.minOrderValue = payload.minOrderValue?.toString() || null;
|
||||
if (payload.maxDiscountAmount !== undefined)
|
||||
updateValues.maxDiscountAmount =
|
||||
payload.maxDiscountAmount?.toString() || null;
|
||||
if (payload.startDate !== undefined)
|
||||
updateValues.startDate = new Date(payload.startDate);
|
||||
if (payload.endDate !== undefined)
|
||||
updateValues.endDate = payload.endDate
|
||||
? new Date(payload.endDate)
|
||||
: null;
|
||||
if (payload.isActive !== undefined)
|
||||
updateValues.isActive = payload.isActive;
|
||||
updateValues.updatedAt = new Date();
|
||||
|
||||
await this.db
|
||||
.update(coupon)
|
||||
.set(updateValues)
|
||||
.where(eq(coupon.id, payload.id))
|
||||
.execute();
|
||||
|
||||
return { data: true };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to update coupon",
|
||||
detail: "An error occurred while updating the coupon",
|
||||
userHint: "Please try again",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCoupon(id: number): Promise<Result<boolean>> {
|
||||
try {
|
||||
// Check if coupon exists
|
||||
const existing = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.id, id),
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||
message: "Coupon not found",
|
||||
detail: "No coupon exists with the provided ID",
|
||||
userHint: "Please check the coupon ID and try again",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
await this.db.delete(coupon).where(eq(coupon.id, id)).execute();
|
||||
|
||||
return { data: true };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to delete coupon",
|
||||
detail: "An error occurred while deleting the coupon",
|
||||
userHint: "Please try again",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async toggleCouponStatus(
|
||||
id: number,
|
||||
isActive: boolean,
|
||||
): Promise<Result<boolean>> {
|
||||
try {
|
||||
// Check if coupon exists
|
||||
const existing = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.id, id),
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||
message: "Coupon not found",
|
||||
detail: "No coupon exists with the provided ID",
|
||||
userHint: "Please check the coupon ID and try again",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
await this.db
|
||||
.update(coupon)
|
||||
.set({
|
||||
isActive,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(coupon.id, id))
|
||||
.execute();
|
||||
|
||||
return { data: true };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to update coupon status",
|
||||
detail: "An error occurred while updating the coupon's status",
|
||||
userHint: "Please try again",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getCouponByCode(code: string): Promise<Result<CouponModel>> {
|
||||
try {
|
||||
const result = await this.db.query.coupon.findFirst({
|
||||
where: eq(coupon.code, code),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.NOT_FOUND_ERROR,
|
||||
message: "Coupon not found",
|
||||
detail: "No coupon exists with the provided code",
|
||||
userHint: "Please check the coupon code and try again",
|
||||
actionable: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = couponModel.safeParse(result);
|
||||
if (!parsed.success) {
|
||||
Logger.error("Failed to parse coupon", result);
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to parse coupon",
|
||||
userHint: "Please try again",
|
||||
detail: "Failed to parse coupon",
|
||||
}),
|
||||
};
|
||||
}
|
||||
return { data: parsed.data };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to fetch coupon",
|
||||
detail:
|
||||
"An error occurred while retrieving the coupon from the database",
|
||||
userHint: "Please try again",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveCoupons(): Promise<Result<CouponModel[]>> {
|
||||
try {
|
||||
const now = new Date();
|
||||
|
||||
const results = await this.db.query.coupon.findMany({
|
||||
where: and(
|
||||
eq(coupon.isActive, true),
|
||||
lte(coupon.startDate, now),
|
||||
// Either endDate is null (no end date) or it's greater than now
|
||||
or(isNull(coupon.endDate), gte(coupon.endDate, now)),
|
||||
),
|
||||
orderBy: [asc(coupon.code)],
|
||||
});
|
||||
const out = [] as CouponModel[];
|
||||
for (const result of results) {
|
||||
const parsed = couponModel.safeParse(result);
|
||||
if (!parsed.success) {
|
||||
Logger.error("Failed to parse coupon", result);
|
||||
continue;
|
||||
}
|
||||
out.push(parsed.data);
|
||||
}
|
||||
return { data: out };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
message: "Failed to fetch active coupons",
|
||||
detail:
|
||||
"An error occurred while retrieving active coupons from the database",
|
||||
userHint: "Please try refreshing the page",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getBestActiveCoupon(): Promise<Result<CouponModel>> {
|
||||
try {
|
||||
const now = new Date();
|
||||
|
||||
// Fetch all active coupons that are currently valid
|
||||
const activeCoupons = await this.db.query.coupon.findMany({
|
||||
where: and(
|
||||
eq(coupon.isActive, true),
|
||||
lte(coupon.startDate, now),
|
||||
// Either endDate is null (no end date) or it's greater than now
|
||||
or(isNull(coupon.endDate), gte(coupon.endDate, now)),
|
||||
),
|
||||
orderBy: [
|
||||
// Order by discount type (PERCENTAGE first) and then by discount value (descending)
|
||||
asc(coupon.discountType),
|
||||
desc(coupon.discountValue),
|
||||
],
|
||||
});
|
||||
|
||||
if (!activeCoupons || activeCoupons.length === 0) {
|
||||
return {}; // No active coupons found
|
||||
}
|
||||
|
||||
// Get the first (best) coupon
|
||||
const bestCoupon = activeCoupons[0];
|
||||
|
||||
// Check if max usage limit is reached
|
||||
if (
|
||||
bestCoupon.maxUsageCount !== null &&
|
||||
bestCoupon.currentUsageCount >= bestCoupon.maxUsageCount
|
||||
) {
|
||||
return {}; // Coupon usage limit reached
|
||||
}
|
||||
|
||||
const parsed = couponModel.safeParse(bestCoupon);
|
||||
if (!parsed.success) {
|
||||
Logger.error("Failed to parse coupon", bestCoupon);
|
||||
return {}; // Return null on error, don't break ticket search
|
||||
}
|
||||
|
||||
return { data: parsed.data };
|
||||
} catch (e) {
|
||||
Logger.error("Error fetching active coupons", e);
|
||||
return {}; // Return null on error, don't break ticket search
|
||||
}
|
||||
}
|
||||
}
|
||||
49
packages/logic/domains/coupon/usecases.ts
Normal file
49
packages/logic/domains/coupon/usecases.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { db } from "@pkg/db";
|
||||
import { CouponRepository } from "./repository";
|
||||
import type { CreateCouponPayload, UpdateCouponPayload } from "./data";
|
||||
import type { UserModel } from "@pkg/logic/domains/user/data/entities";
|
||||
|
||||
export class CouponUseCases {
|
||||
private repo: CouponRepository;
|
||||
|
||||
constructor(repo: CouponRepository) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
async getAllCoupons() {
|
||||
return this.repo.getAllCoupons();
|
||||
}
|
||||
|
||||
async getCouponById(id: number) {
|
||||
return this.repo.getCouponById(id);
|
||||
}
|
||||
|
||||
async createCoupon(currentUser: UserModel, payload: CreateCouponPayload) {
|
||||
// Set the current user as the creator
|
||||
const payloadWithUser = {
|
||||
...payload,
|
||||
createdBy: currentUser.id,
|
||||
};
|
||||
return this.repo.createCoupon(payloadWithUser);
|
||||
}
|
||||
|
||||
async updateCoupon(payload: UpdateCouponPayload) {
|
||||
return this.repo.updateCoupon(payload);
|
||||
}
|
||||
|
||||
async deleteCoupon(id: number) {
|
||||
return this.repo.deleteCoupon(id);
|
||||
}
|
||||
|
||||
async toggleCouponStatus(id: number, isActive: boolean) {
|
||||
return this.repo.toggleCouponStatus(id, isActive);
|
||||
}
|
||||
|
||||
async getActiveCoupons() {
|
||||
return this.repo.getActiveCoupons();
|
||||
}
|
||||
}
|
||||
|
||||
export function getCouponUseCases() {
|
||||
return new CouponUseCases(new CouponRepository(db));
|
||||
}
|
||||
1295
packages/logic/domains/currency/data/currencies.ts
Normal file
1295
packages/logic/domains/currency/data/currencies.ts
Normal file
File diff suppressed because it is too large
Load Diff
12
packages/logic/domains/currency/data/entities.ts
Normal file
12
packages/logic/domains/currency/data/entities.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import z from "zod";
|
||||
|
||||
// The base currency is always USD, so the ratio is relative to that
|
||||
|
||||
export const currencyModel = z.object({
|
||||
id: z.number(),
|
||||
currency: z.string(),
|
||||
code: z.string(),
|
||||
exchangeRate: z.number(),
|
||||
ratio: z.number(),
|
||||
});
|
||||
export type Currency = z.infer<typeof currencyModel>;
|
||||
133
packages/logic/domains/order/data/entities.ts
Normal file
133
packages/logic/domains/order/data/entities.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
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 { paymentDetailsPayloadModel } from "../../paymentinfo/data/entities";
|
||||
|
||||
export enum OrderCreationStep {
|
||||
ACCOUNT_SELECTION = 0,
|
||||
TICKET_SELECTION = 1,
|
||||
PASSENGER_INFO = 2,
|
||||
SUMMARY = 3,
|
||||
}
|
||||
|
||||
export enum OrderStatus {
|
||||
PENDING_FULLFILLMENT = "PENDING_FULLFILLMENT",
|
||||
PARTIALLY_FULFILLED = "PARTIALLY_FULFILLED",
|
||||
FULFILLED = "FULFILLED",
|
||||
CANCELLED = "CANCELLED",
|
||||
}
|
||||
|
||||
export const orderModel = z.object({
|
||||
id: z.coerce.number().int().positive(),
|
||||
|
||||
discountAmount: z.coerce.number().min(0),
|
||||
basePrice: z.coerce.number().min(0),
|
||||
displayPrice: z.coerce.number().min(0),
|
||||
orderPrice: z.coerce.number().min(0),
|
||||
fullfilledPrice: z.coerce.number().min(0),
|
||||
pricePerPassenger: z.coerce.number().min(0),
|
||||
|
||||
status: z.nativeEnum(OrderStatus),
|
||||
|
||||
flightTicketInfoId: z.number(),
|
||||
emailAccountId: z.number().nullish().optional(),
|
||||
|
||||
paymentDetailsId: z.number().nullish().optional(),
|
||||
|
||||
createdAt: z.coerce.string(),
|
||||
updatedAt: z.coerce.string(),
|
||||
});
|
||||
export type OrderModel = z.infer<typeof orderModel>;
|
||||
|
||||
export const limitedOrderWithTicketInfoModel = orderModel
|
||||
.pick({
|
||||
id: true,
|
||||
basePrice: true,
|
||||
discountAmount: true,
|
||||
displayPrice: true,
|
||||
pricePerPassenger: true,
|
||||
fullfilledPrice: true,
|
||||
status: true,
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
flightTicketInfo: flightTicketModel.pick({
|
||||
id: true,
|
||||
departure: true,
|
||||
arrival: true,
|
||||
departureDate: true,
|
||||
returnDate: true,
|
||||
flightType: true,
|
||||
passengerCounts: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
export type LimitedOrderWithTicketInfoModel = z.infer<
|
||||
typeof limitedOrderWithTicketInfoModel
|
||||
>;
|
||||
|
||||
export const fullOrderModel = orderModel.merge(
|
||||
z.object({
|
||||
flightTicketInfo: flightTicketModel,
|
||||
emailAccount: emailAccountModel.nullable().optional(),
|
||||
passengerInfos: z.array(passengerInfoModel).default([]),
|
||||
}),
|
||||
);
|
||||
export type FullOrderModel = z.infer<typeof fullOrderModel>;
|
||||
|
||||
export const orderCursorModel = z.object({
|
||||
firstItemId: z.number(),
|
||||
lastItemId: z.number(),
|
||||
query: z.string().default(""),
|
||||
});
|
||||
export type OrderCursorModel = z.infer<typeof orderCursorModel>;
|
||||
export function getDefaultOrderCursor() {
|
||||
return orderCursorModel.parse({ firstItemId: 0, lastItemId: 0, query: "" });
|
||||
}
|
||||
|
||||
export const paginatedOrderInfoModel = paginationModel.merge(
|
||||
z.object({ data: z.array(orderModel) }),
|
||||
);
|
||||
export type PaginatedOrderInfoModel = z.infer<typeof paginatedOrderInfoModel>;
|
||||
export function getDefaultPaginatedOrderInfoModel(): PaginatedOrderInfoModel {
|
||||
return {
|
||||
data: [],
|
||||
cursor: encodeCursor<OrderCursorModel>(getDefaultOrderCursor()),
|
||||
limit: 20,
|
||||
asc: true,
|
||||
totalItemCount: 0,
|
||||
totalPages: 0,
|
||||
page: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export const newOrderModel = orderModel.pick({
|
||||
basePrice: true,
|
||||
displayPrice: true,
|
||||
discountAmount: true,
|
||||
orderPrice: true,
|
||||
fullfilledPrice: true,
|
||||
pricePerPassenger: true,
|
||||
flightTicketInfoId: true,
|
||||
paymentDetailsId: true,
|
||||
emailAccountId: true,
|
||||
});
|
||||
export type NewOrderModel = z.infer<typeof newOrderModel>;
|
||||
|
||||
export const createOrderPayloadModel = z.object({
|
||||
flightTicketInfo: flightTicketModel.optional(),
|
||||
flightTicketId: z.number().optional(),
|
||||
refOIds: z.array(z.number()).nullable().optional(),
|
||||
paymentDetails: paymentDetailsPayloadModel.optional(),
|
||||
orderModel: newOrderModel,
|
||||
emailAccountInfo: emailAccountPayloadModel.optional(),
|
||||
passengerInfos: z.array(passengerInfoModel),
|
||||
flowId: z.string().optional(),
|
||||
});
|
||||
export type CreateOrderModel = z.infer<typeof createOrderPayloadModel>;
|
||||
89
packages/logic/domains/passengerinfo/data/entities.ts
Normal file
89
packages/logic/domains/passengerinfo/data/entities.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
flightPriceDetailsModel,
|
||||
allBagDetailsModel,
|
||||
} from "../../ticket/data/entities";
|
||||
import { paymentDetailsModel } from "../../paymentinfo/data/entities";
|
||||
|
||||
export enum Gender {
|
||||
Male = "male",
|
||||
Female = "female",
|
||||
Other = "other",
|
||||
}
|
||||
|
||||
export enum PassengerType {
|
||||
Adult = "adult",
|
||||
Child = "child",
|
||||
}
|
||||
|
||||
export const passengerPIIModel = z.object({
|
||||
firstName: z.string().min(1).max(255),
|
||||
middleName: z.string().min(0).max(255),
|
||||
lastName: z.string().min(1).max(255),
|
||||
email: z.string().email(),
|
||||
phoneCountryCode: z.string().min(2).max(6).regex(/^\+/),
|
||||
phoneNumber: z.string().min(2).max(20),
|
||||
nationality: z.string().min(1).max(128),
|
||||
gender: z.enum([Gender.Male, Gender.Female, Gender.Other]),
|
||||
dob: z.string().date(),
|
||||
passportNo: z.string().min(1).max(64),
|
||||
// add a custom validator to ensure this is not expired (present or older)
|
||||
passportExpiry: z
|
||||
.string()
|
||||
.date()
|
||||
.refine(
|
||||
(v) => new Date(v).getTime() > new Date().getTime(),
|
||||
"Passport expiry must be in the future",
|
||||
),
|
||||
|
||||
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(4).max(21),
|
||||
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 const passengerInfoModel = z.object({
|
||||
id: z.number(),
|
||||
passengerType: z.enum([PassengerType.Adult, PassengerType.Child]),
|
||||
passengerPii: passengerPIIModel,
|
||||
paymentDetails: paymentDetailsModel.optional(),
|
||||
passengerPiiId: z.number().optional(),
|
||||
paymentDetailsId: z.number().optional(),
|
||||
seatSelection: seatSelectionInfoModel,
|
||||
bagSelection: bagSelectionInfoModel,
|
||||
|
||||
agentsInfo: z.boolean().default(false).optional(),
|
||||
agentId: z.coerce.string().optional(),
|
||||
flightTicketInfoId: z.number().optional(),
|
||||
orderId: z.number().optional(),
|
||||
});
|
||||
export type PassengerInfo = z.infer<typeof passengerInfoModel>;
|
||||
92
packages/logic/domains/paymentinfo/data/entities.ts
Normal file
92
packages/logic/domains/paymentinfo/data/entities.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export enum PaymentMethod {
|
||||
Card = "card",
|
||||
// INFO: for other future payment methods
|
||||
}
|
||||
|
||||
function isValidLuhn(cardNumber: string): boolean {
|
||||
const digits = cardNumber.replace(/\D/g, "");
|
||||
let sum = 0;
|
||||
let isEven = false;
|
||||
|
||||
for (let i = digits.length - 1; i >= 0; i--) {
|
||||
let digit = parseInt(digits[i], 10);
|
||||
|
||||
if (isEven) {
|
||||
digit *= 2;
|
||||
if (digit > 9) {
|
||||
digit -= 9;
|
||||
}
|
||||
}
|
||||
|
||||
sum += digit;
|
||||
isEven = !isEven;
|
||||
}
|
||||
|
||||
return sum % 10 === 0;
|
||||
}
|
||||
|
||||
function isValidExpiry(expiryDate: string): boolean {
|
||||
const match = expiryDate.match(/^(0[1-9]|1[0-2])\/(\d{2})$/);
|
||||
if (!match) return false;
|
||||
|
||||
const month = parseInt(match[1], 10);
|
||||
const year = parseInt(match[2], 10);
|
||||
|
||||
const currentDate = new Date();
|
||||
const currentYear = currentDate.getFullYear() % 100; // Get last 2 digits of year
|
||||
const maxYear = currentYear + 20;
|
||||
|
||||
// Check if year is within valid range
|
||||
if (year < currentYear || year > maxYear) return false;
|
||||
|
||||
// If it's the current year, check if the month is valid
|
||||
if (year === currentYear) {
|
||||
const currentMonth = currentDate.getMonth() + 1; // getMonth() returns 0-11
|
||||
if (month < currentMonth) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const cardInfoModel = z.object({
|
||||
cardholderName: z.string().min(1).max(128),
|
||||
cardNumber: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(20)
|
||||
.regex(/^\d+$/, "Card number must be numeric")
|
||||
.refine((val) => isValidLuhn(val), {
|
||||
message: "Invalid card number",
|
||||
}),
|
||||
expiry: z
|
||||
.string()
|
||||
.regex(/^(0[1-9]|1[0-2])\/\d{2}$/, "Expiry must be in mm/yy format")
|
||||
.refine((val) => isValidExpiry(val), {
|
||||
message: "Invalid expiry date",
|
||||
}),
|
||||
cvv: z
|
||||
.string()
|
||||
.min(3)
|
||||
.max(4)
|
||||
.regex(/^\d{3,4}$/, "CVV must be 3-4 digits"),
|
||||
});
|
||||
export type CardInfo = z.infer<typeof cardInfoModel>;
|
||||
|
||||
export const paymentDetailsPayloadModel = z.object({
|
||||
method: z.enum([PaymentMethod.Card]),
|
||||
cardDetails: cardInfoModel,
|
||||
flightTicketInfoId: z.number().int(),
|
||||
});
|
||||
export type PaymentDetailsPayload = z.infer<typeof paymentDetailsPayloadModel>;
|
||||
|
||||
export const paymentDetailsModel = cardInfoModel.merge(
|
||||
z.object({
|
||||
id: z.number().int(),
|
||||
flightTicketInfoId: z.number().int(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
}),
|
||||
);
|
||||
export type PaymentDetails = z.infer<typeof paymentDetailsModel>;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from "zod";
|
||||
import { PackageType, PaymentMethod } from "./enums";
|
||||
export * from "../../../passengerinfo/data/entities";
|
||||
|
||||
// Flight package selection models
|
||||
|
||||
export const packageSelectionModel = z.object({
|
||||
packageType: z.enum([
|
||||
PackageType.Basic,
|
||||
PackageType.Flex,
|
||||
PackageType.Premium,
|
||||
]),
|
||||
insurance: z.boolean().default(false),
|
||||
});
|
||||
export type PackageSelection = z.infer<typeof packageSelectionModel>;
|
||||
|
||||
// payment models
|
||||
|
||||
export const cardInfoModel = z.object({
|
||||
nameOnCard: z.string().min(1).max(255),
|
||||
number: z.string().max(20),
|
||||
expiryDate: z.string().max(8),
|
||||
cvv: z.string().max(6),
|
||||
});
|
||||
export type CardInfo = z.infer<typeof cardInfoModel>;
|
||||
|
||||
export const paymentInfoModel = z.object({
|
||||
method: z.enum([PaymentMethod.Card]).default(PaymentMethod.Card),
|
||||
cardInfo: cardInfoModel,
|
||||
});
|
||||
export type PaymentInfo = z.infer<typeof paymentInfoModel>;
|
||||
43
packages/logic/domains/ticket/data/entities/enums.ts
Normal file
43
packages/logic/domains/ticket/data/entities/enums.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export enum CheckoutStep {
|
||||
Setup = "SETUP",
|
||||
Initial = "INITIAL",
|
||||
Payment = "PAYMENT",
|
||||
Verification = "VERIFICATION",
|
||||
Confirmation = "CONFIRMATION",
|
||||
Complete = "COMPLETE",
|
||||
}
|
||||
|
||||
export const TicketType = {
|
||||
OneWay: "ONEWAY",
|
||||
Return: "RETURN",
|
||||
};
|
||||
|
||||
export const CabinClass = {
|
||||
Economy: "ECONOMY",
|
||||
PremiumEconomy: "PREMIUM_ECONOMY",
|
||||
Business: "BUSINESS",
|
||||
FirstClass: "FIRST_CLASS",
|
||||
};
|
||||
|
||||
export enum Gender {
|
||||
Male = "male",
|
||||
Female = "female",
|
||||
Other = "other",
|
||||
}
|
||||
|
||||
export enum PaymentMethod {
|
||||
Card = "CARD",
|
||||
GooglePay = "GOOGLE_PAY",
|
||||
ApplePay = "APPLEPAY",
|
||||
}
|
||||
|
||||
export enum PassengerType {
|
||||
Adult = "adult",
|
||||
Child = "child",
|
||||
}
|
||||
|
||||
export enum PackageType {
|
||||
Basic = "basic",
|
||||
Flex = "flex",
|
||||
Premium = "premium",
|
||||
}
|
||||
178
packages/logic/domains/ticket/data/entities/index.ts
Normal file
178
packages/logic/domains/ticket/data/entities/index.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { z } from "zod";
|
||||
import { CabinClass, TicketType } from "./enums";
|
||||
|
||||
export * from "./enums";
|
||||
|
||||
export const stationModel = z.object({
|
||||
id: z.number(),
|
||||
type: z.string(),
|
||||
code: z.string(),
|
||||
name: z.string(),
|
||||
city: z.string(),
|
||||
country: z.string(),
|
||||
});
|
||||
export type Station = z.infer<typeof stationModel>;
|
||||
|
||||
export const iteneraryStationModel = z.object({
|
||||
station: stationModel,
|
||||
localTime: z.string(),
|
||||
utcTime: z.string(),
|
||||
});
|
||||
export type IteneraryStation = z.infer<typeof iteneraryStationModel>;
|
||||
|
||||
export const seatInfoModel = z.object({
|
||||
availableSeats: z.number(),
|
||||
seatClass: z.string(),
|
||||
});
|
||||
export type SeatInfo = z.infer<typeof seatInfoModel>;
|
||||
|
||||
export const flightPriceDetailsModel = z.object({
|
||||
currency: z.string(),
|
||||
basePrice: z.number(),
|
||||
discountAmount: z.number(),
|
||||
displayPrice: z.number(),
|
||||
orderPrice: z.number().nullable().optional(),
|
||||
appliedCoupon: z.string().nullish().optional(),
|
||||
couponDescription: z.string().nullish().optional(),
|
||||
});
|
||||
export type FlightPriceDetails = z.infer<typeof flightPriceDetailsModel>;
|
||||
|
||||
export const airlineModel = z.object({
|
||||
code: z.string(),
|
||||
name: z.string(),
|
||||
imageUrl: z.string().nullable().optional(),
|
||||
});
|
||||
export type Airline = z.infer<typeof airlineModel>;
|
||||
|
||||
export const flightIteneraryModel = z.object({
|
||||
flightId: z.string(),
|
||||
flightNumber: z.string(),
|
||||
airline: airlineModel,
|
||||
departure: iteneraryStationModel,
|
||||
destination: iteneraryStationModel,
|
||||
durationSeconds: z.number(),
|
||||
seatInfo: seatInfoModel,
|
||||
});
|
||||
export type FlightItenerary = z.infer<typeof flightIteneraryModel>;
|
||||
|
||||
export const passengerCountModel = z.object({
|
||||
adults: z.number().int().min(0),
|
||||
children: z.number().int().min(0),
|
||||
});
|
||||
export type PassengerCount = z.infer<typeof passengerCountModel>;
|
||||
|
||||
export function countPassengers(model: PassengerCount) {
|
||||
return model.adults + model.children;
|
||||
}
|
||||
|
||||
export const bagDimensionsModel = z.object({
|
||||
length: z.number(),
|
||||
width: z.number(),
|
||||
height: z.number(),
|
||||
});
|
||||
export type BagDimensions = z.infer<typeof bagDimensionsModel>;
|
||||
|
||||
export const bagDetailsModel = z.object({
|
||||
price: z.number(),
|
||||
weight: z.number(),
|
||||
unit: z.string(),
|
||||
dimensions: bagDimensionsModel,
|
||||
});
|
||||
export type BagDetails = z.infer<typeof bagDetailsModel>;
|
||||
|
||||
export const allBagDetailsModel = z.object({
|
||||
personalBags: bagDetailsModel,
|
||||
handBags: bagDetailsModel,
|
||||
checkedBags: bagDetailsModel,
|
||||
});
|
||||
export type AllBagDetails = z.infer<typeof allBagDetailsModel>;
|
||||
|
||||
// INFO: If you are to array-ificate it, you can just modify the details
|
||||
// key below so that the user's order thing is not disrupted
|
||||
|
||||
export const bagsInfoModel = z.object({
|
||||
includedPersonalBags: z.number().default(1),
|
||||
includedHandBags: z.number().default(0),
|
||||
includedCheckedBags: z.number().default(0),
|
||||
hasHandBagsSupport: z.boolean().default(true),
|
||||
hasCheckedBagsSupport: z.boolean().default(true),
|
||||
details: allBagDetailsModel,
|
||||
});
|
||||
export type BagsInfo = z.infer<typeof bagsInfoModel>;
|
||||
|
||||
export const flightTicketModel = z.object({
|
||||
id: z.number().int(),
|
||||
ticketId: z.string(),
|
||||
|
||||
// For lookup purposes, we need these on the top level
|
||||
departure: z.string(),
|
||||
arrival: z.string(),
|
||||
departureDate: z.coerce.string(),
|
||||
returnDate: z.coerce.string().default(""),
|
||||
dates: z.array(z.string()),
|
||||
|
||||
flightType: z.enum([TicketType.OneWay, TicketType.Return]),
|
||||
flightIteneraries: z.object({
|
||||
outbound: z.array(flightIteneraryModel),
|
||||
inbound: z.array(flightIteneraryModel),
|
||||
}),
|
||||
priceDetails: flightPriceDetailsModel,
|
||||
refundable: z.boolean(),
|
||||
passengerCounts: passengerCountModel,
|
||||
cabinClass: z.string(),
|
||||
bagsInfo: bagsInfoModel,
|
||||
lastAvailable: z.object({ availableSeats: z.number() }),
|
||||
|
||||
shareId: z.string(),
|
||||
checkoutUrl: z.string(),
|
||||
|
||||
isCache: z.boolean().nullish().optional(),
|
||||
refOIds: z.array(z.coerce.number()).nullish().optional(),
|
||||
|
||||
createdAt: z.coerce.string(),
|
||||
updatedAt: z.coerce.string(),
|
||||
});
|
||||
export type FlightTicket = z.infer<typeof flightTicketModel>;
|
||||
|
||||
export const limitedFlightTicketModel = flightTicketModel.pick({
|
||||
id: true,
|
||||
departure: true,
|
||||
arrival: true,
|
||||
departureDate: true,
|
||||
returnDate: true,
|
||||
flightType: true,
|
||||
dates: true,
|
||||
priceDetails: true,
|
||||
passengerCounts: true,
|
||||
cabinClass: true,
|
||||
});
|
||||
export type LimitedFlightTicket = z.infer<typeof limitedFlightTicketModel>;
|
||||
|
||||
// INFO: ticket search models
|
||||
|
||||
export const ticketSearchPayloadModel = z.object({
|
||||
sessionId: z.string(),
|
||||
ticketType: z.enum([TicketType.OneWay, TicketType.Return]),
|
||||
cabinClass: z.enum([
|
||||
CabinClass.Economy,
|
||||
CabinClass.PremiumEconomy,
|
||||
CabinClass.Business,
|
||||
CabinClass.FirstClass,
|
||||
]),
|
||||
departure: z.string().min(3),
|
||||
arrival: z.string().min(3),
|
||||
passengerCounts: passengerCountModel,
|
||||
departureDate: z.coerce.string().min(3),
|
||||
returnDate: z.coerce.string(),
|
||||
loadMore: z.boolean().default(false),
|
||||
meta: z.record(z.string(), z.any()).optional(),
|
||||
couponCode: z.string().optional(),
|
||||
});
|
||||
export type TicketSearchPayload = z.infer<typeof ticketSearchPayloadModel>;
|
||||
|
||||
export const ticketSearchDTO = z.object({
|
||||
sessionId: z.string(),
|
||||
ticketSearchPayload: ticketSearchPayloadModel,
|
||||
providers: z.array(z.string()).optional(),
|
||||
});
|
||||
export type TicketSearchDTO = z.infer<typeof ticketSearchDTO>;
|
||||
0
packages/logic/domains/ticket/data/repository.ts
Normal file
0
packages/logic/domains/ticket/data/repository.ts
Normal file
0
packages/logic/domains/ticket/usecases.ts
Normal file
0
packages/logic/domains/ticket/usecases.ts
Normal file
103
packages/logic/domains/user/data/entities.ts
Normal file
103
packages/logic/domains/user/data/entities.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { UserRoleMap } from "../../../core/enums";
|
||||
import { paginationModel } from "../../../core/pagination.utils";
|
||||
import { encodeCursor } from "../../../core/string.utils";
|
||||
import z from "zod";
|
||||
|
||||
const usernameModel = z
|
||||
.string()
|
||||
.min(2)
|
||||
.max(128)
|
||||
.regex(/^[a-zA-Z0-9_-]+$/, {
|
||||
message:
|
||||
"Username can only contain letters, numbers, underscores, and hyphens.",
|
||||
});
|
||||
|
||||
export const emailModel = z.string().email().min(5).max(128);
|
||||
|
||||
export const discountPercentModel = z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.default(0)
|
||||
.nullish();
|
||||
|
||||
const roleModel = z.enum(Object.values(UserRoleMap) as [string, ...string[]]);
|
||||
|
||||
export const createUserPayloadModel = z.object({
|
||||
username: usernameModel,
|
||||
email: emailModel,
|
||||
password: z.string().min(8).max(128),
|
||||
role: roleModel,
|
||||
});
|
||||
|
||||
export type CreateUserPayloadModel = z.infer<typeof createUserPayloadModel>;
|
||||
|
||||
export type GetUserByPayload = { id?: string; username?: string };
|
||||
|
||||
export const updateUserInfoInputModel = z.object({
|
||||
email: emailModel,
|
||||
username: usernameModel,
|
||||
discountPercent: discountPercentModel,
|
||||
banned: z.boolean().nullable().optional(),
|
||||
});
|
||||
export type UpdateUserInfoInputModel = z.infer<typeof updateUserInfoInputModel>;
|
||||
|
||||
export const userModel = updateUserInfoInputModel.merge(
|
||||
z.object({
|
||||
id: z.coerce.string(),
|
||||
role: roleModel.nullable().optional(),
|
||||
discountPercent: discountPercentModel,
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
parentId: z.coerce.string().nullable().optional(),
|
||||
}),
|
||||
);
|
||||
export type UserModel = z.infer<typeof userModel>;
|
||||
|
||||
export const limitedUserModel = userModel.pick({
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
});
|
||||
|
||||
export type LimitedUserModel = z.infer<typeof limitedUserModel>;
|
||||
|
||||
export const userFullModel = userModel.merge(
|
||||
z.object({
|
||||
banned: z.boolean(),
|
||||
banReason: z.string().optional(),
|
||||
banExpires: z.date().optional(),
|
||||
twoFactorEnabled: z.boolean().default(false),
|
||||
}),
|
||||
);
|
||||
export type UserFullModel = z.infer<typeof userFullModel>;
|
||||
|
||||
export const usersCursorModel = z.object({
|
||||
firstItemUsername: z.string(),
|
||||
lastItemUsername: z.string(),
|
||||
query: z.string().default(""),
|
||||
});
|
||||
export type UsersCursorModel = z.infer<typeof usersCursorModel>;
|
||||
export function getDefaultUsersCursor() {
|
||||
return usersCursorModel.parse({
|
||||
firstItemUsername: "",
|
||||
lastItemUsername: "",
|
||||
query: "",
|
||||
});
|
||||
}
|
||||
|
||||
export const paginatedUserModel = paginationModel.merge(
|
||||
z.object({ data: z.array(userModel) }),
|
||||
);
|
||||
export type PaginatedUserModel = z.infer<typeof paginatedUserModel>;
|
||||
export function getDefaultPaginatedUserModel(): PaginatedUserModel {
|
||||
return {
|
||||
data: [],
|
||||
cursor: encodeCursor<UsersCursorModel>(getDefaultUsersCursor()),
|
||||
limit: 20,
|
||||
asc: true,
|
||||
totalItemCount: 0,
|
||||
totalPages: 0,
|
||||
page: 0,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user