492 lines
14 KiB
TypeScript
492 lines
14 KiB
TypeScript
import {
|
|
and,
|
|
asc,
|
|
desc,
|
|
eq,
|
|
gte,
|
|
isNull,
|
|
lte,
|
|
or,
|
|
type Database,
|
|
} from "@pkg/db";
|
|
import { coupon } from "@pkg/db/schema";
|
|
import { getError, Logger } from "@pkg/logger";
|
|
import { ERROR_CODES, type Result } from "@pkg/result";
|
|
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
|
|
}
|
|
}
|
|
}
|