stashing code
This commit is contained in:
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));
|
||||
}
|
||||
Reference in New Issue
Block a user