18 Commits

Author SHA1 Message Date
rahat0078 49cb339e5a feat(analytics:admin): andmin analytics dashboard api done 2026-05-16 23:30:05 +06:00
sharafat 64a0a80a2a merge sharafat and rahat branch 2026-04-30 21:45:27 +06:00
sharafat d2b320f3b1 Change the api and add some new feature 2026-04-30 21:26:18 +06:00
rahat0078 7d89f3f4ea feat(stats): seller stat api completed 2026-04-29 20:18:49 +06:00
sharafat b09fdfc255 Template-API's : create tamplate and also add the get all template and get single templates 2026-04-28 00:29:01 +06:00
sharafat 47d30d96eb Template API: create database schema 2026-04-27 00:03:06 +06:00
abumahid 61fd639faf feat(account, order, plan, profile, redis): enhance functionality and security
- Updated CORS settings for frontend compatibility.
- Integrated Redis URL configuration.
- Improved login response structure in account service.
- Added role-based authorization for order and plan management.
- Enhanced error handling and logging in profile and plan services.
- Updated Swagger documentation for clarity on order statuses.
- Configured Redis connection for better performance.
2026-04-26 19:14:37 +06:00
abumahid 2d54031c33 🔧 refactor(plan): update imports to use .js extensions and secure delete route with admin auth 2026-04-26 19:10:56 +06:00
abumahid f886c392aa feat(account, queues): enhance email sending and Redis connection settings
- Added attempts and removal options for email queue tasks in `account.service.ts`.
- Updated Redis connection parameters to specify `maxRetriesPerRequest` and disable `enableReadyCheck` in `connection.ts`.
- Introduced an empty line for clarity in the `email.queue.ts` file.
2026-04-26 19:04:34 +06:00
sharafat 107b94bc97 adding the order service delete on the plan api 2026-04-26 18:57:33 +06:00
abumahid e227c42f7d feat(account): update login response and modify CORS origin
- Changed the CORS origin from `http://localhost:3000` to `http://localhost:5173`.
- Updated the login response to return a comprehensive object containing the `accessToken` and user profile data.
- Modified cookie setup to directly use `result.accessToken` instead of `result`.
- Refactored account service to include additional user fields in the login data returned.
- Added `dist` to `.gitignore`.
2026-04-26 18:56:50 +06:00
abumahid 0f7af70b90 ♻️ refactor(account, order, plan, profile, support, email): restructure application modules and enhance error handling
Updated Docker configuration, refactored middleware for improved error handling, and restructured account, order, plan, profile, and support modules, including their routes, services, and validations. Enhanced email processing queues and utilities for token generation, pagination, and response management to streamline the application architecture and enhance maintainability.
2026-04-21 03:12:39 +06:00
abumahid c881efea0f Merge remote-tracking branch 'origin/sharafat' into dev 2026-04-20 20:39:16 +06:00
sharafat 4c1614601a Order API:All routes was created and fully tested 2026-04-19 00:26:50 +06:00
sharafat 1bc1fae274 order api:implement pagination system 2026-04-17 23:36:01 +06:00
rahat0078 86b2292272 feat(support): update Swagger documentation for support endpoints 2026-04-17 00:19:22 +06:00
abumahid 739e3d1ad6 merge rahat 2026-04-15 23:38:15 +06:00
rahat0078 ba04c54c5b feat(support): complete full CRUD for support module 2026-04-13 00:35:51 +06:00
121 changed files with 5120 additions and 213 deletions
+5 -4
View File
@@ -1,8 +1,9 @@
node_modules
npm-debug.log
Dockerfile
dist
.git
.gitignore
README.md
Dockerfile
docker-compose.yml
*.log
.env
dist
uploads
+2 -1
View File
@@ -4,4 +4,5 @@ node_modules
.env.example
.env.prod
package-lock.json
prisma/generated/
prisma/generated/
dist
+21 -6
View File
@@ -1,25 +1,40 @@
# ---------- BUILD STAGE ----------
FROM node:18-alpine AS builder
FROM node:20-alpine AS builder
WORKDIR /app
# Only install deps first (cache friendly)
COPY package*.json ./
RUN npm install
RUN npm ci
# Copy source
COPY . .
# Generate prisma + build
RUN npx prisma generate
RUN npm run build
# ---------- PRODUCTION STAGE ----------
FROM node:18-alpine
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install && npm cache clean --force
ENV NODE_ENV=production
# Only install production deps
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
# Copy Prisma generated client + schema
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma
COPY --from=builder /app/prisma ./prisma
# Copy built app
COPY --from=builder /app/dist ./dist
# Uploads folder
RUN mkdir -p /app/uploads
EXPOSE 5000
Vendored
+37
View File
@@ -0,0 +1,37 @@
import cookieParser from 'cookie-parser';
import cors from 'cors';
import express from 'express';
import swaggerJSDoc from 'swagger-jsdoc';
import swaggerUi from "swagger-ui-express";
import globalErrorHandler from './app/middlewares/global_error_handler.js';
import notFound from './app/middlewares/not_found_api.js';
import appRouter from './routes.js';
import { swaggerOptions } from './swaggerOptions.js';
// define app
const app = express();
const swaggerSpec = swaggerJSDoc(swaggerOptions);
app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// middleware
app.use(cors({
origin: ["http://localhost:5173"],
methods: ["GET", "POST", "PATCH", "DELETE", "PUT"],
credentials: true
}));
app.use(express.json({ limit: "100mb" }));
app.use(express.raw());
app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));
app.use("/api", appRouter);
// stating point
app.get('/', (req, res) => {
res.status(200).json({
status: 'success',
message: 'Server is running successful !!',
data: null,
});
});
// global error handler
app.use(globalErrorHandler);
app.use(notFound);
// export app
export default app;
+26
View File
@@ -0,0 +1,26 @@
import "dotenv/config";
export const configs = {
port: process.env.PORT,
env: process.env.NODE_ENV,
db_url: process.env.DATABASE_URL,
jwt: {
access_token: process.env.ACCESS_TOKEN,
refresh_token: process.env.REFRESH_TOKEN,
access_expires: process.env.ACCESS_EXPIRES,
refresh_expires: process.env.REFRESH_EXPIRES,
reset_secret: process.env.RESET_SECRET,
reset_expires: process.env.RESET_EXPIRES,
front_end_url: process.env.FRONT_END_URL,
verified_token: process.env.VERIFIED_TOKEN,
},
email: {
app_email: process.env.APP_USER_EMAIL,
app_password: process.env.APP_PASSWORD,
},
cloudinary: {
cloud_name: process.env.CLOUD_NAME,
cloud_api_key: process.env.CLOUD_API_KEY,
cloud_api_secret: process.env.CLOUD_API_SECRET,
},
redis_url: process.env.REDIS_URL,
};
+15
View File
@@ -0,0 +1,15 @@
const handleZodError = (err) => {
const errorSources = err.issues.map((issue) => {
return {
path: issue?.path[issue.path.length - 1],
message: issue.message
};
});
const statusCode = 400;
return {
statusCode,
message: 'Validation Error',
errorSources
};
};
export default handleZodError;
+8
View File
@@ -0,0 +1,8 @@
import { PrismaPg } from "@prisma/adapter-pg";
import pkg from "@prisma/client";
import "dotenv/config";
const { PrismaClient } = pkg;
const connectionString = `${process.env.DATABASE_URL}`;
const adapter = new PrismaPg({ connectionString });
const prisma = new PrismaClient({ adapter });
export { prisma };
+23
View File
@@ -0,0 +1,23 @@
import { configs } from "../configs/index.js";
import { AppError } from "../utils/app_error.js";
import { jwtHelpers } from "../utils/JWT.js";
const auth = (...roles) => {
return async (req, res, next) => {
try {
const token = req.headers.authorization || req.cookies.access_token;
if (!token) {
throw new AppError("You are not authorize!!", 401);
}
const verifiedUser = jwtHelpers.verifyToken(token, configs.jwt.access_token);
if (!roles.length || !roles.includes(verifiedUser.role)) {
throw new AppError("You are not authorize!!", 401);
}
req.user = verifiedUser;
next();
}
catch (err) {
next(err);
}
};
};
export default auth;
+47
View File
@@ -0,0 +1,47 @@
import { ZodError } from "zod";
import { configs } from "../configs/index.js";
import handleZodError from "../errors/zodError.js";
import { AppError } from "../utils/app_error.js";
const globalErrorHandler = (err, req, res, next) => {
let statusCode = 500;
let message = "Something went wrong!";
let errorSources = [
{
path: "",
message: "Something went wrong",
},
];
if (err instanceof ZodError) {
const simplifiedError = handleZodError(err);
statusCode = simplifiedError?.statusCode;
message = simplifiedError?.message;
errorSources = simplifiedError?.errorSources;
}
else if (err instanceof AppError) {
statusCode = err?.statusCode;
message = err.message;
errorSources = [
{
path: "",
message: err?.message,
},
];
}
else if (err instanceof Error) {
message = err.message;
errorSources = [
{
path: "",
message: err?.message,
},
];
}
res.status(statusCode).json({
success: false,
message,
errorSources,
err,
stack: configs.env === "development" ? err?.stack : null,
});
};
export default globalErrorHandler;
+8
View File
@@ -0,0 +1,8 @@
const notFound = (req, res, next) => {
res.status(404).json({
message: 'Sorry Route is not found!! 😴😴😴',
success: false,
error: '',
});
};
export default notFound;
+12
View File
@@ -0,0 +1,12 @@
const RequestValidator = (schema) => {
return async (req, res, next) => {
try {
req.body = await schema.parseAsync(req.body);
next();
}
catch (err) {
next(err);
}
};
};
export default RequestValidator;
+12
View File
@@ -0,0 +1,12 @@
import multer from "multer";
import path from "path";
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, path.join(process.cwd(), "uploads"));
},
filename: function (req, file, cb) {
cb(null, file.originalname);
}
});
const uploader = multer({ storage: storage });
export default uploader;
+101
View File
@@ -0,0 +1,101 @@
import { configs } from "../../configs/index.js";
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { account_services } from "./account.service.js";
const create_account = catchAsync(async (req, res) => {
const result = await account_services.create_account_into_db(req);
manageResponse(res, {
statusCode: 200,
success: true,
message: "Account created successfully",
data: result,
});
});
const verify_account_using_otp = catchAsync(async (req, res) => {
const result = await account_services.verify_account_using_otp_into_db(req);
manageResponse(res, {
statusCode: 200,
success: true,
message: "Otp verification successful",
data: result,
});
});
const verify_account_using_link = catchAsync(async (req, res) => {
const result = await account_services.verify_account_using_link_into_db(req);
manageResponse(res, {
statusCode: 200,
success: true,
message: "Account verification successful",
data: result,
});
});
const login_user = catchAsync(async (req, res) => {
const result = await account_services.login_user_into_db(req);
// set access token into cookie
res.cookie("access_token", result.accessToken, {
secure: configs.env === "production",
httpOnly: true,
});
manageResponse(res, {
statusCode: 200,
success: true,
message: "User logged in successfully",
data: result,
});
});
const get_user_account = catchAsync(async (req, res) => {
const result = await account_services.get_user_account_from_db(req);
manageResponse(res, {
statusCode: 200,
success: true,
message: "Account fetched successfully",
data: result,
});
});
const change_password = catchAsync(async (req, res) => {
const result = await account_services.change_password_into_db(req);
manageResponse(res, {
statusCode: 200,
success: true,
message: "Password Change successfully",
data: result,
});
});
const resend_otp_and_verification_link = catchAsync(async (req, res) => {
const result = await account_services.resend_otp_and_verification_link_from_db(req);
manageResponse(res, {
statusCode: 200,
success: true,
message: "OTP reset successfully",
data: result,
});
});
const forget_password_generate_reset_token = catchAsync(async (req, res) => {
const result = await account_services.forget_password_generate_reset_token_from_db(req);
manageResponse(res, {
statusCode: 200,
success: true,
message: "Password reset successfully",
data: result,
});
});
const reset_password_using_token = catchAsync(async (req, res) => {
const result = await account_services.reset_password_using_token_into_db(req);
manageResponse(res, {
statusCode: 200,
success: true,
message: "Password reset successfully",
data: result,
});
});
export const account_controller = {
create_account,
login_user,
get_user_account,
change_password,
verify_account_using_otp,
resend_otp_and_verification_link,
verify_account_using_link,
forget_password_generate_reset_token,
reset_password_using_token
};
+16
View File
@@ -0,0 +1,16 @@
import { Router } from "express";
import auth from "../../middlewares/auth.js";
import RequestValidator from "../../middlewares/request_validator.js";
import { account_controller } from "./account.controller.js";
import { account_validation } from "./account.validation.js";
const accountRouter = Router();
accountRouter.post("/sign-up", RequestValidator(account_validation.sign_up), account_controller.create_account);
accountRouter.post("/sign-in", RequestValidator(account_validation.sing_in), account_controller.login_user);
accountRouter.put("/verify-otp", RequestValidator(account_validation.verify_otp), account_controller.verify_account_using_otp);
accountRouter.put("/verify-link", RequestValidator(account_validation.verify_link), account_controller.verify_account_using_link);
accountRouter.get("/me", auth("USER", "ADMIN"), account_controller.get_user_account);
accountRouter.put("/change-password", auth("USER", "ADMIN"), RequestValidator(account_validation.change_password), account_controller.change_password);
accountRouter.put("/resend-otp", RequestValidator(account_validation.resend_otp), account_controller.resend_otp_and_verification_link);
accountRouter.put("/forget-password", RequestValidator(account_validation.resend_otp), account_controller.forget_password_generate_reset_token);
accountRouter.put("/reset-password", RequestValidator(account_validation.reset_pass), account_controller.reset_password_using_token);
export default accountRouter;
+371
View File
@@ -0,0 +1,371 @@
import bcrypt from "bcrypt";
import { configs } from "../../configs/index.js";
import { prisma } from "../../lib/prisma.js";
import { emailQueue } from "../../queues/email/email.queue.js";
import { AppError } from "../../utils/app_error.js";
import { jwtHelpers } from "../../utils/JWT.js";
import sendMail from "../../utils/mail_sender.js";
import { otpGenerator } from "../../utils/otpGenerator.js";
const create_account_into_db = async (req) => {
const payload = req?.body;
// check account exist or not
const existingAccount = await prisma.account.findUnique({
where: { email: payload.email },
});
if (existingAccount) {
throw new AppError("Email already exists", 403);
}
// hash password
const hashPassword = bcrypt.hashSync(payload.password, 10);
// create account and profile
const result = await prisma.$transaction(async (tx) => {
const account = await tx.account.create({
data: {
email: payload.email,
password: hashPassword,
},
});
const profile = await tx.profile.create({
data: {
shopName: payload.shopName,
accountId: account.id,
},
});
return {
account,
profile,
};
});
// sending otp and verification link
const newOtp = otpGenerator();
const verificationToken = jwtHelpers.generateToken({
email: payload.email,
accountId: result.account.id,
}, configs.jwt.verified_token, "5m");
const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`;
// save otp into db
await prisma.account.update({
where: {
email: payload.email,
},
data: {
lastOtp: newOtp,
lastOtpSendingTime: new Date(),
},
});
await emailQueue.add("email-queue", {
name: payload.shopName,
otp: newOtp,
verificationLink: verificationLink,
subject: "Welcome to Quick Launch - Verification OTP",
email: payload.email,
textBody: "You can use otp or verification link for verifying your account"
}, {
attempts: 1,
removeOnComplete: true,
removeOnFail: true,
});
return null;
};
const verify_account_using_otp_into_db = async (req) => {
const payload = req?.body;
// check account
const account = await prisma.account.findUnique({
where: {
email: payload.email,
},
});
// check if account exists
if (!account) {
throw new AppError("Account not found", 404);
}
// match with last otp
const isOtpMatch = payload.otp === account.lastOtp;
if (!isOtpMatch) {
throw new AppError("Invalid OTP, Please try again!!", 401);
}
// check otp timing
const OTP_EXPIRY_TIME = 5 * 60 * 1000; // 5 minutes in ms
const isOtpExpired = account.lastOtpSendingTime
? new Date().getTime() - new Date(account.lastOtpSendingTime).getTime() >
OTP_EXPIRY_TIME
: true;
if (isOtpExpired) {
throw new AppError("OTP Expired, Please try again!!", 401);
}
// change account status
await prisma.account.update({
where: {
id: account.id,
},
data: {
isAccountVerified: true,
},
});
// infuter user welcome email
return "";
};
const verify_account_using_link_into_db = async (req) => {
const token = req?.body?.token;
let decoadeToken;
try {
decoadeToken = jwtHelpers.verifyToken(token, configs.jwt.verified_token);
}
catch (error) {
if (error?.message == "invalid signature") {
throw new AppError("Invalid Token", 403);
}
else if (error?.message == "jwt expired") {
throw new AppError("Token expired, please reset again", 403);
}
}
// check account
const account = await prisma.account.findUnique({
where: {
email: decoadeToken.email,
},
});
// check if account exists
if (!account) {
throw new AppError("Account not found", 404);
}
// change account status
await prisma.account.update({
where: {
id: account.id,
},
data: {
isAccountVerified: true,
},
});
// infuter user welcome email
return "";
};
const login_user_into_db = async (req) => {
const payload = req?.body;
const account = await prisma.account.findUnique({
where: {
email: payload.email,
},
select: {
id: true,
email: true,
role: true,
isAccountVerified: true,
isDeleted: true,
password: true,
profile: true,
},
});
// check if account exists
if (!account) {
throw new AppError("Account not found", 404);
}
// checking password
const isPasswordMatch = bcrypt.compareSync(payload.password, account.password);
if (!isPasswordMatch) {
throw new AppError("Invalid password", 401);
}
// check if account is deleted
if (account.isDeleted) {
throw new AppError("Account is deleted", 401);
}
// check if account is verified
if (!account.isAccountVerified) {
throw new AppError("Account is not verified", 401);
}
// generate access
const accessToken = jwtHelpers.generateToken({
email: account.email,
role: account.role,
accountId: account.id,
}, configs.jwt.access_token, configs.jwt.access_expires);
const finalOutputData = {
id: account.id,
email: account.email,
role: account.role,
shopName: account?.profile?.shopName,
shopLogo: account?.profile?.shopLogo,
};
return {
accessToken,
profile: finalOutputData
};
};
const get_user_account_from_db = async (req) => {
const user = req?.user;
const result = await prisma.account.findUnique({
where: {
id: user?.accountId,
},
select: {
id: true,
email: true,
role: true,
isAccountVerified: true,
isDeleted: true,
profile: true,
},
});
return result;
};
const change_password_into_db = async (req) => {
const user = req?.user;
// payload
const payload = req?.body;
// check old and new password is not same
const isSamePassword = payload.oldPassword === payload.newPassword;
if (isSamePassword) {
throw new AppError("Old and new password are same, Please provide deffirent password", 404);
}
// check user validity
const isUserExist = await prisma.account.findFirst({
where: {
email: user?.email,
},
});
// if account not exists
if (!isUserExist) {
throw new AppError("Account not found!!", 404);
}
// check old password
const isPasswordMatch = bcrypt.compareSync(payload.oldPassword, isUserExist.password);
if (!isPasswordMatch) {
throw new AppError("Incorrect password", 401);
}
// change password logic
const newHashPassword = bcrypt.hashSync(payload.newPassword, 10);
await prisma.account.update({
where: {
id: isUserExist.id,
},
data: {
password: newHashPassword,
},
});
// in future email notification for more sucurity
return "";
};
const resend_otp_and_verification_link_from_db = async (req) => {
const email = req?.body?.email;
const account = await prisma.account.findUnique({
where: {
email,
},
});
// check if account exists
if (!account) {
throw new AppError("Account not found", 404);
}
// if already verified
if (account.isAccountVerified) {
throw new AppError("Account already verified", 403);
}
// make new otp and verification link
const newOtp = otpGenerator();
const verificationToken = jwtHelpers.generateToken({
email,
accountId: account.id,
}, configs.jwt.verified_token, "5m");
const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`;
// save otp into db
await prisma.account.update({
where: {
email,
},
data: {
lastOtp: newOtp,
lastOtpSendingTime: new Date(),
},
});
await sendMail({
to: email,
subject: "New Verification otp and link",
htmlBody: `
<p><strong>OTP</strong> ${newOtp}</p>
<small>Otp will be expire in 5 minutes</small>
<br/> <br/>
<p>Or you can use Verification link </p>
<p>${verificationLink}</p>
`,
textBody: "You can use otp or direct link",
});
};
const forget_password_generate_reset_token_from_db = async (req) => {
const email = req?.body?.email;
const account = await prisma.account.findUnique({
where: {
email,
},
});
// check if account exists
if (!account) {
throw new AppError("Account not found", 404);
}
// generate forget token
const verificationToken = jwtHelpers.generateToken({
email: email,
accountId: account.id,
}, configs.jwt.verified_token, "5m");
const verificationLink = `${configs.jwt.front_end_url}/verify/token?=${verificationToken}`;
await sendMail({
to: email,
subject: "Forget Password- Use this link for new password ",
htmlBody: `
<p>Your Reset Link: </p>
<p>${verificationLink}</p>
`,
textBody: "",
});
};
const reset_password_using_token_into_db = async (req) => {
const token = req?.body?.token;
const newPass = req?.body?.newPass;
let decoadeToken;
try {
decoadeToken = jwtHelpers.verifyToken(token, configs.jwt.verified_token);
}
catch (error) {
if (error?.message == "invalid signature") {
throw new AppError("Invalid Token", 403);
}
else if (error?.message == "jwt expired") {
throw new AppError("Link expired, please reset again", 403);
}
}
// check account
const account = await prisma.account.findUnique({
where: {
email: decoadeToken.email,
},
});
// check if account exists
if (!account) {
throw new AppError("Account not found", 404);
}
// change account password
const newHash = bcrypt.hashSync(newPass, 10);
await prisma.account.update({
where: {
id: account.id,
},
data: {
password: newHash,
},
});
// infuter user alart for changing password
return "";
};
export const account_services = {
create_account_into_db,
login_user_into_db,
get_user_account_from_db,
change_password_into_db,
verify_account_using_otp_into_db,
resend_otp_and_verification_link_from_db,
verify_account_using_link_into_db,
forget_password_generate_reset_token_from_db,
reset_password_using_token_into_db
};
+187
View File
@@ -0,0 +1,187 @@
export const accountSwaggerDocs = {
"/api/auth/sign-up": {
post: {
tags: ["account"],
summary: "Create new account",
description: "",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
email: "user@gmail.com",
password: "password",
shopName: "User",
}),
},
},
},
responses: {
201: { description: "account created successfully" },
500: { description: "Validation error or internal server error" },
},
},
},
"/api/auth/sign-in": {
post: {
tags: ["account"],
summary: "Sign In your account",
description: "",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
email: "user@gmail.com",
password: "password",
}),
},
},
},
responses: {
201: { description: "User signed in successfully" },
500: { description: "Validation error or internal server error" },
},
},
},
"/api/auth/verify-otp": {
put: {
tags: ["account"],
summary: "Verify OTP",
description: "",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
email: "user@gmail.com",
otp: "654321",
}),
},
},
},
responses: {
201: { description: "OTP verification successfully" },
500: { description: "Validation error or internal server error" },
},
},
},
"/api/auth/verify-link": {
put: {
tags: ["account"],
summary: "Verify Link",
description: "",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
token: "dsakfjasdkj",
}),
},
},
},
responses: {
201: { description: "Token verification successfully" },
500: { description: "Validation error or internal server error" },
},
},
},
"/api/auth/me": {
get: {
tags: ["account"],
summary: "Get me account",
description: "",
responses: {
200: { description: "account fetched successfully" },
401: { description: "unauthorized" },
},
},
},
"/api/auth/change-password": {
put: {
tags: ["account"],
summary: "Change Password",
description: "",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
oldPassword: "123456",
newPassword: "654321",
}),
},
},
},
responses: {
201: { description: "Passwrod change successfully" },
500: { description: "Validation error or internal server error" },
},
},
},
"/api/auth/resend-otp": {
put: {
tags: ["account"],
summary: "Resend OTP",
description: "",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
email: "user@gmail.com",
}),
},
},
},
responses: {
201: { description: "OTP resend successfully" },
500: { description: "Validation error or internal server error" },
},
},
},
"/api/auth/forget-password": {
put: {
tags: ["account"],
summary: "Forget Password",
description: "",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
email: "user@gmail.com",
}),
},
},
},
responses: {
201: { description: "Forget password successfully" },
500: { description: "Validation error or internal server error" },
},
},
},
"/api/auth/reset-password": {
put: {
tags: ["account"],
summary: "Reset Password",
description: "",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
token: "dkfjadskfds",
newPass: "newpass",
}),
},
},
},
responses: {
201: { description: "Password reset successfully" },
500: { description: "Validation error or internal server error" },
},
},
},
};
+37
View File
@@ -0,0 +1,37 @@
import z from "zod";
const sign_up = z.object({
email: z.string("Email is required."),
password: z.string("Password is required."),
shopName: z.string("Full name is required."),
});
const sing_in = z.object({
email: z.string("Email is required."),
password: z.string("Password is required."),
});
const change_password = z.object({
oldPassword: z.string("Old Password is required"),
newPassword: z.string("New Password is required"),
});
const verify_otp = z.object({
email: z.string("Email is required"),
otp: z.string("OTP is required"),
});
const verify_link = z.object({
token: z.string("Token is required "),
});
const resend_otp = z.object({
email: z.string("Email is required"),
});
const reset_pass = z.object({
token: z.string("Token is required"),
newPass: z.string("Password is required"),
});
export const account_validation = {
sign_up,
sing_in,
change_password,
verify_otp,
resend_otp,
verify_link,
reset_pass
};
+60
View File
@@ -0,0 +1,60 @@
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { order_service } from "./order.service.js";
const get_all_order = catchAsync(async (req, res) => {
const result = await order_service.get_all_order_from_db(req);
manageResponse(res, {
success: true,
statusCode: 200,
message: "All order fetched successfully.",
data: result,
meta: {},
});
});
const get_single_order = catchAsync(async (req, res) => {
const result = await order_service.get_single_order_from_db(req);
manageResponse(res, {
success: true,
statusCode: 200,
message: "Single order fetched successfully.",
data: result,
meta: {},
});
});
const create_order = catchAsync(async (req, res) => {
const result = await order_service.create_order_into_db(req);
manageResponse(res, {
success: true,
statusCode: 200,
message: "order created successfully.",
data: result,
meta: {},
});
});
const update_order = catchAsync(async (req, res) => {
const result = await order_service.update_order_into_db(req);
manageResponse(res, {
success: true,
statusCode: 200,
message: "order updated successfully.",
data: result,
meta: {},
});
});
const delete_order = catchAsync(async (req, res) => {
const result = await order_service.delete_order_from_db(req);
manageResponse(res, {
success: true,
statusCode: 200,
message: "order deleted successfully.",
data: result,
meta: {},
});
});
export const order_controller = {
get_all_order,
get_single_order,
create_order,
update_order,
delete_order,
};
+12
View File
@@ -0,0 +1,12 @@
import { Router } from "express";
import RequestValidator from "../../middlewares/request_validator.js";
import { order_controller } from "./order.controller.js";
import { order_validations } from "./order.validation.js";
import auth from "../../middlewares/auth.js";
const router = Router();
router.get("/", order_controller.get_all_order);
router.post("/", RequestValidator(order_validations.create_order), order_controller.create_order);
router.get("/:id", order_controller.get_single_order);
router.patch("/:id", auth("ADMIN"), RequestValidator(order_validations.update_order), order_controller.update_order);
router.delete("/:id", order_controller.delete_order);
export default router;
+163
View File
@@ -0,0 +1,163 @@
import { configs } from "../../configs/index.js";
import { prisma } from "../../lib/prisma.js";
import { orderEmailQueue } from "../../queues/email/order/order.email.queue.js";
import { AppError } from "../../utils/app_error.js";
import paginationHelper from "../../utils/pagination_helper.js";
const get_all_order_from_db = async (req) => {
// define your own login here
const search = req.query.search;
const customerName = req.query.customerName;
const productName = req.query.productName;
// for date filter
const startDate = req.query.startDate;
const endDate = req.query.endDate;
const status = req.query.status || undefined;
const { page, limit, skip, sortBy, sortOrder } = paginationHelper(req.query);
const andCondition = [];
if (search) {
andCondition.push({
OR: [
{
productName: {
contains: search,
mode: "insensitive",
},
},
],
});
}
if (customerName) {
andCondition.push({
OR: [
{
customerName: {
contains: customerName,
mode: "insensitive",
},
},
],
});
}
if (productName) {
andCondition.push({
OR: [
{
productName: {
contains: productName,
mode: "insensitive",
},
},
],
});
}
if (status) {
andCondition.push({
OR: [
{
status: {
contains: status,
mode: "insensitive",
},
},
],
});
}
// for date filter
const dateFilter = {};
if (startDate) {
const start = new Date(startDate);
start.setHours(0, 0, 0, 0);
dateFilter.gte = start;
}
if (endDate) {
const end = new Date(endDate);
end.setHours(23, 59, 59, 999);
dateFilter.lte = end;
}
if (Object.keys(dateFilter).length > 0) {
andCondition.push({
createdAt: dateFilter,
});
}
const getAllOrders = await prisma.order.findMany({
take: limit,
skip,
where: {
AND: andCondition,
},
orderBy: {
[sortBy]: sortOrder,
},
});
const result = await prisma.order.count({
where: {
AND: andCondition,
},
});
return {
data: getAllOrders,
pagination: {
total: result,
page,
limit,
totalPages: Math.ceil(result / limit),
},
};
};
const get_single_order_from_db = async (req) => {
// define your own login here
const { id } = req.params;
const result = await prisma.order.findUnique({ where: { id } });
return result;
};
const create_order_into_db = async (req) => {
const payload = req?.body;
console.log(payload);
payload.status = "INITIATED";
payload.paymentType = "COD";
// nwo init order
const result = await prisma.order.create({ data: payload });
// if email exist sent tracking link
if (payload.customerEmail) {
const trackingLink = `${configs.jwt.front_end_url}/track-order/${result.id}`;
await orderEmailQueue.add("order-email-queue", {
email: payload.customerEmail,
subject: "Order Tracking",
textBody: `Your order has been created. Track your order here: ${trackingLink}`,
htmlBody: `<p>Your order has been created. Track your order here: <a href="${trackingLink}">Track Order</a></p>`,
});
}
return result;
};
const update_order_into_db = async (req) => {
// define your own login here
const user = req.user;
console.log(user);
if (user?.role !== "ADMIN") {
throw new AppError("You are not authorized to perform this action", 403);
}
const { id } = req.params;
const isProductExist = await prisma.order.findUnique({ where: { id } });
if (!isProductExist) {
throw new AppError("Order is not found", 404);
}
const result = await prisma.order.update({ where: { id }, data: req.body });
return result;
};
const delete_order_from_db = async (req) => {
// define your own login here
const { id } = req.params;
const user = req.user;
if (user?.role !== "ADMIN") {
throw new AppError("You are not authorized to perform this action", 403);
}
const result = await prisma.order.delete({ where: { id } });
return result;
};
export const order_service = {
get_all_order_from_db,
get_single_order_from_db,
create_order_into_db,
update_order_into_db,
delete_order_from_db,
};
+165
View File
@@ -0,0 +1,165 @@
export const orderSwaggerDocs = {
"/api/order": {
post: {
tags: ["order"],
summary: "Create new order",
description: ` INITIATED
CONFIRMED
ONGOING
DELIVERED
CANCELLED`,
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
shopAccountId: "",
productPrice: 1500,
productQuantity: 2,
productName: "Wireless Mouse",
customerName: "Rahim Uddin",
customerPhone: "+8801712345678",
customerEmail: "softvence.abumahid@gmail.com",
customerAddress: "Rangpur, Bangladesh",
customerNote: "Please deliver between 3-5 PM",
}),
},
},
},
responses: {
201: { description: "order created successfully" },
500: { description: "Validation error or internal server error" },
},
},
get: {
tags: ["order"],
summary: "Get all order",
description: "",
parameters: [
{
name: "page",
in: "query",
required: false,
schema: { type: "number" },
},
{
name: "limit",
in: "query",
required: false,
schema: { type: "number" },
},
{
name: "search",
in: "query",
required: false,
schema: { type: "string" },
},
{
name: "customerName",
in: "query",
required: false,
schema: { type: "string" },
},
{
name: "productName",
in: "query",
required: false,
schema: { type: "string" },
},
{
name: "status",
in: "query",
required: false,
schema: { type: "string" },
},
{
name: "date",
in: "query",
required: false,
schema: { type: "string" },
},
{
name: "startDate",
in: "query",
required: false,
schema: { type: "string", format: "date" },
example: "2026-04-01",
},
{
name: "endDate",
in: "query",
required: false,
schema: { type: "string", format: "date" },
example: "2026-04-31",
},
],
responses: {
200: { description: "order fetched successfully" },
401: { description: "unauthorized" },
},
},
},
"/api/order/{id}": {
get: {
tags: ["order"],
summary: "Get single order",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
200: { description: "order fetched successfully" },
401: { description: "unauthorized" },
},
},
patch: {
tags: ["order"],
summary: "Update order -(Admin route)",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
status: "INITIATED",
}), // put your request body
},
},
},
responses: {
200: { description: "order updated successfully" },
500: { description: "Validation error or internal server error" },
},
},
delete: {
tags: ["order"],
summary: "Delete order",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
200: { description: "order delete successfully" },
401: { description: "unauthorized" },
},
},
},
};
+19
View File
@@ -0,0 +1,19 @@
import { z } from "zod";
const create_order = z.object({
shopAccountId: z.string(),
productPrice: z.number(),
productQuantity: z.number(),
productName: z.string(),
customerName: z.string(),
customerPhone: z.string(),
customerEmail: z.string().optional(),
customerAddress: z.string(),
customerNote: z.string().optional()
});
const update_order = z.object({
status: z.string().optional()
});
export const order_validations = {
create_order,
update_order,
};
+60
View File
@@ -0,0 +1,60 @@
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { plan_service } from "./plan.service.js";
const get_all_plan = catchAsync(async (req, res) => {
const result = await plan_service.get_all_plan_from_db(req);
manageResponse(res, {
success: true,
statusCode: 200,
message: "All plan fetched successfully.",
data: result,
meta: {},
});
});
const get_single_plan = catchAsync(async (req, res) => {
const result = await plan_service.get_single_plan_from_db(req);
manageResponse(res, {
success: true,
statusCode: 200,
message: "Single plan fetched successfully.",
data: result,
meta: {},
});
});
const create_plan = catchAsync(async (req, res) => {
const result = await plan_service.create_plan_into_db(req);
manageResponse(res, {
success: true,
statusCode: 200,
message: "plan created successfully.",
data: result,
meta: {},
});
});
const update_plan = catchAsync(async (req, res) => {
const result = await plan_service.update_plan_into_db(req);
manageResponse(res, {
success: true,
statusCode: 200,
message: "plan updated successfully.",
data: result,
meta: {},
});
});
const delete_plan = catchAsync(async (req, res) => {
const result = await plan_service.delete_plan_from_db(req);
manageResponse(res, {
success: true,
statusCode: 200,
message: "plan deleted successfully.",
data: result,
meta: {},
});
});
export const plan_controller = {
get_all_plan,
get_single_plan,
create_plan,
update_plan,
delete_plan,
};
+12
View File
@@ -0,0 +1,12 @@
import { Router } from "express";
import auth from "../../middlewares/auth.js";
import RequestValidator from "../../middlewares/request_validator.js";
import { plan_controller } from "./plan.controller.js";
import { plan_validations } from "./plan.validation.js";
const router = Router();
router.get("/", plan_controller.get_all_plan);
router.post("/", RequestValidator(plan_validations.create_plan), auth("ADMIN"), plan_controller.create_plan);
router.get("/:id", plan_controller.get_single_plan);
router.patch("/:id", RequestValidator(plan_validations.update_plan), auth("ADMIN"), plan_controller.update_plan);
router.delete("/:id", auth("ADMIN"), plan_controller.delete_plan);
export default router;
+68
View File
@@ -0,0 +1,68 @@
import { prisma } from "../../lib/prisma.js";
import { AppError } from "../../utils/app_error.js";
const get_all_plan_from_db = async (req) => {
// define your own login here
const result = await prisma.plan.findMany();
return result;
};
const get_single_plan_from_db = async (req) => {
// define your own login here
const { id } = req.params;
const result = await prisma.plan.findUnique({
where: {
id: id
}
});
return result;
};
const create_plan_into_db = async (req) => {
// define your own login here
const user = req?.user;
if (user?.role !== "ADMIN") {
throw new AppError("You dont have permission to create plan information.!!!", 401);
}
const result = await prisma.plan.create({ data: req.body });
return result;
};
const update_plan_into_db = async (req) => {
// define your own login here
const { id } = req.params;
const user = req.user;
if (user?.role !== "ADMIN") {
throw new AppError("You dont have permission to update plan information.!!!", 401);
}
const isPlanExist = await prisma.plan.findFirst({
where: {
id: id
}
});
if (!isPlanExist) {
throw new AppError("The plan is not available!!!", 404);
}
const result = await prisma.plan.update({
where: {
id: isPlanExist.id
},
data: {
...req.body
}
});
return result;
};
const delete_plan_from_db = async (req) => {
// define your own login here
const { id } = req.params;
const user = req.user;
if (user?.role !== "ADMIN") {
throw new AppError("You dont have permission to delete plan information.!!!", 401);
}
const result = await prisma.plan.delete({ where: { id: id } });
return result;
};
export const plan_service = {
get_all_plan_from_db,
get_single_plan_from_db,
create_plan_into_db,
update_plan_into_db,
delete_plan_from_db,
};
+125
View File
@@ -0,0 +1,125 @@
export const planSwaggerDocs = {
"/api/plan": {
post: {
tags: ["plan"],
summary: "Create new plan",
description: "",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
"planName": "PRO Plan",
"price": 12,
"planType": "PRO",
"planDesc": "The plan is only for pro users",
"planFeatures": {
"storage": "10GB",
"projects": 5,
"support": "Email Support"
}
}), // put your request body
},
},
},
responses: {
201: { description: "plan created successfully" },
500: { description: "Validation error or internal server error" },
},
},
get: {
tags: ["plan"],
summary: "Get all plan",
description: "",
parameters: [
{
name: "page",
in: "query",
required: false,
schema: { type: "number" },
},
{
name: "limit",
in: "query",
required: false,
schema: { type: "number" },
},
],
responses: {
200: { description: "plan fetched successfully" },
401: { description: "unauthorized" },
},
},
},
"/api/plan/{id}": {
get: {
tags: ["plan"],
summary: "Get single plan",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
200: { description: "plan fetched successfully" },
401: { description: "unauthorized" },
},
},
patch: {
tags: ["plan"],
summary: "Update plan",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
"planName": "PRO Plan",
"price": 12,
"planType": "PRO",
"planDesc": "The plan is only for pro users",
"planFeatures": {
"storage": "10GB",
"projects": 5,
"support": "Email Support"
}
}), // put your request body
},
},
},
responses: {
200: { description: "plan updated successfully" },
500: { description: "Validation error or internal server error" },
},
},
delete: {
tags: ["plan"],
summary: "Delete plan",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
200: { description: "plan delete successfully" },
401: { description: "unauthorized" },
},
},
},
};
+33
View File
@@ -0,0 +1,33 @@
import { z } from "zod";
const create_plan = z.object({
planName: z.string("Enter the plan name..."),
price: z.number("Enter the plan price..."),
planType: z.enum(["FREE", "STANDARD", "PRO"]),
planDesc: z.string("Enter the plan description..."),
planFeatures: z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.array(z.any()),
z.record(z.string(), z.any())
])
});
const update_plan = z.object({
planName: z.string(),
price: z.number(),
planType: z.enum(["FREE", "STANDARD", "PRO"]),
planDesc: z.string().optional(),
planFeatures: z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.array(z.any()),
z.record(z.string(), z.any())
])
});
export const plan_validations = {
create_plan,
update_plan,
};
+15
View File
@@ -0,0 +1,15 @@
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { profile_service } from "./profile.service.js";
const update_profile = catchAsync(async (req, res) => {
const result = await profile_service.update_profile_into_db(req);
manageResponse(res, {
success: true,
statusCode: 200,
message: "profile updated successfully.",
data: result,
});
});
export const profile_controller = {
update_profile,
};
+12
View File
@@ -0,0 +1,12 @@
import { Router } from "express";
import RequestValidator from "../../middlewares/request_validator.js";
import { profile_controller } from "./profile.controller.js";
import { profile_validations } from "./profile.validation.js";
import auth from "../../middlewares/auth.js";
import uploader from "../../middlewares/uploader.js";
const router = Router();
router.patch("/", auth("USER"), uploader.single("file"), (req, res, next) => {
req.body = JSON.parse(req?.body?.data);
next();
}, RequestValidator(profile_validations.update_profile), profile_controller.update_profile);
export default router;
+23
View File
@@ -0,0 +1,23 @@
import uploadCloud from "../../utils/cloudinary.js";
import { prisma } from "../../lib/prisma.js";
const update_profile_into_db = async (req) => {
const user = req?.user;
const payload = req?.body;
const file = req?.file;
console.log(payload);
// check file and upload to cloud
if (file) {
const cloudRes = await uploadCloud(file);
payload.profilePhoto = cloudRes?.secure_url;
}
const result = await prisma.profile.update({
where: {
accountId: user.accountId,
},
data: payload,
});
return result;
};
export const profile_service = {
update_profile_into_db,
};
+34
View File
@@ -0,0 +1,34 @@
export const profileSwaggerDocs = {
"/api/profile": {
patch: {
tags: ["profile"],
summary: "Update profile",
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
properties: {
data: {
type: "object",
properties: {
fullName: { type: "string" },
},
},
file: {
type: "string",
format: "binary",
},
},
},
},
},
},
responses: {
200: { description: "profile updated successfully" },
500: { description: "Validation error or internal server error" },
},
},
},
};
+7
View File
@@ -0,0 +1,7 @@
import { z } from "zod";
const update_profile = z.object({
fullName: z.string().optional(),
});
export const profile_validations = {
update_profile,
};
+80
View File
@@ -0,0 +1,80 @@
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { support_service } from "./support.service.js";
const createSupport = catchAsync(async (req, res) => {
const id = req?.user?.accountId;
const data = {
...req.body,
storeAccountId: id
};
const result = await support_service.createSupportIntoDB(data);
manageResponse(res, {
success: true,
statusCode: 200,
message: "support created successfully.",
data: result,
meta: {},
});
});
const getAllSupport = catchAsync(async (req, res) => {
const role = req?.user?.role;
const id = req?.user?.accountId;
const search = req?.query?.search;
const type = req?.query?.type;
const status = req?.query?.status;
const result = await support_service.getAllSupportFromDB(id, role, search, type, status);
manageResponse(res, {
success: true,
statusCode: 200,
message: "All support fetched successfully.",
data: result,
meta: {},
});
});
const get_single_support = catchAsync(async (req, res) => {
const { id } = req.params;
const userId = req?.user?.accountId;
const role = req?.user?.role;
const result = await support_service.getSingleSupportFromDB(id, userId, role);
manageResponse(res, {
success: true,
statusCode: 200,
message: "Single support fetched successfully.",
data: result,
meta: {},
});
});
const update_support = catchAsync(async (req, res) => {
const { id } = req.params;
const userId = req?.user?.accountId;
const role = req?.user?.role;
const data = req.body;
const result = await support_service.updateSupportIntoDB(id, userId, role, data);
manageResponse(res, {
success: true,
statusCode: 200,
message: "support updated successfully.",
data: result,
meta: {},
});
});
const delete_support = catchAsync(async (req, res) => {
const { id } = req.params;
const userId = req?.user?.accountId;
const role = req?.user?.role;
const result = await support_service.deleteSupportFromDB(id, userId, role);
manageResponse(res, {
success: true,
statusCode: 200,
message: "support deleted successfully.",
data: result,
meta: {},
});
});
export const support_controller = {
createSupport,
getAllSupport,
get_single_support,
update_support,
delete_support,
};
+12
View File
@@ -0,0 +1,12 @@
import { Router } from "express";
import RequestValidator from "../../middlewares/request_validator.js";
import { support_controller } from "./support.controller.js";
import { support_validations } from "./support.validation.js";
import auth from "../../middlewares/auth.js";
const router = Router();
router.get("/", auth("ADMIN", "USER"), support_controller.getAllSupport);
router.post("/", auth("ADMIN", "USER"), RequestValidator(support_validations.create_support), support_controller.createSupport);
router.get("/:id", auth("ADMIN", "USER"), support_controller.get_single_support);
router.patch("/:id", auth("ADMIN", "USER"), RequestValidator(support_validations.update_support), support_controller.update_support);
router.delete("/:id", auth("ADMIN", "USER"), support_controller.delete_support);
export default router;
+100
View File
@@ -0,0 +1,100 @@
import { prisma } from "../../lib/prisma.js";
import { AppError } from "../../utils/app_error.js";
const createSupportIntoDB = async (payload) => {
const result = await prisma.support.create({ data: payload });
return result;
};
const getAllSupportFromDB = async (user_id, role, search, type, status) => {
const andCondition = [];
if (search) {
andCondition.push({
OR: [
{
issueName: {
contains: search,
mode: "insensitive",
},
},
{
description: {
contains: search,
mode: "insensitive",
},
},
],
});
}
if (type) {
andCondition.push({
type: type,
});
}
if (status) {
andCondition.push({
status: status,
});
}
if (role !== "ADMIN") {
andCondition.push({
storeAccountId: user_id,
});
}
const whereCondition = andCondition.length > 0 ? { AND: andCondition } : {};
const result = await prisma.support.findMany({
where: whereCondition,
orderBy: {
createdAt: "desc",
},
});
return result;
};
const getSingleSupportFromDB = async (id, userId, role) => {
const support = await prisma.support.findUnique({
where: { id },
});
if (!support) {
throw new AppError("Support not found", 404);
}
if (role !== "ADMIN" && support.storeAccountId !== userId) {
throw new AppError("You are not authorized", 403);
}
return support;
};
const updateSupportIntoDB = async (id, userId, role, payload) => {
const support = await prisma.support.findUnique({
where: { id },
});
if (!support) {
throw new AppError("Support not found", 404);
}
if (role !== "ADMIN" && support.storeAccountId !== userId) {
throw new AppError("You are not authorized", 403);
}
const result = await prisma.support.update({
where: { id },
data: payload,
});
return result;
};
const deleteSupportFromDB = async (id, userId, role) => {
const support = await prisma.support.findUnique({
where: { id },
});
if (!support) {
throw new AppError("Support not found", 404);
}
if (role !== "ADMIN" && support.storeAccountId !== userId) {
throw new AppError("You are not authorized", 403);
}
const result = await prisma.support.delete({
where: { id }
});
return result;
};
export const support_service = {
createSupportIntoDB,
getAllSupportFromDB,
getSingleSupportFromDB,
updateSupportIntoDB,
deleteSupportFromDB,
};
+109
View File
@@ -0,0 +1,109 @@
export const supportSwaggerDocs = {
"/api/support": {
post: {
tags: ["support"],
summary: "Create new support",
description: "",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
"issueName": "Your issue name",
"description": "Issue description",
"type": "Issue Type"
}), // put your request body
},
},
},
responses: {
201: { description: "support created successfully" },
500: { description: "Validation error or internal server error" },
},
},
get: {
tags: ["support"],
summary: "Get all support",
description: "",
parameters: [
{
name: "page",
in: "query",
required: false,
schema: { type: "number" },
},
{
name: "limit",
in: "query",
required: false,
schema: { type: "number" },
},
],
responses: {
200: { description: "support fetched successfully" },
401: { description: "unauthorized" },
},
},
},
"/api/support/{id}": {
get: {
tags: ["support"],
summary: "Get single support",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
200: { description: "support fetched successfully" },
401: { description: "unauthorized" },
},
},
patch: {
tags: ["support"],
summary: "Update support",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({}), // put your request body
},
},
},
responses: {
200: { description: "support updated successfully" },
500: { description: "Validation error or internal server error" },
},
},
delete: {
tags: ["support"],
summary: "Delete support",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
200: { description: "support delete successfully" },
401: { description: "unauthorized" },
},
},
},
};
+28
View File
@@ -0,0 +1,28 @@
import { z } from "zod";
const create_support = z.object({
issueName: z.string().min(1, "issueName is required"),
description: z.string().min(1, "description is required"),
type: z.enum([
"TECHNICAL",
"BILLING",
"DOMAIN",
"TEMPLATE",
"PAYMENT",
"ACCOUNT",
"FEATURE_REQUEST",
"BUG",
"OTHER",
]),
});
const update_support = z.object({
issueName: z.string().optional(),
description: z.string().optional(),
type: z.enum(["BUG", "PAYMENT", "ACCOUNT", "OTHER"]).optional(),
status: z.enum(["OPEN", "IN_PROGRESS", "RESOLVED", "REJECTED"]).optional(),
resolvedBy: z.string().optional(),
resolvedAt: z.coerce.date().optional(),
});
export const support_validations = {
create_support,
update_support,
};
+7
View File
@@ -0,0 +1,7 @@
import { Redis } from "ioredis";
import { configs } from "../configs/index.js";
export const redisConnection = new Redis(configs.redis_url, {
tls: {},
maxRetriesPerRequest: null,
enableReadyCheck: false,
});
+14
View File
@@ -0,0 +1,14 @@
import { otpTemplate } from "../../templates/otpTemplate.js";
import sendMail from "../../utils/mail_sender.js";
// email.processor.ts
export const emailProcessor = async (job) => {
const payload = job.data;
await sendMail({
to: payload.email,
subject: payload.subject,
htmlBody: otpTemplate(payload),
textBody: payload.textBody || "",
name: payload.name,
});
console.log("Sending email job complete:", job.id);
};
+6
View File
@@ -0,0 +1,6 @@
// email.queue.ts
import { Queue } from "bullmq";
import { redisConnection } from "../connection.js";
export const emailQueue = new Queue("email-queue", {
connection: redisConnection,
});
+7
View File
@@ -0,0 +1,7 @@
// email.worker.ts
import { Worker } from "bullmq";
import { redisConnection } from "../connection.js";
import { emailProcessor } from "./email.processor.js";
export const emailWorker = new Worker("email-queue", async (job) => emailProcessor(job), {
connection: redisConnection,
});
+12
View File
@@ -0,0 +1,12 @@
import sendMail from "../../../utils/mail_sender.js";
// email.processor.ts
export const orderEmailProcessor = async (job) => {
const payload = job.data;
await sendMail({
to: payload.email,
subject: payload.subject,
htmlBody: payload.htmlBody,
textBody: payload.textBody,
});
console.log("Sending email job complete:", job.id);
};
+5
View File
@@ -0,0 +1,5 @@
import { Queue } from "bullmq";
import { redisConnection } from "../../connection.js";
export const orderEmailQueue = new Queue("order-email-queue", {
connection: redisConnection,
});
+7
View File
@@ -0,0 +1,7 @@
// email.worker.ts
import { Worker } from "bullmq";
import { redisConnection } from "../../connection.js";
import { orderEmailProcessor } from "./order.email.processor.js";
export const emailWorker = new Worker("order-email-queue", async (job) => orderEmailProcessor(job), {
connection: redisConnection,
});
+3
View File
@@ -0,0 +1,3 @@
import "./email/email.worker.js";
import "./email/order/order.email.worker.js";
console.log("Workers running...");
+80
View File
@@ -0,0 +1,80 @@
export const otpTemplate = (payload) => {
return `
<div
style="
margin: 0;
padding: 0;
background-color: #f4f6f8;
font-family: Arial, Helvetica, sans-serif;
"
>
<table
align="center"
width="100%"
cellpadding="0"
cellspacing="0"
style="
max-width: 600px;
margin: auto;
background: #ffffff;
border-radius: 8px;
overflow: hidden;
"
>
<tr>
<td style="padding: 30px; color: #333333">
<p style="margin: 0 0 20px 0; font-size: 15px">
Use the following One-Time Password (OTP) to complete your
verification:
</p>
<!-- OTP Box -->
<div style="text-align: center; margin: 25px 0">
<span
style="
display: inline-block;
background: #f1f5f9;
padding: 15px 25px;
font-size: 24px;
letter-spacing: 4px;
font-weight: bold;
color: #111827;
border-radius: 6px;
"
>
${payload.otp}
</span>
</div>
<p style="margin: 0 0 20px 0; font-size: 13px; color: #6b7280">
This OTP will expire in <strong>5 minutes</strong>.
</p>
<!-- Divider -->
<hr
style="
border: none;
border-top: 1px solid #e5e7eb;
margin: 25px 0;
"
/>
<!-- Verification Link -->
<p style="margin: 0 0 10px 0; font-size: 15px">
Or verify using this link:
</p>
<p style="word-break: break-all; font-size: 14px">
<a
href="${payload.verificationLink}"
style="color: #4f46e5; text-decoration: none"
>
${payload.verificationLink}
</a>
</p>
</td>
</tr>
</table>
</div>
`;
};
+15
View File
@@ -0,0 +1,15 @@
import jwt from "jsonwebtoken";
const generateToken = (payload, secret, expiresIn) => {
const token = jwt.sign(payload, secret, {
algorithm: "HS256",
expiresIn: expiresIn,
});
return token;
};
const verifyToken = (token, secret) => {
return jwt.verify(token, secret);
};
export const jwtHelpers = {
generateToken,
verifyToken,
};
+13
View File
@@ -0,0 +1,13 @@
export class AppError extends Error {
statusCode;
constructor(message, statusCode, stack = '') {
super(message);
this.statusCode = statusCode;
if (stack) {
this.stack = stack;
}
else {
Error.captureStackTrace(this, this.constructor);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
const catchAsync = (fn) => {
return async (req, res, next) => {
try {
await fn(req, res, next);
}
catch (error) {
next(error);
}
};
};
export default catchAsync;
+23
View File
@@ -0,0 +1,23 @@
import { v2 as cloudinary } from 'cloudinary';
import fs from 'fs';
import { configs } from '../configs/index.js';
// Configuration
cloudinary.config({
cloud_name: configs.cloudinary.cloud_name,
api_key: configs.cloudinary.cloud_api_key,
api_secret: configs.cloudinary.cloud_api_secret,
});
const uploadCloud = async (file) => {
return new Promise((resolve, reject) => {
cloudinary.uploader.upload(file.path, (error, result) => {
fs.unlinkSync(file.path);
if (error) {
reject(error);
}
else {
resolve(result);
}
});
});
};
export default uploadCloud;
+113
View File
@@ -0,0 +1,113 @@
import nodemailer from 'nodemailer';
import { configs } from '../configs/index.js';
const transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 465,
secure: true, // true for 465, false for other ports
auth: {
user: configs.email.app_email,
pass: configs.email.app_password,
},
});
// ✅ Email Sender Function
const sendMail = async (payload) => {
const info = await transporter.sendMail({
from: 'info@digitalcreditai.com',
to: payload.to,
subject: payload.subject,
text: payload.textBody,
html: `
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Welcome Email</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Fallback styles for unsupported clients (some email clients ignore <style> tags) */
@media only screen and (max-width: 600px) {
.container {
padding: 20px !important;
}
.btn {
padding: 12px 18px !important;
font-size: 16px !important;
}
}
</style>
</head>
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif">
<div
style="
max-width: 600px;
margin: 40px auto;
background-color: #f4f4f4;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
"
class="container"
>
<div style="font-size: 16px; color: #555555; line-height: 1.6">
<p style="margin-bottom: 30px">
Hi <strong>${payload?.name || ""}</strong>,
</p>
${payload?.htmlBody}
<div style="margin-top: 60px; text-align: center">
<img
style="width: 50px; height: 50px; border-radius: 50%"
src="https://i.ibb.co.com/RkFJjPWg/quick-launch-1.png"
alt="Quick Launch"
/>
<p style="font-size: 12px">The Support Team</p>
<h3>Quick Launch</h3>
</div>
</div>
<p
style="
font-size: 14px;
color: #999999;
margin-top: 20px;
margin-bottom: 10px;
text-align: center;
"
>
This is an automated message — please do not reply to this email.
<br />
If you need assistance, feel free to contact our support team.
<br /><br />
Thank you for choosing us!
</p>
<hr />
<div
style="
text-align: center;
font-size: 12px;
color: #999999;
margin-top: 20px;
"
>
&copy; 2026 to {{year}} Quick Launch. All rights reserved.
</div>
</div>
</body>
</html>
`,
});
return info;
};
export default sendMail;
+9
View File
@@ -0,0 +1,9 @@
const manageResponse = (res, payload) => {
res.status(payload.statusCode).json({
success: payload.success,
message: payload.message,
data: payload.data || undefined || null,
meta: payload.meta || undefined || null
});
};
export default manageResponse;
+23
View File
@@ -0,0 +1,23 @@
export const otpGenerator = () => {
const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const lower = "abcdefghijklmnopqrstuvwxyz";
const numbers = "0123456789";
const symbols = "@#$%&*!?";
const allChars = upper + lower + numbers + symbols;
let otp = "";
// Ensure at least one character from each set
otp += upper[Math.floor(Math.random() * upper.length)];
otp += lower[Math.floor(Math.random() * lower.length)];
otp += numbers[Math.floor(Math.random() * numbers.length)];
otp += symbols[Math.floor(Math.random() * symbols.length)];
// Fill the rest randomly
for (let i = otp.length; i < 6; i++) {
otp += allChars[Math.floor(Math.random() * allChars.length)];
}
// Shuffle to remove predictable order
otp = otp
.split("")
.sort(() => Math.random() - 0.5)
.join("");
return otp;
};
+15
View File
@@ -0,0 +1,15 @@
const paginationHelper = (options) => {
const page = Number(options?.page) || 1;
const limit = Number(options?.limit) || 10;
const skip = (page - 1) * limit;
const sortBy = options?.sortBy || "createdAt";
const sortOrder = options?.sortOrder || "desc";
return {
page,
limit,
skip,
sortBy,
sortOrder
};
};
export default paginationHelper;
+16
View File
@@ -0,0 +1,16 @@
import { Router } from "express";
import accountRouter from "./app/modules/account/account.route.js";
import orderRoute from "./app/modules/order/order.route.js";
import planRoute from "./app/modules/plan/plan.route.js";
import profileRoute from "./app/modules/profile/profile.route.js";
import supportRoute from "./app/modules/support/support.route.js";
const appRouter = Router();
const moduleRoutes = [
{ path: "/order", route: orderRoute },
{ path: "/support", route: supportRoute },
{ path: "/plan", route: planRoute },
{ path: "/profile", route: profileRoute },
{ path: "/auth", route: accountRouter },
];
moduleRoutes.forEach((route) => appRouter.use(route.path, route.route));
export default appRouter;
+19
View File
@@ -0,0 +1,19 @@
import app from "./app.js";
import { configs } from "./app/configs/index.js";
import { prisma } from "./app/lib/prisma.js";
import "./app/queues/worker.js";
async function main() {
try {
app.listen(configs.port, async () => {
await prisma.$connect();
console.log(`Server is running on port ${configs.port}`);
});
}
catch (error) {
console.error("Error starting server:", error);
}
}
main().catch((error) => {
console.error("Error in main function:", error);
process.exit(1);
});
+48
View File
@@ -0,0 +1,48 @@
import { fileURLToPath } from "node:url";
import path from "path";
import { configs } from "./app/configs/index.js";
import { accountSwaggerDocs } from "./app/modules/account/account.swagger.js";
import { orderSwaggerDocs } from "./app/modules/order/order.swagger.js";
import { planSwaggerDocs } from "./app/modules/plan/plan.swagger.js";
import { profileSwaggerDocs } from "./app/modules/profile/profile.swagger.js";
import { supportSwaggerDocs } from "./app/modules/support/support.swagger.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const swaggerOptions = {
definition: {
openapi: "3.0.0",
info: {
title: "API Doc - Build with exp-node-server",
version: "1.0.0",
description: "Express + Prisma API with auto-generated Swagger docs",
},
paths: {
...accountSwaggerDocs,
...planSwaggerDocs,
...orderSwaggerDocs,
...profileSwaggerDocs,
...supportSwaggerDocs,
},
servers: configs.env === "production"
? [{ url: "https://quicklunch-server.onrender.com" }, { url: "http://localhost:5000" }]
: [{ url: "http://localhost:5000" }, { url: "https://quicklunch-server.onrender.com" }],
components: {
securitySchemes: {
AuthorizationToken: {
type: "apiKey",
in: "header",
name: "Authorization",
description: "Put your accessToken here ",
},
},
},
security: [
{
AuthorizationToken: [],
},
],
},
apis: [
path.join(__dirname, configs.env === "production" ? "./**/*.js" : "./**/*.ts"),
],
};
+2
View File
@@ -20,6 +20,7 @@
"cloudinary": "^2.7.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dayjs": "^1.11.20",
"dotenv": "^17.3.1",
"express": "^5.1.0",
"ioredis": "^5.10.1",
@@ -29,6 +30,7 @@
"pg": "^8.20.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"typescript": "^6.0.3",
"zod": "^4.1.12"
},
"devDependencies": {
@@ -0,0 +1,30 @@
-- CreateEnum
CREATE TYPE "T_SupportType" AS ENUM ('TECHNICAL', 'BILLING', 'DOMAIN', 'TEMPLATE', 'PAYMENT', 'ACCOUNT', 'FEATURE_REQUEST', 'BUG', 'OTHER');
-- CreateEnum
CREATE TYPE "T_SupportStatus" AS ENUM ('OPEN', 'IN_PROGRESS', 'RESOLVED', 'CLOSED');
-- CreateTable
CREATE TABLE "Support" (
"id" TEXT NOT NULL,
"issueName" TEXT NOT NULL,
"description" TEXT NOT NULL,
"type" "T_SupportType" NOT NULL,
"status" "T_SupportStatus" NOT NULL DEFAULT 'OPEN',
"resolvedBy" TEXT,
"resolvedAt" TIMESTAMP(3),
"storeAccountId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Support_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Support_storeAccountId_idx" ON "Support"("storeAccountId");
-- CreateIndex
CREATE INDEX "Support_storeAccountId_status_idx" ON "Support"("storeAccountId", "status");
-- AddForeignKey
ALTER TABLE "Support" ADD CONSTRAINT "Support_storeAccountId_fkey" FOREIGN KEY ("storeAccountId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,16 @@
/*
Warnings:
- The values [CLOSED] on the enum `T_SupportStatus` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "T_SupportStatus_new" AS ENUM ('OPEN', 'IN_PROGRESS', 'RESOLVED', 'REJECTED');
ALTER TABLE "public"."Support" ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE "Support" ALTER COLUMN "status" TYPE "T_SupportStatus_new" USING ("status"::text::"T_SupportStatus_new");
ALTER TYPE "T_SupportStatus" RENAME TO "T_SupportStatus_old";
ALTER TYPE "T_SupportStatus_new" RENAME TO "T_SupportStatus";
DROP TYPE "public"."T_SupportStatus_old";
ALTER TABLE "Support" ALTER COLUMN "status" SET DEFAULT 'OPEN';
COMMIT;
@@ -0,0 +1,207 @@
-- CreateTable
CREATE TABLE "Template" (
"id" TEXT NOT NULL,
"language" TEXT NOT NULL,
"deliveryCharge" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Template_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Banner" (
"id" TEXT NOT NULL,
"isVisible" BOOLEAN NOT NULL,
"bannerTitle" TEXT NOT NULL,
"bannerDesc" TEXT NOT NULL,
"bannerImage" TEXT NOT NULL,
"templateId" TEXT NOT NULL,
CONSTRAINT "Banner_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Address" (
"id" TEXT NOT NULL,
"isVisible" BOOLEAN NOT NULL,
"phoneNumber" TEXT NOT NULL,
"shopLocation" TEXT NOT NULL,
"shopEmail" TEXT NOT NULL,
"templateId" TEXT NOT NULL,
CONSTRAINT "Address_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Ingredient" (
"id" TEXT NOT NULL,
"isVisible" BOOLEAN NOT NULL,
"templateId" TEXT NOT NULL,
CONSTRAINT "Ingredient_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ingredientOptions" (
"id" TEXT NOT NULL,
"ingrImg" TEXT NOT NULL,
"ingrTitle" TEXT NOT NULL,
"ingrDes" TEXT NOT NULL,
"ingredientId" TEXT NOT NULL,
CONSTRAINT "ingredientOptions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Instruction" (
"id" TEXT NOT NULL,
"isVisible" BOOLEAN NOT NULL,
"instBanner" TEXT NOT NULL,
"templateId" TEXT NOT NULL,
CONSTRAINT "Instruction_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InstructionOptions" (
"id" TEXT NOT NULL,
"hint" TEXT NOT NULL,
"detail" TEXT NOT NULL,
"instructionId" TEXT NOT NULL,
CONSTRAINT "InstructionOptions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FAQ" (
"id" TEXT NOT NULL,
"isVisible" BOOLEAN NOT NULL,
"templateId" TEXT NOT NULL,
CONSTRAINT "FAQ_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FAQOptions" (
"id" TEXT NOT NULL,
"index" INTEGER NOT NULL,
"text" TEXT NOT NULL,
"faqId" TEXT NOT NULL,
CONSTRAINT "FAQOptions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Tips" (
"id" TEXT NOT NULL,
"isVisible" BOOLEAN NOT NULL,
"tipsBanner" TEXT NOT NULL,
"templateId" TEXT NOT NULL,
CONSTRAINT "Tips_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TipsOption" (
"id" TEXT NOT NULL,
"index" INTEGER NOT NULL,
"text" TEXT NOT NULL,
"tipsId" TEXT NOT NULL,
CONSTRAINT "TipsOption_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PriceCombo" (
"id" TEXT NOT NULL,
"isVisible" BOOLEAN NOT NULL,
"templateId" TEXT NOT NULL,
CONSTRAINT "PriceCombo_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PriceOption" (
"id" TEXT NOT NULL,
"quantity" TEXT NOT NULL,
"price" TEXT NOT NULL,
"comboId" TEXT NOT NULL,
CONSTRAINT "PriceOption_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Product" (
"id" TEXT NOT NULL,
"isVisible" BOOLEAN NOT NULL,
"price" TEXT NOT NULL,
"discount" INTEGER NOT NULL,
"productName" TEXT NOT NULL,
"templateId" TEXT NOT NULL,
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Banner_templateId_key" ON "Banner"("templateId");
-- CreateIndex
CREATE UNIQUE INDEX "Address_templateId_key" ON "Address"("templateId");
-- CreateIndex
CREATE UNIQUE INDEX "Ingredient_templateId_key" ON "Ingredient"("templateId");
-- CreateIndex
CREATE UNIQUE INDEX "Instruction_templateId_key" ON "Instruction"("templateId");
-- CreateIndex
CREATE UNIQUE INDEX "FAQ_templateId_key" ON "FAQ"("templateId");
-- CreateIndex
CREATE UNIQUE INDEX "Tips_templateId_key" ON "Tips"("templateId");
-- CreateIndex
CREATE UNIQUE INDEX "PriceCombo_templateId_key" ON "PriceCombo"("templateId");
-- CreateIndex
CREATE UNIQUE INDEX "Product_templateId_key" ON "Product"("templateId");
-- AddForeignKey
ALTER TABLE "Banner" ADD CONSTRAINT "Banner_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Address" ADD CONSTRAINT "Address_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Ingredient" ADD CONSTRAINT "Ingredient_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ingredientOptions" ADD CONSTRAINT "ingredientOptions_ingredientId_fkey" FOREIGN KEY ("ingredientId") REFERENCES "Ingredient"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Instruction" ADD CONSTRAINT "Instruction_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InstructionOptions" ADD CONSTRAINT "InstructionOptions_instructionId_fkey" FOREIGN KEY ("instructionId") REFERENCES "Instruction"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FAQ" ADD CONSTRAINT "FAQ_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FAQOptions" ADD CONSTRAINT "FAQOptions_faqId_fkey" FOREIGN KEY ("faqId") REFERENCES "FAQ"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Tips" ADD CONSTRAINT "Tips_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TipsOption" ADD CONSTRAINT "TipsOption_tipsId_fkey" FOREIGN KEY ("tipsId") REFERENCES "Tips"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PriceCombo" ADD CONSTRAINT "PriceCombo_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PriceOption" ADD CONSTRAINT "PriceOption_comboId_fkey" FOREIGN KEY ("comboId") REFERENCES "PriceCombo"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Product" ADD CONSTRAINT "Product_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+1
View File
@@ -21,4 +21,5 @@ model Account {
profile Profile? //one-to-one
orders Order[] //one-to-many
supports Support[]
}
+2 -2
View File
@@ -2,12 +2,12 @@ model Profile {
id String @id @default(uuid())
accountId String @unique
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
shopName String
shopLogo String?
contactNumber String?
shopAddress String?
shopMapLocation String?
shopCategory String?
}
+1 -2
View File
@@ -1,6 +1,5 @@
generator client {
provider = "prisma-client"
output = "../generated/prisma"
provider = "prisma-client-js"
}
datasource db {
+43
View File
@@ -0,0 +1,43 @@
enum T_SupportType {
TECHNICAL
BILLING
DOMAIN
TEMPLATE
PAYMENT
ACCOUNT
FEATURE_REQUEST
BUG
OTHER
}
enum T_SupportStatus {
OPEN
IN_PROGRESS
RESOLVED
REJECTED
}
model Support {
id String @id @default(uuid())
issueName String
description String
type T_SupportType
status T_SupportStatus @default(OPEN)
resolvedBy String?
resolvedAt DateTime?
storeAccountId String
storeAccount Account @relation(fields: [storeAccountId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([storeAccountId])
@@index([storeAccountId, status])
}
+139
View File
@@ -0,0 +1,139 @@
model Template {
id String @id @default(uuid())
language String
deliveryCharge String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
address Address?
banner Banner?
ingredient Ingredient?
instruction Instruction?
faq FAQ?
tips Tips?
priceCombo PriceCombo?
product Product?
}
// banner model
model Banner {
id String @id @default(uuid())
isVisible Boolean
bannerTitle String
bannerDesc String
bannerImage String
templateId String @unique
template Template @relation(fields: [templateId], references: [id])
}
// address model
model Address {
id String @id @default(uuid())
isVisible Boolean
phoneNumber String
shopLocation String
shopEmail String
templateId String @unique
template Template @relation(fields: [templateId], references: [id])
}
//ingredient model
model Ingredient {
id String @id @default(uuid())
isVisible Boolean
templateId String @unique
template Template @relation(fields: [templateId], references: [id])
options ingredientOptions[]
}
//ingredient options model
model ingredientOptions {
id String @id @default(uuid())
ingrImg String
ingrTitle String
ingrDes String
ingredientId String
ingredient Ingredient @relation(fields: [ingredientId], references: [id])
}
//instruction model & instruction options
model Instruction {
id String @id @default(uuid())
isVisible Boolean
instBanner String
templateId String @unique
template Template @relation(fields: [templateId], references: [id])
options InstructionOptions[]
}
model InstructionOptions {
id String @id @default(uuid())
hint String
detail String
instructionId String
instruction Instruction @relation(fields: [instructionId], references: [id])
}
//FAQ & FAQOptions model
model FAQ {
id String @id @default(uuid())
isVisible Boolean
templateId String @unique
template Template @relation(fields: [templateId], references: [id])
options FAQOptions[]
}
model FAQOptions {
id String @id @default(uuid())
index Int
text String
faqId String
faq FAQ @relation(fields: [faqId], references: [id])
}
//Tips & Tips options model
model Tips {
id String @id @default(uuid())
isVisible Boolean
tipsBanner String
templateId String @unique
template Template @relation(fields: [templateId], references: [id])
options TipsOption[]
}
model TipsOption {
id String @id @default(uuid())
index Int
text String
tipsId String
tips Tips @relation(fields: [tipsId], references: [id])
}
//PriceCombo & PriceComboOptions model
model PriceCombo {
id String @id @default(uuid())
isVisible Boolean
templateId String @unique
template Template @relation(fields: [templateId], references: [id])
options PriceOption[]
}
model PriceOption {
id String @id @default(uuid())
quantity String
price String
comboId String
combo PriceCombo @relation(fields: [comboId], references: [id])
}
//product model
model Product {
id String @id @default(uuid())
isVisible Boolean
price String
discount Int
productName String
templateId String @unique
template Template @relation(fields: [templateId], references: [id])
}
+5 -5
View File
@@ -3,10 +3,10 @@ import cors from 'cors';
import express, { Request, Response } from 'express';
import swaggerJSDoc from 'swagger-jsdoc';
import swaggerUi from "swagger-ui-express";
import globalErrorHandler from './app/middlewares/global_error_handler';
import notFound from './app/middlewares/not_found_api';
import appRouter from './routes';
import { swaggerOptions } from './swaggerOptions';
import globalErrorHandler from './app/middlewares/global_error_handler.js';
import notFound from './app/middlewares/not_found_api.js';
import appRouter from './routes.js';
import { swaggerOptions } from './swaggerOptions.js';
// define app
const app = express()
@@ -15,7 +15,7 @@ app.use("/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// middleware
app.use(cors({
origin: ["http://localhost:3000"],
origin: ["http://localhost:5173"],
methods: ["GET", "POST", "PATCH", "DELETE", "PUT"],
credentials: true
}))
+1
View File
@@ -23,4 +23,5 @@ export const configs = {
cloud_api_key: process.env.CLOUD_API_KEY,
cloud_api_secret: process.env.CLOUD_API_SECRET,
},
redis_url: process.env.REDIS_URL,
};
+1 -1
View File
@@ -1,5 +1,5 @@
import { ZodError, ZodIssue } from 'zod'
import { TErrorSources, TGenericErrorResponse } from '../types/error'
import { TErrorSources, TGenericErrorResponse } from '../types/error.js'
const handleZodError = (err: ZodError): TGenericErrorResponse => {
const errorSources: TErrorSources = err.issues.map((issue: ZodIssue) => {
+2 -1
View File
@@ -1,6 +1,7 @@
import { PrismaPg } from "@prisma/adapter-pg";
import pkg from "@prisma/client";
import "dotenv/config";
import { PrismaClient } from "../../../prisma/generated/prisma/client";
const { PrismaClient } = pkg;
const connectionString = `${process.env.DATABASE_URL}`;
+3 -3
View File
@@ -1,7 +1,7 @@
import { NextFunction, Request, Response } from "express";
import { configs } from "../configs";
import { AppError } from "../utils/app_error";
import { jwtHelpers, JwtPayloadType } from "../utils/JWT";
import { configs } from "../configs/index.js";
import { AppError } from "../utils/app_error.js";
import { jwtHelpers, JwtPayloadType } from "../utils/JWT.js";
type Role = "ADMIN" | "USER";
+4 -4
View File
@@ -1,9 +1,9 @@
import { ErrorRequestHandler } from "express";
import { ZodError } from "zod";
import { configs } from "../configs";
import handleZodError from "../errors/zodError";
import { TErrorSources } from "../types/error";
import { AppError } from "../utils/app_error";
import { configs } from "../configs/index.js";
import handleZodError from "../errors/zodError.js";
import { TErrorSources } from "../types/error.js";
import { AppError } from "../utils/app_error.js";
const globalErrorHandler: ErrorRequestHandler = (err, req, res, next) => {
let statusCode = 500;
@@ -1,7 +1,7 @@
import { configs } from "../../configs";
import catchAsync from "../../utils/catch_async";
import manageResponse from "../../utils/manage_response";
import { account_services } from "./account.service";
import { configs } from "../../configs/index.js";
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { account_services } from "./account.service.js";
const create_account = catchAsync(async (req, res) => {
const result = await account_services.create_account_into_db(req);
@@ -37,7 +37,7 @@ const login_user = catchAsync(async (req, res) => {
const result = await account_services.login_user_into_db(req);
// set access token into cookie
res.cookie("access_token", result, {
res.cookie("access_token", result.accessToken, {
secure: configs.env === "production",
httpOnly: true,
});
@@ -46,9 +46,7 @@ const login_user = catchAsync(async (req, res) => {
statusCode: 200,
success: true,
message: "User logged in successfully",
data: {
accessToken: result,
},
data: result,
});
});
const get_user_account = catchAsync(async (req, res) => {
+4 -4
View File
@@ -1,8 +1,8 @@
import { Router } from "express";
import auth from "../../middlewares/auth";
import RequestValidator from "../../middlewares/request_validator";
import { account_controller } from "./account.controller";
import { account_validation } from "./account.validation";
import auth from "../../middlewares/auth.js";
import RequestValidator from "../../middlewares/request_validator.js";
import { account_controller } from "./account.controller.js";
import { account_validation } from "./account.validation.js";
const accountRouter = Router();
+36 -9
View File
@@ -1,12 +1,12 @@
import bcrypt from "bcrypt";
import { Request } from "express";
import { configs } from "../../configs";
import { prisma } from "../../lib/prisma";
import { emailQueue } from "../../queues/email/email.queue";
import { AppError } from "../../utils/app_error";
import { jwtHelpers } from "../../utils/JWT";
import sendMail from "../../utils/mail_sender";
import { otpGenerator } from "../../utils/otpGenerator";
import { configs } from "../../configs/index.js";
import { prisma } from "../../lib/prisma.js";
import { emailQueue } from "../../queues/email/email.queue.js";
import { AppError } from "../../utils/app_error.js";
import { jwtHelpers } from "../../utils/JWT.js";
import sendMail from "../../utils/mail_sender.js";
import { otpGenerator } from "../../utils/otpGenerator.js";
const create_account_into_db = async (req: Request) => {
const payload = req?.body;
@@ -22,7 +22,7 @@ const create_account_into_db = async (req: Request) => {
const hashPassword = bcrypt.hashSync(payload.password, 10);
// create account and profile
const result = await prisma.$transaction(async (tx) => {
const result = await prisma.$transaction(async (tx: any) => {
const account = await tx.account.create({
data: {
email: payload.email,
@@ -71,6 +71,10 @@ const create_account_into_db = async (req: Request) => {
subject: "Welcome to Quick Launch - Verification OTP",
email: payload.email,
textBody: "You can use otp or verification link for verifying your account"
}, {
attempts: 1,
removeOnComplete: true,
removeOnFail: true,
})
return null;
};
@@ -160,6 +164,15 @@ const login_user_into_db = async (req: Request) => {
where: {
email: payload.email,
},
select: {
id: true,
email: true,
role: true,
isAccountVerified: true,
isDeleted: true,
password: true,
profile: true,
},
});
// check if account exists
@@ -196,7 +209,21 @@ const login_user_into_db = async (req: Request) => {
configs.jwt.access_token as string,
configs.jwt.access_expires as string,
);
return accessToken;
const finalOutputData = {
id: account.id,
email: account.email,
role: account.role,
shopName: account?.profile?.shopName,
shopLogo: account?.profile?.shopLogo,
}
return {
accessToken,
profile: finalOutputData
};
};
const get_user_account_from_db = async (req: Request) => {
@@ -0,0 +1,34 @@
import { Request, Response } from "express";
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { analytics_service } from "./analytics.service.js";
const getOverview = catchAsync(async (req: Request, res: Response) => {
const range = (req.query.range as "7d" | "30d") || "7d";
const result = await analytics_service.getOverview(range);
manageResponse(res, {
success: true,
statusCode: 200,
message: "Analytics overview fetched successfully.",
data: result,
meta: {},
});
});
const getLastSevenDaysRevenue = catchAsync(
async (req: Request, res: Response) => {
const result = await analytics_service.getLastSevenDaysRevenue();
manageResponse(res, {
success: true,
statusCode: 200,
message: "Analytics overview fetched successfully.",
data: result,
meta: {},
});
},
);
export const analytics_controller = {
getOverview,
getLastSevenDaysRevenue,
};
@@ -0,0 +1,25 @@
import { Router } from "express";
// import RequestValidator from "../../middlewares/request_validator.js";
import { analytics_controller } from "./analytics.controller.js";
import auth from "../../middlewares/auth.js";
// import { analytics_validations } from "./analytics.validation.js";
//! "/admin/analytics"
const router = Router();
router.get("/overview", auth("ADMIN"), analytics_controller.getOverview);
router.get("/revenue-overview", auth("ADMIN"), analytics_controller.getLastSevenDaysRevenue);
// router.post(
// "/",
// RequestValidator(analytics_validations.create_analytics),
// analytics_controller.create_analytics,
// );
export default router;
@@ -0,0 +1,190 @@
import { Request } from "express";
import { prisma } from "../../lib/prisma.js";
import dayjs from "dayjs";
const getOverview = async (range: "7d" | "30d") => {
const days = range === "30d" ? 30 : 7;
const now = dayjs();
const currentStartDate = now.subtract(days, "day").startOf("day").toDate();
const currentEndDate = now.endOf("day").toDate();
const previousStartDate = now
.subtract(days * 2, "day")
.startOf("day")
.toDate();
const previousEndDate = now.subtract(days, "day").endOf("day").toDate();
const rangeFilter = (start: Date, end: Date) => ({
gte: start,
lte: end,
});
const [
currentActiveStores,
previousActiveStores,
currentRevenue,
previousRevenue,
currentSignups,
previousSignups,
currentPendingActions,
previousPendingActions,
] = await Promise.all([
prisma.profile.count({
where: {
account: {
isAccountVerified: true,
},
},
}),
prisma.profile.count({
where: {
account: {
isDeleted: false,
isAccountVerified: true,
createdAt: rangeFilter(previousStartDate, previousEndDate),
},
},
}),
prisma.order.aggregate({
_sum: {
productPrice: true,
},
where: {
status: "DELIVERED",
createdAt: rangeFilter(currentStartDate, currentEndDate),
},
}),
prisma.order.aggregate({
_sum: {
productPrice: true,
},
where: {
status: "DELIVERED",
createdAt: rangeFilter(previousStartDate, previousEndDate),
},
}),
prisma.account.count({
where: {
createdAt: rangeFilter(currentStartDate, currentEndDate),
},
}),
prisma.account.count({
where: {
createdAt: rangeFilter(previousStartDate, previousEndDate),
},
}),
prisma.order.count({
where: {
status: {
in: ["INITIATED", "CONFIRMED"],
},
createdAt: rangeFilter(currentStartDate, currentEndDate),
},
}),
prisma.order.count({
where: {
status: {
in: ["INITIATED", "CONFIRMED"],
},
createdAt: rangeFilter(previousStartDate, previousEndDate),
},
}),
]);
const percentage = (current: number, previous: number) => {
// avoid division by zero
if (previous === 0) return current > 0 ? 100 : 0;
return Number((((current - previous) / previous) * 100).toFixed(2));
};
const currentRevenueTotal = currentRevenue._sum.productPrice ?? 0;
const previousRevenueTotal = previousRevenue._sum.productPrice ?? 0;
return {
activeStores: {
total: currentActiveStores,
changePercentage: percentage(currentActiveStores, previousActiveStores),
},
revenue: {
total: currentRevenueTotal,
changePercentage: percentage(currentRevenueTotal, previousRevenueTotal),
},
signups: {
total: currentSignups,
changePercentage: percentage(currentSignups, previousSignups),
},
pendingActions: {
total: currentPendingActions,
changePercentage: percentage(
currentPendingActions,
previousPendingActions,
),
},
};
};
const getLastSevenDaysRevenue = async () => {
const startDate = dayjs().subtract(6, "day").startOf("day").toDate();
const endDate = dayjs().endOf("day").toDate();
const orders = await prisma.order.findMany({
where: {
status: "DELIVERED",
createdAt: {
gte: startDate,
lte: endDate,
},
},
select: {
productPrice: true,
productQuantity: true,
createdAt: true,
},
});
const revenueMap: Record<string, number> = {};
for (let i = 0; i < 7; i++) {
const date = dayjs(startDate).add(i, "day");
revenueMap[date.format("dddd")] = 0;
}
for (const order of orders) {
const day = dayjs(order.createdAt).format("dddd");
revenueMap[day] += order.productPrice * order.productQuantity;
}
const data = Object.entries(revenueMap).map(([day, revenue]) => ({
day,
revenue,
}));
return data;
};
export const analytics_service = {
getOverview, getLastSevenDaysRevenue
};
@@ -0,0 +1,92 @@
export const analyticsSwaggerDocs = {
"/api/admin/analytics/overview": {
get: {
tags: ["Analytics"],
summary: "Get analytics overview (7d / 30d comparison)",
description:
"Returns aggregated analytics including active stores, revenue, signups, and percentage change compared to previous period.",
parameters: [
{
name: "range",
in: "query",
required: true,
description: "Time range for analytics overview",
schema: {
type: "string",
enum: ["7d", "30d"],
example: "7d",
},
},
],
responses: {
200: {
description: "Analytics overview fetched successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
activeStores: {
type: "object",
properties: {
total: { type: "number", example: 120 },
changePercentage: {
type: "number",
example: 5.23,
},
},
},
revenue: {
type: "object",
properties: {
total: { type: "number", example: 45230 },
changePercentage: {
type: "number",
example: -3.45,
},
},
},
signups: {
type: "object",
properties: {
total: { type: "number", example: 340 },
changePercentage: {
type: "number",
example: 12.1,
},
},
},
},
},
},
},
},
401: {
description: "Unauthorized",
},
500: {
description: "Internal server error",
},
},
},
},
"/api/admin/analytics/revenue-overview": {
get: {
tags: ["Analytics"],
summary: "Get analytics revenue overview (last 7 days)",
description: "",
responses: {
200: {
description: "Revenue overview fetched successfully",
},
401: {
description: "Unauthorized",
},
500: {
description: "Internal server error",
},
},
},
},
};
@@ -0,0 +1,10 @@
import { z } from "zod";
const create_analytics = z.object({});
const update_analytics = z.object({});
export const analytics_validations = {
create_analytics,
update_analytics,
};
+3 -3
View File
@@ -1,7 +1,7 @@
import catchAsync from "../../utils/catch_async";
import manageResponse from "../../utils/manage_response";
import { order_service } from "./order.service";
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { order_service } from "./order.service.js";
const get_all_order = catchAsync(async (req, res) => {
const result = await order_service.get_all_order_from_db(req);
+5 -4
View File
@@ -1,8 +1,9 @@
import { Router } from "express";
import RequestValidator from "../../middlewares/request_validator";
import { order_controller } from "./order.controller";
import { order_validations } from "./order.validation";
import RequestValidator from "../../middlewares/request_validator.js";
import { order_controller } from "./order.controller.js";
import { order_validations } from "./order.validation.js";
import auth from "../../middlewares/auth.js";
const router = Router();
@@ -14,7 +15,7 @@ router.post(
);
router.get("/:id", order_controller.get_single_order);
router.patch(
"/:id",
"/:id",auth("ADMIN"),
RequestValidator(order_validations.update_order),
order_controller.update_order,
);
+132 -56
View File
@@ -1,61 +1,124 @@
import { Request } from "express";
import { configs } from "../../configs";
import { prisma } from "../../lib/prisma";
import { orderEmailQueue } from "../../queues/email/order/order.email.queue";
import { configs } from "../../configs/index.js";
import { prisma } from "../../lib/prisma.js";
import { orderEmailQueue } from "../../queues/email/order/order.email.queue.js";
import { AppError } from "../../utils/app_error.js";
import paginationHelper from "../../utils/pagination_helper.js";
const get_all_order_from_db = async (req: Request) => {
// define your own login here
const search=req.query.search as string
const customerName=req.query.customerName as string
const productName=req.query.productName as string
console.log(productName)
const andCondition=[] as any[]
if(search){
andCondition.push({
OR:[
{
productName:{
contains:search,
mode:"insensitive"
}
}
]
})
}
if(customerName){
andCondition.push({
OR:[
{
customerName:{
contains:customerName,
mode:"insensitive"
}
}
]
})
}
if(productName){
andCondition.push({
OR:[
{
productName:{
contains:productName,
mode:"insensitive"
}
}
]
})
}
console.log(search)
const search = req.query.search as string;
const customerName = req.query.customerName as string;
const productName = req.query.productName as string;
const result = await prisma.order.findMany({
where:{
AND:andCondition
}
// for date filter
const startDate = req.query.startDate as string;
const endDate = req.query.endDate as string;
const status = (req.query.status as string) || undefined;
const { page, limit, skip, sortBy, sortOrder } = paginationHelper(req.query);
const andCondition = [] as any[];
if (search) {
andCondition.push({
OR: [
{
productName: {
contains: search,
mode: "insensitive",
},
},
],
});
}
if (customerName) {
andCondition.push({
OR: [
{
customerName: {
contains: customerName,
mode: "insensitive",
},
},
],
});
}
if (productName) {
andCondition.push({
OR: [
{
productName: {
contains: productName,
mode: "insensitive",
},
},
],
});
}
if (status) {
andCondition.push({
OR: [
{
status: {
contains: status,
mode: "insensitive",
},
},
],
});
}
// for date filter
const dateFilter: any = {};
if (startDate) {
const start = new Date(startDate);
start.setHours(0, 0, 0, 0);
dateFilter.gte = start;
}
if (endDate) {
const end = new Date(endDate);
end.setHours(23, 59, 59, 999);
dateFilter.lte = end;
}
if (Object.keys(dateFilter).length > 0) {
andCondition.push({
createdAt: dateFilter,
});
}
const getAllOrders = await prisma.order.findMany({
take: limit,
skip,
where: {
AND: andCondition,
},
select: {
id: true,
customerName: true,
productQuantity: true,
productPrice: true,
status: true,
createdAt: true,
},
orderBy: {
[sortBy as string]: sortOrder,
},
});
return result;
const result = await prisma.order.count({
where: {
AND: andCondition,
},
});
return {
data: getAllOrders,
pagination: {
total: result,
page,
limit,
totalPages: Math.ceil(result / limit),
},
};
};
const get_single_order_from_db = async (req: Request) => {
@@ -67,9 +130,9 @@ const get_single_order_from_db = async (req: Request) => {
const create_order_into_db = async (req: Request) => {
const payload = req?.body;
console.log(payload)
console.log(payload);
payload.status = "INITIATED";
payload.paymentType = "COD"
payload.paymentType = "COD";
// nwo init order
const result = await prisma.order.create({ data: payload });
@@ -81,15 +144,24 @@ const create_order_into_db = async (req: Request) => {
email: payload.customerEmail,
subject: "Order Tracking",
textBody: `Your order has been created. Track your order here: ${trackingLink}`,
htmlBody: `<p>Your order has been created. Track your order here: <a href="${trackingLink}">Track Order</a></p>`
})
htmlBody: `<p>Your order has been created. Track your order here: <a href="${trackingLink}">Track Order</a></p>`,
});
}
return result;
};
const update_order_into_db = async (req: Request) => {
// define your own login here
const user = req.user;
console.log(user);
if (user?.role !== "ADMIN") {
throw new AppError("You are not authorized to perform this action", 403);
}
const { id } = req.params as { id: string };
const isProductExist = await prisma.order.findUnique({ where: { id } });
if (!isProductExist) {
throw new AppError("Order is not found", 404);
}
const result = await prisma.order.update({ where: { id }, data: req.body });
return result;
};
@@ -97,6 +169,10 @@ const update_order_into_db = async (req: Request) => {
const delete_order_from_db = async (req: Request) => {
// define your own login here
const { id } = req.params as { id: string };
const user = req.user;
if (user?.role !== "ADMIN") {
throw new AppError("You are not authorized to perform this action", 403);
}
const result = await prisma.order.delete({ where: { id } });
return result;
};
+43 -23
View File
@@ -1,25 +1,28 @@
export const orderSwaggerDocs = {
"/api/order": {
post: {
tags: ["order"],
summary: "Create new order",
description: "",
description: ` INITIATED
CONFIRMED
ONGOING
DELIVERED
CANCELLED`,
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
"shopAccountId": "",
"productPrice": 1500,
"productQuantity": 2,
"productName": "Wireless Mouse",
"customerName": "Rahim Uddin",
"customerPhone": "+8801712345678",
"customerEmail": "softvence.abumahid@gmail.com",
"customerAddress": "Rangpur, Bangladesh",
"customerNote": "Please deliver between 3-5 PM"
})
shopAccountId: "",
productPrice: 1500,
productQuantity: 2,
productName: "Wireless Mouse",
customerName: "Rahim Uddin",
customerPhone: "+8801712345678",
customerEmail: "softvence.abumahid@gmail.com",
customerAddress: "Rangpur, Bangladesh",
customerNote: "Please deliver between 3-5 PM",
}),
},
},
},
@@ -63,6 +66,32 @@ export const orderSwaggerDocs = {
required: false,
schema: { type: "string" },
},
{
name: "status",
in: "query",
required: false,
schema: { type: "string" },
},
{
name: "date",
in: "query",
required: false,
schema: { type: "string" },
},
{
name: "startDate",
in: "query",
required: false,
schema: { type: "string", format: "date" },
example: "2026-04-01",
},
{
name: "endDate",
in: "query",
required: false,
schema: { type: "string", format: "date" },
example: "2026-04-31",
},
],
responses: {
200: { description: "order fetched successfully" },
@@ -91,7 +120,7 @@ export const orderSwaggerDocs = {
},
patch: {
tags: ["order"],
summary: "Update order",
summary: "Update order -(Admin route)",
description: "",
parameters: [
{
@@ -106,14 +135,7 @@ export const orderSwaggerDocs = {
content: {
"application/json": {
example: JSON.stringify({
"shopAccountId": "",
"productPrice": 1500,
"productQuantity": 2,
"productName": "Wireless Mouse",
"customerName": "Rahim Uddin",
"customerPhone": "+8801712345678",
"customerAddress": "Rangpur, Bangladesh",
"customerNote": "Please deliver between 3-5 PM"
status: "INITIATED",
}), // put your request body
},
},
@@ -142,5 +164,3 @@ export const orderSwaggerDocs = {
},
},
};
+3 -3
View File
@@ -1,7 +1,7 @@
import catchAsync from "../../utils/catch_async";
import manageResponse from "../../utils/manage_response";
import { plan_service } from "./plan.service";
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { plan_service } from "./plan.service.js";
const get_all_plan = catchAsync(async (req, res) => {
const result = await plan_service.get_all_plan_from_db(req);
+8 -7
View File
@@ -1,8 +1,8 @@
import { Router } from "express";
import RequestValidator from "../../middlewares/request_validator";
import { plan_controller } from "./plan.controller";
import { plan_validations } from "./plan.validation";
import { Router } from "express";
import auth from "../../middlewares/auth.js";
import RequestValidator from "../../middlewares/request_validator.js";
import { plan_controller } from "./plan.controller.js";
import { plan_validations } from "./plan.validation.js";
const router = Router();
@@ -10,15 +10,16 @@ router.get("/", plan_controller.get_all_plan);
router.post(
"/",
RequestValidator(plan_validations.create_plan),
auth("ADMIN"),
plan_controller.create_plan,
);
router.get("/:id", plan_controller.get_single_plan);
router.patch(
"/:id",
RequestValidator(plan_validations.update_plan),
auth("ADMIN"),
plan_controller.update_plan,
);
router.delete("/:id", plan_controller.delete_plan);
router.delete("/:id", auth("ADMIN"), plan_controller.delete_plan);
export default router;
+13 -4
View File
@@ -1,11 +1,20 @@
import { Request } from "express";
import { prisma } from "../../lib/prisma";
import { AppError } from "../../utils/app_error";
import { prisma } from "../../lib/prisma.js";
import { AppError } from "../../utils/app_error.js";
const get_all_plan_from_db = async (req: Request) => {
// define your own login here
const result = await prisma.plan.findMany();
const result = await prisma.plan.findMany({
select:{
planName: true,
price: true,
planType: true,
planDesc: true,
planFeatures: true,
}
});
return result;
};
@@ -22,7 +31,7 @@ const get_single_plan_from_db = async (req: Request) => {
const create_plan_into_db = async (req: Request) => {
// define your own login here
const user = req.user
const user = req?.user
if (user?.role !== "ADMIN") {
throw new AppError("You dont have permission to create plan information.!!!", 401)
}
@@ -1,6 +1,6 @@
import catchAsync from "../../utils/catch_async";
import manageResponse from "../../utils/manage_response";
import { profile_service } from "./profile.service";
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { profile_service } from "./profile.service.js";
const update_profile = catchAsync(async (req, res) => {
const result = await profile_service.update_profile_into_db(req);
+5 -5
View File
@@ -1,9 +1,9 @@
import { Router } from "express";
import RequestValidator from "../../middlewares/request_validator";
import { profile_controller } from "./profile.controller";
import { profile_validations } from "./profile.validation";
import auth from "../../middlewares/auth";
import uploader from "../../middlewares/uploader";
import RequestValidator from "../../middlewares/request_validator.js";
import { profile_controller } from "./profile.controller.js";
import { profile_validations } from "./profile.validation.js";
import auth from "../../middlewares/auth.js";
import uploader from "../../middlewares/uploader.js";
const router = Router();
+4 -3
View File
@@ -1,12 +1,13 @@
import { Request } from "express";
import uploadCloud from "../../utils/cloudinary";
import { prisma } from "../../lib/prisma";
import { JwtPayloadType } from "../../utils/JWT";
import uploadCloud from "../../utils/cloudinary.js";
import { prisma } from "../../lib/prisma.js";
import { JwtPayloadType } from "../../utils/JWT.js";
const update_profile_into_db = async (req: Request) => {
const user = req?.user as JwtPayloadType;
const payload = req?.body;
const file = req?.file;
console.log(payload);
// check file and upload to cloud
if (file) {
const cloudRes = await uploadCloud(file);
@@ -0,0 +1,92 @@
import manageResponse from "../../utils/manage_response";
import { Request, Response } from "express";
import catchAsync from "../../utils/catch_async";
import { statictics_service } from "./statictics.service";
const get_seller_stats = catchAsync(async (req: Request, res: Response) => {
const shopAccountId = req.user?.accountId;
if (!shopAccountId) {
return res.status(401).json({
success: false,
message: "Unauthorized",
});
}
const range = (req.query.range as "7d" | "30d" | "all") || "7d";
const result = await statictics_service.get_seller_stats_fromDb(
shopAccountId,
range,
);
manageResponse(res, {
success: true,
statusCode: 200,
message: "All statictics fetched successfully.",
data: result,
meta: {},
});
});
// const get_all_statictics = catchAsync(async (req, res) => {
// const result = await statictics_service.get_all_statictics_from_db(req);
// manageResponse(res, {
// success: true,
// statusCode: 200,
// message: "All statictics fetched successfully.",
// data: result,
// meta: {},
// });
// });
// const get_single_statictics = catchAsync(async (req, res) => {
// const result = await statictics_service.get_single_statictics_from_db(req);
// manageResponse(res, {
// success: true,
// statusCode: 200,
// message: "Single statictics fetched successfully.",
// data: result,
// meta: {},
// });
// });
// const create_statictics = catchAsync(async (req, res) => {
// const result = await statictics_service.create_statictics_into_db(req);
// manageResponse(res, {
// success: true,
// statusCode: 200,
// message: "statictics created successfully.",
// data: result,
// meta: {},
// });
// });
// const update_statictics = catchAsync(async (req, res) => {
// const result = await statictics_service.update_statictics_into_db(req);
// manageResponse(res, {
// success: true,
// statusCode: 200,
// message: "statictics updated successfully.",
// data: result,
// meta: {},
// });
// });
// const delete_statictics = catchAsync(async (req, res) => {
// const result = await statictics_service.delete_statictics_from_db(req);
// manageResponse(res, {
// success: true,
// statusCode: 200,
// message: "statictics deleted successfully.",
// data: result,
// meta: {},
// });
// });
export const statictics_controller = {
get_seller_stats,
// get_all_statictics,
// get_single_statictics,
// create_statictics,
// update_statictics,
// delete_statictics,
};
@@ -0,0 +1,24 @@
import { Router } from "express";
import auth from "../../middlewares/auth.js";
import { statictics_controller } from "./statictics.controller.js";
// import { statictics_controller } from "./statictics.controller";
// import { statictics_validations } from "./statictics.validation";
const router = Router();
router.get("/seller", auth("USER"), statictics_controller.get_seller_stats);
// router.post(
// "/",
// RequestValidator(statictics_validations.create_statictics),
// statictics_controller.create_statictics,
// );
// router.get("/:id", statictics_controller.get_single_statictics);
// router.patch(
// "/:id",
// RequestValidator(statictics_validations.update_statictics),
// statictics_controller.update_statictics,
// );
// router.delete("/:id", statictics_controller.delete_statictics);
export const staticticsRoute = router;
@@ -0,0 +1,151 @@
// import { Request } from "express";
// import { prisma } from "../../lib/prisma";
import { prisma } from "../../lib/prisma";
type Range = "7d" | "30d" | "all";
const get_seller_stats_fromDb = async (shopAccountId: string, range: Range) => {
let createdAtFilter: any = {};
if (range !== "all") {
const days = range === "7d" ? 7 : 30;
const from = new Date();
from.setDate(from.getDate() - days);
createdAtFilter = { gte: from };
}
const baseWhere: any = {
shopAccountId,
...(range !== "all" && { createdAt: createdAtFilter }),
};
const [
totalOrders,
completedOrders,
pendingOrders,
rejectedOrders,
revenueResult,
last7DaysRejected,
ordersForChart,
] = await Promise.all([
prisma.order.count({ where: baseWhere }),
prisma.order.count({
where: { ...baseWhere, status: "DELIVERED" },
}),
prisma.order.count({
where: {
...baseWhere,
status: { in: ["INITIATED", "CONFIRMED", "ONGOING"] },
},
}),
prisma.order.count({
where: { ...baseWhere, status: "CANCELLED" },
}),
prisma.order.aggregate({
where: { ...baseWhere, status: "DELIVERED" },
_sum: { productPrice: true },
}),
prisma.order.count({
where: {
shopAccountId,
status: "CANCELLED",
createdAt: {
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
},
},
}),
range !== "all"
? prisma.order.findMany({
where: {
shopAccountId,
status: "DELIVERED",
createdAt: createdAtFilter,
},
select: {
productPrice: true,
createdAt: true,
},
})
: Promise.resolve([]),
]);
const totalRevenue = revenueResult._sum.productPrice || 0;
const avgOrderValue =
completedOrders > 0 ? totalRevenue / completedOrders : 0;
const dailyMap: Record<string, number> = {};
for (const o of ordersForChart) {
const date = o.createdAt.toISOString().split("T")[0];
if (!dailyMap[date]) dailyMap[date] = 0;
dailyMap[date] += o.productPrice;
}
const dailyRevenue = Object.entries(dailyMap).map(([date, revenue]) => ({
date,
revenue,
}));
return {
totalOrders,
completedOrders,
pendingOrders,
rejectedOrders,
totalRevenue,
avgOrderValue,
last7DaysRejected,
dailyRevenue,
};
};
// const get_all_statictics_from_db = async (req: Request) => {
// // define your own login here
// const result = await prisma.statictics.findMany();
// return result;
// };
// const get_single_statictics_from_db = async (req: Request) => {
// // define your own login here
// const { id } = req.params;
// const result = await prisma.statictics.findUnique({where:{id}});
// return result;
// };
// const create_statictics_into_db = async (req: Request) => {
// // define your own login here
// const result = await prisma.statictics.create({data:req.body});
// return result;
// };
// const update_statictics_into_db = async (req: Request) => {
// // define your own login here
// const { id } = req.params;
// const result = await prisma.statictics.update({where:{id},data:req.body});
// return result;
// };
// const delete_statictics_from_db = async (req: Request) => {
// // define your own login here
// const { id } = req.params;
// const result = await prisma.statictics.delete({where:{id}});
// return result;
// };
export const statictics_service = {
get_seller_stats_fromDb,
// get_all_statictics_from_db,
// get_single_statictics_from_db,
// create_statictics_into_db,
// update_statictics_into_db,
// delete_statictics_from_db,
};
@@ -0,0 +1,141 @@
export const staticticsSwaggerDocs = {
"/api/statictics": {
post: {
tags: ["statictics"],
summary: "Create new statictics",
description: "",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({}), // put your request body
},
},
},
responses: {
201: { description: "statictics created successfully" },
500: { description: "Validation error or internal server error" },
},
},
get: {
tags: ["statictics"],
summary: "Get all statictics",
description: "",
parameters: [
{
name: "page",
in: "query",
required: false,
schema: { type: "number" },
},
{
name: "limit",
in: "query",
required: false,
schema: { type: "number" },
},
],
responses: {
200: { description: "statictics fetched successfully" },
401: { description: "unauthorized" },
},
},
},
"/api/statictics/seller": {
get: {
tags: ["statistics"],
summary: "Get seller statistics",
description: "Fetch seller dashboard stats (7d, 30d, all)",
parameters: [
{
name: "range",
in: "query",
required: false,
schema: {
type: "string",
enum: ["7d", "30d", "all"],
default: "7d",
},
description: "Time range for statistics",
},
],
responses: {
200: {
description: "Statistics fetched successfully",
},
401: {
description: "Unauthorized",
},
},
},
},
"/api/statictics/{id}": {
get: {
tags: ["statictics"],
summary: "Get single statictics",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
200: { description: "statictics fetched successfully" },
401: { description: "unauthorized" },
},
},
patch: {
tags: ["statictics"],
summary: "Update statictics",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({}), // put your request body
},
},
},
responses: {
200: { description: "statictics updated successfully" },
500: { description: "Validation error or internal server error" },
},
},
delete: {
tags: ["statictics"],
summary: "Delete statictics",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
200: { description: "statictics delete successfully" },
401: { description: "unauthorized" },
},
},
},
};
@@ -0,0 +1,10 @@
import { z } from "zod";
const create_statictics = z.object({});
const update_statictics = z.object({});
export const statictics_validations = {
create_statictics,
update_statictics,
};
@@ -0,0 +1,115 @@
import { Request, Response } from "express";
import catchAsync from "../../utils/catch_async.js";
import manageResponse from "../../utils/manage_response.js";
import { support_service } from "./support.service.js";
const createSupport = catchAsync(async (req: Request, res: Response) => {
const id = req?.user?.accountId;
const data = {
...req.body,
storeAccountId: id as string,
};
const result = await support_service.createSupportIntoDB(data);
manageResponse(res, {
success: true,
statusCode: 200,
message: "support created successfully.",
data: result,
meta: {},
});
});
const getAllSupport = catchAsync(async (req: Request, res: Response) => {
const role = req?.user?.role;
const id = req?.user?.accountId;
const search = req?.query?.search;
const type = req?.query?.type;
const status = req?.query?.status;
const page = Number(req?.query?.page) || 1;
const limit = Number(req?.query?.limit) || 10;
const result = await support_service.getAllSupportFromDB(
id as string,
role as string,
search as string,
type as string,
status as string,
page,
limit
);
manageResponse(res, {
success: true,
statusCode: 200,
message: "All support fetched successfully.",
data: result.data,
meta: result.meta,
});
});
const get_single_support = catchAsync(async (req, res) => {
const { id } = req.params;
const userId = req?.user?.accountId;
const role = req?.user?.role;
const result = await support_service.getSingleSupportFromDB(
id as string,
userId as string,
role as string,
);
manageResponse(res, {
success: true,
statusCode: 200,
message: "Single support fetched successfully.",
data: result,
meta: {},
});
});
const update_support = catchAsync(async (req, res) => {
const { id } = req.params;
const userId = req?.user?.accountId;
const role = req?.user?.role;
const data = req.body;
const result = await support_service.updateSupportIntoDB(
id as string,
userId as string,
role as string,
data,
);
manageResponse(res, {
success: true,
statusCode: 200,
message: "support updated successfully.",
data: result,
meta: {},
});
});
const delete_support = catchAsync(async (req, res) => {
const { id } = req.params;
const userId = req?.user?.accountId;
const role = req?.user?.role;
const result = await support_service.deleteSupportFromDB(
id as string,
userId as string,
role as string,
);
manageResponse(res, {
success: true,
statusCode: 200,
message: "support deleted successfully.",
data: result,
meta: {},
});
});
export const support_controller = {
createSupport,
getAllSupport,
get_single_support,
update_support,
delete_support,
};
+30
View File
@@ -0,0 +1,30 @@
import { Router } from "express";
import RequestValidator from "../../middlewares/request_validator.js";
import { support_controller } from "./support.controller.js";
import { support_validations } from "./support.validation.js";
import auth from "../../middlewares/auth.js";
const router = Router();
router.get("/", auth("ADMIN", "USER"), support_controller.getAllSupport);
router.post(
"/",
auth("ADMIN", "USER"),
RequestValidator(support_validations.create_support),
support_controller.createSupport,
);
router.get("/:id", auth("ADMIN", "USER"), support_controller.get_single_support);
router.patch(
"/:id",
auth("ADMIN", "USER"),
RequestValidator(support_validations.update_support),
support_controller.update_support,
);
router.delete("/:id", auth("ADMIN", "USER"), support_controller.delete_support);
export default router;
+158
View File
@@ -0,0 +1,158 @@
import { prisma } from "../../lib/prisma.js";
import { Prisma } from "@prisma/client";
import { AppError } from "../../utils/app_error.js";
const createSupportIntoDB = async (payload: any) => {
const result = await prisma.support.create({ data: payload });
return result;
};
const getAllSupportFromDB = async (
user_id: string,
role: string,
search?: string,
type?: string,
status?: string,
page: number = 1,
limit: number = 10,
) => {
const andCondition: Prisma.SupportWhereInput[] = [];
if (search) {
andCondition.push({
OR: [
{
issueName: {
contains: search,
mode: "insensitive",
},
},
{
description: {
contains: search,
mode: "insensitive",
},
},
],
});
}
if (type) {
andCondition.push({
type: type as any,
});
}
if (status) {
andCondition.push({
status: status as any,
});
}
if (role !== "ADMIN") {
andCondition.push({
storeAccountId: user_id,
});
}
const whereCondition: Prisma.SupportWhereInput =
andCondition.length > 0 ? { AND: andCondition } : {};
const skip = (page - 1) * limit;
const [data, total] = await Promise.all([
prisma.support.findMany({
where: whereCondition,
skip,
take: limit,
orderBy: {
createdAt: "desc",
},
}),
prisma.support.count({
where: whereCondition,
}),
]);
return {
meta: {
page,
limit,
total,
totalPage: Math.ceil(total / limit),
},
data,
};
};
const getSingleSupportFromDB = async (
id: string,
userId: string,
role: string,
) => {
const support = await prisma.support.findUnique({
where: { id },
});
if (!support) {
throw new AppError("Support not found", 404);
}
if (role !== "ADMIN" && support.storeAccountId !== userId) {
throw new AppError("You are not authorized", 403);
}
return support;
};
const updateSupportIntoDB = async (
id: string,
userId: string,
role: string,
payload: any,
) => {
const support = await prisma.support.findUnique({
where: { id },
});
if (!support) {
throw new AppError("Support not found", 404);
}
if (role !== "ADMIN" && support.storeAccountId !== userId) {
throw new AppError("You are not authorized", 403);
}
const result = await prisma.support.update({
where: { id },
data: payload,
});
return result;
};
const deleteSupportFromDB = async (
id: string,
userId: string,
role: string,
) => {
const support = await prisma.support.findUnique({
where: { id },
});
if (!support) {
throw new AppError("Support not found", 404);
}
if (role !== "ADMIN" && support.storeAccountId !== userId) {
throw new AppError("You are not authorized", 403);
}
const result = await prisma.support.delete({
where: { id },
});
return result;
};
export const support_service = {
createSupportIntoDB,
getAllSupportFromDB,
getSingleSupportFromDB,
updateSupportIntoDB,
deleteSupportFromDB,
};
+161
View File
@@ -0,0 +1,161 @@
export const supportSwaggerDocs = {
"/api/support": {
post: {
tags: ["support"],
summary: "Create new support",
description:
"type must be: TECHNICAL | BILLING | DOMAIN | TEMPLATE | PAYMENT | ACCOUNT | FEATURE_REQUEST | BUG | OTHER",
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
issueName: "Your issue name",
description: "Issue description",
type: "Issue Type",
}), // put your request body
},
},
},
responses: {
201: { description: "support created successfully" },
500: { description: "Validation error or internal server error" },
},
},
get: {
tags: ["support"],
summary: "Get all support",
description: "",
parameters: [
{
name: "page",
in: "query",
required: false,
schema: { type: "number" },
},
{
name: "limit",
in: "query",
required: false,
schema: { type: "number" },
},
{
name: "search",
in: "query",
required: false,
description: "Search by issue name or description",
schema: {
type: "string",
},
},
{
name: "type",
in: "query",
required: false,
description: "Filter by support type",
schema: {
type: "string",
enum: [
"TECHNICAL",
"BILLING",
"DOMAIN",
"TEMPLATE",
"PAYMENT",
"ACCOUNT",
"FEATURE_REQUEST",
"BUG",
"OTHER",
],
},
},
{
name: "status",
in: "query",
required: false,
description: "Filter by support status",
schema: {
type: "string",
enum: ["OPEN", "IN_PROGRESS", "RESOLVED", "REJECTED"],
},
},
],
responses: {
200: { description: "support fetched successfully" },
401: { description: "unauthorized" },
},
},
},
"/api/support/{id}": {
get: {
tags: ["support"],
summary: "Get single support",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
200: { description: "support fetched successfully" },
401: { description: "unauthorized" },
},
},
patch: {
tags: ["support"],
summary: "Update support",
description: `type(enum): TECHNICAL | BILLING | DOMAIN | TEMPLATE | PAYMENT | ACCOUNT | FEATURE_REQUEST | BUG | OTHER \n
status(enum): OPEN
| IN_PROGRESS
| RESOLVED
| REJECTED`,
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
requestBody: {
required: true,
content: {
"application/json": {
example: JSON.stringify({
issueName: "Your issue name",
description: "Issue description",
type: "Issue Type",
status: "issue current status",
resolvedBy: "dataTime()",
resolvedAt: "dateTime()",
}), // put your request body
},
},
},
responses: {
200: { description: "support updated successfully" },
500: { description: "Validation error or internal server error" },
},
},
delete: {
tags: ["support"],
summary: "Delete support",
description: "",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string" },
},
],
responses: {
200: { description: "support delete successfully" },
401: { description: "unauthorized" },
},
},
},
};
@@ -0,0 +1,33 @@
import { z } from "zod";
const create_support = z.object({
issueName: z.string().min(1, "issueName is required"),
description: z.string().min(1, "description is required"),
type: z.enum([
"TECHNICAL",
"BILLING",
"DOMAIN",
"TEMPLATE",
"PAYMENT",
"ACCOUNT",
"FEATURE_REQUEST",
"BUG",
"OTHER",
]),
});
const update_support = z.object({
issueName: z.string().optional(),
description: z.string().optional(),
type: z.enum(["BUG", "PAYMENT", "ACCOUNT", "OTHER"]).optional(),
status: z.enum(["OPEN", "IN_PROGRESS", "RESOLVED", "REJECTED"]).optional(),
resolvedBy: z.string().optional(),
resolvedAt: z.coerce.date().optional(),
});
export const support_validations = {
create_support,
update_support,
};

Some files were not shown because too many files have changed in this diff Show More