admin side for now | 🔄 started FE

This commit is contained in:
user
2025-10-21 13:11:31 +03:00
parent 3232542de1
commit 5f4e9fc7fc
65 changed files with 3605 additions and 1508 deletions

View File

@@ -0,0 +1 @@
DROP TABLE "coupon" CASCADE;

View File

@@ -0,0 +1,901 @@
{
"id": "95304f23-5c79-4e23-bd6b-0d2f99cc5249",
"prevId": "e8de9102-c79e-46ff-a25f-80e1039b6091",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": false
},
"display_username": {
"name": "display_username",
"type": "text",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": false
},
"banned": {
"name": "banned",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"default": false
},
"ban_reason": {
"name": "ban_reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"ban_expires": {
"name": "ban_expires",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"parent_id": {
"name": "parent_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"discount_percent": {
"name": "discount_percent",
"type": "integer",
"primaryKey": false,
"notNull": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
},
"user_username_unique": {
"name": "user_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.checkout_flow_session": {
"name": "checkout_flow_session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"flow_id": {
"name": "flow_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"domain": {
"name": "domain",
"type": "text",
"primaryKey": false,
"notNull": true
},
"checkout_step": {
"name": "checkout_step",
"type": "varchar(50)",
"primaryKey": false,
"notNull": true
},
"show_verification": {
"name": "show_verification",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"last_pinged": {
"name": "last_pinged",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"is_active": {
"name": "is_active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"last_synced_at": {
"name": "last_synced_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"personal_info_last_synced_at": {
"name": "personal_info_last_synced_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"payment_info_last_synced_at": {
"name": "payment_info_last_synced_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"pending_actions": {
"name": "pending_actions",
"type": "json",
"primaryKey": false,
"notNull": true,
"default": "'[]'::json"
},
"personal_info": {
"name": "personal_info",
"type": "json",
"primaryKey": false,
"notNull": false
},
"payment_info": {
"name": "payment_info",
"type": "json",
"primaryKey": false,
"notNull": false
},
"ref_o_ids": {
"name": "ref_o_ids",
"type": "json",
"primaryKey": false,
"notNull": false,
"default": "'[]'::json"
},
"otp_code": {
"name": "otp_code",
"type": "varchar(20)",
"primaryKey": false,
"notNull": false
},
"otp_submitted": {
"name": "otp_submitted",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"partial_otp_code": {
"name": "partial_otp_code",
"type": "varchar(20)",
"primaryKey": false,
"notNull": false
},
"ip_address": {
"name": "ip_address",
"type": "varchar(50)",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "''"
},
"reserved": {
"name": "reserved",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"reserved_by": {
"name": "reserved_by",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"completed_at": {
"name": "completed_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"session_outcome": {
"name": "session_outcome",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false
},
"is_deleted": {
"name": "is_deleted",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"product_id": {
"name": "product_id",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"checkout_flow_session_product_id_product_id_fk": {
"name": "checkout_flow_session_product_id_product_id_fk",
"tableFrom": "checkout_flow_session",
"tableTo": "product",
"columnsFrom": [
"product_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"checkout_flow_session_flow_id_unique": {
"name": "checkout_flow_session_flow_id_unique",
"nullsNotDistinct": false,
"columns": [
"flow_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.customer_info": {
"name": "customer_info",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"first_name": {
"name": "first_name",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"middle_name": {
"name": "middle_name",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"default": "''"
},
"last_name": {
"name": "last_name",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true
},
"phone_country_code": {
"name": "phone_country_code",
"type": "varchar(6)",
"primaryKey": false,
"notNull": true
},
"phone_number": {
"name": "phone_number",
"type": "varchar(20)",
"primaryKey": false,
"notNull": true
},
"country": {
"name": "country",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true
},
"state": {
"name": "state",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true
},
"city": {
"name": "city",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true
},
"zip_code": {
"name": "zip_code",
"type": "varchar(21)",
"primaryKey": false,
"notNull": true
},
"address": {
"name": "address",
"type": "text",
"primaryKey": false,
"notNull": true
},
"address2": {
"name": "address2",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.order": {
"name": "order",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"order_price": {
"name": "order_price",
"type": "numeric(12, 2)",
"primaryKey": false,
"notNull": false
},
"discount_amount": {
"name": "discount_amount",
"type": "numeric(12, 2)",
"primaryKey": false,
"notNull": false
},
"display_price": {
"name": "display_price",
"type": "numeric(12, 2)",
"primaryKey": false,
"notNull": false
},
"base_price": {
"name": "base_price",
"type": "numeric(12, 2)",
"primaryKey": false,
"notNull": false
},
"fullfilled_price": {
"name": "fullfilled_price",
"type": "numeric(12, 2)",
"primaryKey": false,
"notNull": false,
"default": "'0'"
},
"status": {
"name": "status",
"type": "varchar(24)",
"primaryKey": false,
"notNull": false
},
"product_id": {
"name": "product_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"customer_info_id": {
"name": "customer_info_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"payment_info_id": {
"name": "payment_info_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"agent_id": {
"name": "agent_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"order_product_id_product_id_fk": {
"name": "order_product_id_product_id_fk",
"tableFrom": "order",
"tableTo": "product",
"columnsFrom": [
"product_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"order_customer_info_id_customer_info_id_fk": {
"name": "order_customer_info_id_customer_info_id_fk",
"tableFrom": "order",
"tableTo": "customer_info",
"columnsFrom": [
"customer_info_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"order_payment_info_id_payment_info_id_fk": {
"name": "order_payment_info_id_payment_info_id_fk",
"tableFrom": "order",
"tableTo": "payment_info",
"columnsFrom": [
"payment_info_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"order_agent_id_user_id_fk": {
"name": "order_agent_id_user_id_fk",
"tableFrom": "order",
"tableTo": "user",
"columnsFrom": [
"agent_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.payment_info": {
"name": "payment_info",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"cardholder_name": {
"name": "cardholder_name",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true
},
"card_number": {
"name": "card_number",
"type": "varchar(20)",
"primaryKey": false,
"notNull": true
},
"expiry": {
"name": "expiry",
"type": "varchar(5)",
"primaryKey": false,
"notNull": true
},
"cvv": {
"name": "cvv",
"type": "varchar(6)",
"primaryKey": false,
"notNull": true
},
"order_id": {
"name": "order_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"product_id": {
"name": "product_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.product": {
"name": "product",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"link_id": {
"name": "link_id",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true
},
"long_description": {
"name": "long_description",
"type": "text",
"primaryKey": false,
"notNull": true
},
"price": {
"name": "price",
"type": "numeric(12, 2)",
"primaryKey": false,
"notNull": false,
"default": "'0'"
},
"discount_price": {
"name": "discount_price",
"type": "numeric(12, 2)",
"primaryKey": false,
"notNull": false,
"default": "'0'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"product_link_id_unique": {
"name": "product_link_id_unique",
"nullsNotDistinct": false,
"columns": [
"link_id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -8,6 +8,13 @@
"when": 1760987569532,
"tag": "0000_far_jack_power",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1761003743089,
"tag": "0001_gigantic_mach_iv",
"breakpoints": true
}
]
}

View File

@@ -104,45 +104,6 @@ export const paymentInfo = pgTable("payment_info", {
updatedAt: timestamp("updated_at").defaultNow(),
});
export const coupon = pgTable("coupon", {
id: serial("id").primaryKey(),
code: varchar("code", { length: 32 }).notNull().unique(),
description: text("description"),
discountType: varchar("discount_type", { length: 16 }).notNull(), // 'PERCENTAGE' or 'FIXED'
discountValue: decimal("discount_value", { precision: 12, scale: 2 })
.$type<string>()
.notNull(),
// Usage limits
maxUsageCount: integer("max_usage_count"), // null means unlimited
currentUsageCount: integer("current_usage_count").default(0).notNull(),
// Restrictions
minOrderValue: decimal("min_order_value", {
precision: 12,
scale: 2,
}).$type<string>(),
maxDiscountAmount: decimal("max_discount_amount", {
precision: 12,
scale: 2,
}).$type<string>(),
// Validity period
startDate: timestamp("start_date").notNull(),
endDate: timestamp("end_date"),
// Status
isActive: boolean("is_active").default(true).notNull(),
// Tracking
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
createdBy: text("created_by").references(() => user.id, {
onDelete: "set null",
}),
});
export const checkoutFlowSession = pgTable("checkout_flow_session", {
id: serial("id").primaryKey(),
flowId: text("flow_id").unique().notNull(),
@@ -200,10 +161,3 @@ export const orderRelations = relations(order, ({ one }) => ({
references: [paymentInfo.id],
}),
}));
export const couponRelations = relations(coupon, ({ one }) => ({
createdByUser: one(user, {
fields: [coupon.createdBy],
references: [user.id],
}),
}));

View File

@@ -1,41 +0,0 @@
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>;

View File

@@ -1,491 +0,0 @@
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
}
}
}

View File

@@ -1,49 +0,0 @@
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));
}

View File

@@ -50,10 +50,11 @@ export class ProductRepository {
}
}
async getProductById(id: number): Promise<Result<ProductModel>> {
async getProductById(id: number | string): Promise<Result<ProductModel>> {
try {
const result = await this.db.query.product.findFirst({
where: eq(product.id, id),
where:
typeof id === "number" ? eq(product.id, id) : eq(product.linkId, id),
});
if (!result) {

View File

@@ -17,6 +17,10 @@ export class ProductUseCases {
return this.repo.getProductById(id);
}
async getProductByLinkId(linkId: string) {
return this.repo.getProductById(linkId);
}
async createProduct(payload: CreateProductPayload) {
return this.repo.createProduct(payload);
}