This commit is contained in:
Rynare 2025-05-19 21:20:05 +07:00
parent cd198e9272
commit da539298fb
19 changed files with 732 additions and 190 deletions

230
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "order-services-node",
"version": "0.0.0",
"dependencies": {
"@ngrok/ngrok": "^1.5.1",
"amqplib": "^0.10.3",
"axios": "^1.4.0",
"bcrypt": "^5.1.1",
@ -158,6 +159,235 @@
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@ngrok/ngrok": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok/-/ngrok-1.5.1.tgz",
"integrity": "sha512-sfcgdpiAJHqmuO3e6QjQGbavIrR3E72do/NAsnGhm+7SGstLj1aM3Sd8mkfTORb2Hj7ATMuoBYuED5ylKuRQCg==",
"license": "(MIT OR Apache-2.0)",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@ngrok/ngrok-android-arm64": "1.5.1",
"@ngrok/ngrok-darwin-arm64": "1.5.1",
"@ngrok/ngrok-darwin-universal": "1.5.1",
"@ngrok/ngrok-darwin-x64": "1.5.1",
"@ngrok/ngrok-freebsd-x64": "1.5.1",
"@ngrok/ngrok-linux-arm-gnueabihf": "1.5.1",
"@ngrok/ngrok-linux-arm64-gnu": "1.5.1",
"@ngrok/ngrok-linux-arm64-musl": "1.5.1",
"@ngrok/ngrok-linux-x64-gnu": "1.5.1",
"@ngrok/ngrok-linux-x64-musl": "1.5.1",
"@ngrok/ngrok-win32-arm64-msvc": "1.5.1",
"@ngrok/ngrok-win32-ia32-msvc": "1.5.1",
"@ngrok/ngrok-win32-x64-msvc": "1.5.1"
}
},
"node_modules/@ngrok/ngrok-android-arm64": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-android-arm64/-/ngrok-android-arm64-1.5.1.tgz",
"integrity": "sha512-2Tokwi5GVWNLw3JEoM0Ieb/ypALniZu6fciUTgpuByutbKxOjvahD4fYOKwW3KMdV9bCb3XGGtWJCZXfRPPq1g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ngrok/ngrok-darwin-arm64": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-arm64/-/ngrok-darwin-arm64-1.5.1.tgz",
"integrity": "sha512-HNOhrPDP+nJJY7Bh45DOeh6jmcGASWINGbUuseZM0C8psQMp7crPywjRh0inkRegUrb4K8y06sfmgt2fmsF6jQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ngrok/ngrok-darwin-universal": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-universal/-/ngrok-darwin-universal-1.5.1.tgz",
"integrity": "sha512-EsMxYC/tY+ZqhjbeZtVq5MFIuD8SEPgAlHINEszsHd8ZRICc2U9Xl15CbDrew3pcfEg/ZVFrOH9CyC4aZ/V/cA==",
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ngrok/ngrok-darwin-x64": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-x64/-/ngrok-darwin-x64-1.5.1.tgz",
"integrity": "sha512-H/x1BsYpAoTMhOtv4oYvwY6WHqbY0MsJ1XFcJQgrpAIjgmYqlwsnsUMHvEdBB/KY9kXF9DPgKUdRMfJwUIpwGA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ngrok/ngrok-freebsd-x64": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-freebsd-x64/-/ngrok-freebsd-x64-1.5.1.tgz",
"integrity": "sha512-dY2W6HUv7e2xkpdfVj7fIk+5qmvrC7kVu6PJWJ8/rshW1FrU7qMcpnU53JvoQJRZzUf5k8xMNdx30zai/8mqYA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ngrok/ngrok-linux-arm-gnueabihf": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm-gnueabihf/-/ngrok-linux-arm-gnueabihf-1.5.1.tgz",
"integrity": "sha512-JvbI/IIycw4Qq02ysyOBsSK5E0bZDgRqXSslHLTwuDAfw14lmrq2U0QkBeEOL8qwJ7wCwCH1PEOJacUyrqa9bg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ngrok/ngrok-linux-arm64-gnu": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm64-gnu/-/ngrok-linux-arm64-gnu-1.5.1.tgz",
"integrity": "sha512-yLFAlqTYYvH7QRg589HJarQGw1QrKQZcHiw0gm175eCqc+jpUG/Zcf8wohCTIJVLylMIzjDzVFSUsXC7UtMJdQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ngrok/ngrok-linux-arm64-musl": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm64-musl/-/ngrok-linux-arm64-musl-1.5.1.tgz",
"integrity": "sha512-momB/ZjjrxaGYOZ3YPAw1kT4DAfWT1x3dAHL0YoSVfNCpc8Fw0189ZAcxGn0hUFqkGDmSARS9o8b7hYd1b41oA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ngrok/ngrok-linux-x64-gnu": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-x64-gnu/-/ngrok-linux-x64-gnu-1.5.1.tgz",
"integrity": "sha512-fmMaz0b1Ry2CDLLn0mV8b9nLxqm0taQ2jYyn+C9OrazYNMT4XYYDKRQSm4UEaNoakdnoH+f2FsrWi/712GFxAQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ngrok/ngrok-linux-x64-musl": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-x64-musl/-/ngrok-linux-x64-musl-1.5.1.tgz",
"integrity": "sha512-6Ajl9wpJSlvukl4WrkIw+WxVwAr7WTGnE35Voec6CERWtKMsO/F+BOSu3pfAa6iwxGK//JBpsTT1IwLLw7b2xQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ngrok/ngrok-win32-arm64-msvc": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-arm64-msvc/-/ngrok-win32-arm64-msvc-1.5.1.tgz",
"integrity": "sha512-JUH2yZxDPQGmQNT1d2KIu64u2k/R6uG1kEIXjcbsoff37v9aI6nUlzldRWB/wFSYkpZ4W/EuovM4Epar+fQOxQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ngrok/ngrok-win32-ia32-msvc": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-ia32-msvc/-/ngrok-win32-ia32-msvc-1.5.1.tgz",
"integrity": "sha512-zS1JsMTJHnY+lPJFUwKnB5fzPm4GZCKeeZLehHrXP0LpQaKN8Y/vywqDGhuC0WtymvWE88+oreMV/6hQdviLSA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@ngrok/ngrok-win32-x64-msvc": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-x64-msvc/-/ngrok-win32-x64-msvc-1.5.1.tgz",
"integrity": "sha512-HegRwV9Gchh4p7K7sC6SPpWmFRwDEgwPByrb8tkuWDyP+EWNgpt3GKp8OAIK2xdWWHnN5VIwMa9u3COE/e5S8w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",

View File

@ -9,6 +9,7 @@
"build": "tsc --build"
},
"dependencies": {
"@ngrok/ngrok": "^1.5.1",
"amqplib": "^0.10.3",
"axios": "^1.4.0",
"bcrypt": "^5.1.1",

View File

@ -10,31 +10,40 @@ import helmet from 'helmet';
import compression from 'compression';
import cors from 'cors';
// import internalRouter from './routes/internal';
import apiRouter from './routes/api';
import connectDB from './database/MySQL';
import { root_path } from './utils/core/storage';
import { respondWithError } from './middleware/core/errorHandler';
// import schedule from 'node-schedule';
import { getLocalIP } from './dev-core';
const app = express();
// Middleware
const allowedOrigins = ['http://localhost:3000', 'https://myapp.com'];
const localIP = getLocalIP()
app.use(cors({
// 🚨 1. Setup Allowed Origins
const allowedOrigins = ['http://localhost:3000', 'https://myapp.com', `http://${localIP}:3000`];
const corsOptions: cors.CorsOptions = {
origin(origin, callback) {
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
return callback(null, true);
} else {
return callback(new Error('Not allowed by CORS'));
}
if (!origin) return callback(null, true); // Allow server-to-server or curl
if (allowedOrigins.includes(origin)) return callback(null, true);
return callback(new Error('Not allowed by CORS'));
},
credentials: true
}));
credentials: true,
};
// 🚨 2. Pasang CORS Middleware DULUAN
app.use(cors(corsOptions));
// 💡 3. Handle all OPTIONS preflight
app.options('*', cors(corsOptions));
// 🔍 4. Logging semua request (termasuk OPTIONS)
app.use((req, _res, next) => {
console.log(`[${req.method}] ${req.originalUrl}`);
next();
});
// 💨 Middleware lain
app.use(compression());
app.use(helmet());
app.use(logger('dev'));
@ -43,40 +52,35 @@ app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// View engine setup
// app.set('views', path.join(__dirname, 'views'));
// 🖼️ View engine
app.set('views', root_path('src/views'));
app.set('view engine', 'ejs');
// Database connection
// 🔌 DB connection
connectDB();
app.get('/', function (req, res, next) {
// ✅ Basic endpoint untuk cek server hidup
app.get('/', function (req, res) {
res.json({
status: 'running',
developer: 'Fahim',
project: 'Stock Prediction with ARIMA'
project: 'Stock Prediction with ARIMA',
});
});
// 🔗 API Router
app.use('/api', apiRouter);
// 404 handler (opsional, bisa diaktifkan jika perlu)
// app.use((_req, _res, next) => {
// next(createHttpError(404));
// });
// Scheduler (opsional, jika ingin dijalankan otomatis)
// const scheduleMidnight = schedule.scheduleJob('0 0 * * *', async () => {
// const result = await runningSchedule() ? 'success running midnight schedule' : 'failed running midnight schedule';
// console.log(result);
// });
// const scheduleNoon = schedule.scheduleJob('0 12 * * *', async () => {
// const result = await runningSchedule() ? 'success running noon schedule' : 'failed running noon schedule';
// console.log(result);
// });
// 🚨 5. Tangani CORS error secara eksplisit
app.use(((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err.message === 'Not allowed by CORS') {
console.error('❌ CORS error:', req.headers.origin);
return res.status(403).json({ error: 'CORS policy does not allow this origin.' });
}
next(err);
}) as express.ErrorRequestHandler);
// ❗ 6. Global error handler (dari kamu)
app.use(respondWithError);
export default app;
export default app;

View File

@ -1,94 +1,99 @@
import express, { NextFunction, Request, Response } from 'express';
import { body, matchedData, param } from 'express-validator';
import createHttpError from 'http-errors';
import { product_categories } from '../../database/models';
import expressValidatorErrorHandler from '../../middleware/expressValidatorErrorHandler';
import { body, matchedData, param } from "express-validator";
import expressValidatorErrorHandler from "../../middleware/expressValidatorErrorHandler";
import { NextFunction, Request, Response } from "express";
import { IProductCategoryTable } from "../../types/db-model";
import { createProductCategory, deleteProductCategory, showAllProductCategory, updateProductCategoryById } from "../../services/productCategoryServices";
import createHttpError from "http-errors";
import { TAPIResponse } from "../../types/core/http";
const router = express.Router();
// POST: Tambah kategori baru
const createCategory = [
body("category_name").notEmpty().withMessage("Category name is required"),
body("description").optional().isString(),
body("user_id").notEmpty().withMessage("User ID is required").isMongoId(),
const addProductCategory = [
body("category_name").notEmpty().isString(),
expressValidatorErrorHandler,
async (req: Request, res: Response, next: NextFunction) => {
const reqBody = matchedData<IProductCategoryTable>(req)
try {
const data = matchedData(req);
const category = new product_categories(data);
await category.save();
res.status(201).json({ message: "Category created", category });
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
const newProductId = await createProductCategory({
...reqBody,
category_name: reqBody.category_name.toLowerCase(),
user_id: req.user!.id
})
const result: TAPIResponse = {
success: true,
message: 'New product category successfully created.',
data: {
productCategoryId: newProductId
}
}
res.json(result)
} catch (error: unknown) {
next(createHttpError(500, error as Error))
}
}
];
// PATCH: Perbarui kategori
const updateCategory = [
param("id").isMongoId().withMessage("Invalid category ID"),
body("category_name").optional().isString(),
body("description").optional().isString(),
expressValidatorErrorHandler,
}
]
const getProductCategories = [
async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const data = matchedData(req);
const category = await product_categories.findByIdAndUpdate(id, data, { new: true });
if (!category) return next(createHttpError(404, "Category not found"));
res.json({ message: "Category updated", category });
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
const productCategories = await showAllProductCategory(req.user!.id)
console.log(productCategories)
const result: TAPIResponse = {
success: true,
data: productCategories
}
res.json(result)
} catch (error) {
next(createHttpError(500, error as Error))
}
}
]
// DELETE: Hapus kategori
const deleteCategory = [
param("id").isMongoId().withMessage("Invalid category ID"),
const deleteProductCategoryRoute = [
param('id'),
expressValidatorErrorHandler,
async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const category = await product_categories.findByIdAndDelete(id);
if (!category) return next(createHttpError(404, "Category not found"));
res.json({ message: "Category deleted" });
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
const { id } = matchedData(req)
await deleteProductCategory(id, req.user!.id)
const result: TAPIResponse = {
success: true,
message: 'Product category successfully deleted'
}
res.json(result)
} catch (error) {
next(createHttpError(500, error as Error))
}
}
]
// GET: Ambil satu kategori berdasarkan ID
const getCategory = [
param("id").isMongoId().withMessage("Invalid category ID"),
const updateProductCategory = [
param("id").notEmpty(),
body("category_name").notEmpty(),
expressValidatorErrorHandler,
async (req: Request, res: Response, next: NextFunction) => {
const reqParam = matchedData(req, { locations: ['params'] })
const reqBody = matchedData<IProductCategoryTable>(req, { locations: ['body'] })
try {
const { id } = req.params;
const category = await product_categories.findById(id);
if (!category) return next(createHttpError(404, "Category not found"));
res.json(category);
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
const newProductId = await updateProductCategoryById(reqParam.id, {
...reqBody,
category_name: reqBody.category_name.toLowerCase()
})
const result: TAPIResponse = {
success: true,
message: 'Product category successfully updated.',
data: {
productCategoryId: newProductId
}
}
res.json(result)
} catch (error: unknown) {
next(createHttpError(500, error as Error))
}
}
]
// GET: Ambil semua kategori
const getCategories = async (req: Request, res: Response, next: NextFunction) => {
try {
const categories = await product_categories.find();
res.json(categories);
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
}
}
export default {
createCategory,
updateCategory,
deleteCategory,
getCategory,
getCategories,
};
addProductCategory, getProductCategories, deleteProductCategoryRoute
}

View File

@ -0,0 +1,170 @@
import { body, matchedData, param, query } from "express-validator"
import expressValidatorErrorHandler from "../../middleware/expressValidatorErrorHandler"
import { NextFunction, Request, Response } from "express"
import { TAPIResponse } from "../../types/core/http"
import { IProductTable, ISupplierTable } from "../../types/db-model"
import createHttpError from "http-errors"
import parsePhoneNumberFromString from "libphonenumber-js"
import { createProduct, deleteProductById, getProductByProductCode, getProducts, showProductById, updateProductById } from "../../services/productServices"
const addProduct = [
body('product_code').notEmpty().isString(),
body('product_name').notEmpty().isString(),
body('stock')
.optional({ values: 'falsy' })
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
body('selling_price')
.optional({ values: 'falsy' })
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
body('buying_price')
.optional({ values: 'falsy' })
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
body('product_category_id')
.optional({ values: 'falsy' })
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
expressValidatorErrorHandler,
async (req: Request, res: Response, next: NextFunction) => {
const reqData = matchedData<IProductTable>(req)
try {
const newProductId = await createProduct({
...reqData,
buying_price: reqData.buying_price || null,
selling_price: reqData.selling_price || null,
stock: reqData.stock || 0,
product_category_id: reqData.product_category_id || null,
user_id: req.user!.id
})
const result: TAPIResponse = {
success: true,
message: 'New product successfully created.',
data: {
productId: newProductId
}
}
res.json(result)
} catch (error) {
next(createHttpError(500, error as Error))
}
}
]
const showAllProducts = [
query('page').optional().isInt(),
query('limit').optional().isInt(),
async (req: Request, res: Response, next: NextFunction) => {
try {
const { page = 1, limit = 10 } = matchedData(req)
const products = await getProducts(req.user!.id, page, limit);
const result: TAPIResponse = {
success: true,
data: products
}
res.json(result)
} catch (error) {
next(createHttpError(500, error as Error))
}
}
]
const getProductDetail = [
param('id'),
expressValidatorErrorHandler,
async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = matchedData(req)
const product = await showProductById(id)
const result: TAPIResponse = {
success: true,
data: product
}
res.json(result)
} catch (error) {
next(createHttpError(500, error as Error))
}
}
]
const updateProductRoute = [
param('id').notEmpty(),
body('product_code').notEmpty().isString(),
body('product_name').notEmpty().isString(),
body('stock')
.optional({ values: 'falsy' })
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
body('selling_price')
.optional({ values: 'falsy' })
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
body('buying_price')
.optional({ values: 'falsy' })
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
body('product_category_id')
.optional({ values: 'falsy' })
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
expressValidatorErrorHandler,
async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = matchedData(req, { locations: ['params'] })
const reqBody = matchedData<IProductTable>(req, { locations: ['body'] })
await updateProductById(id, {
...reqBody,
buying_price: reqBody.buying_price || null,
selling_price: reqBody.selling_price || null,
stock: reqBody.stock || 0,
product_category_id: reqBody.product_category_id || null,
})
const result: TAPIResponse = {
success: true,
message: 'Product data successfully updated'
}
res.json(result)
} catch (error) {
next(createHttpError(500, error as Error))
}
}
]
const deleteProductRoute = [
param('id'),
expressValidatorErrorHandler,
async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = matchedData(req)
await deleteProductById(id)
const result: TAPIResponse = {
success: true,
message: 'Product successfully deleted'
}
res.json(result)
} catch (error) {
next(createHttpError(500, error as Error))
}
}
]
const getProductByProductCodeRoute = [
param('product_code'),
expressValidatorErrorHandler,
async (req: Request, res: Response, next: NextFunction) => {
try {
const { product_code } = matchedData(req)
const product = await getProductByProductCode(
req.user!.id,
product_code,
)
if (!product) {
return next(createHttpError(404, 'Produk tidak ditemukan'))
}
const result: TAPIResponse = {
success: true,
data: product
}
res.json(result)
} catch (error) {
next(createHttpError(500, error as Error))
}
}
]
export default { addProduct, showAllProducts, getProductDetail, updateProductRoute, deleteProductRoute, getProductByProductCodeRoute }

View File

@ -10,18 +10,19 @@ import parsePhoneNumberFromString from "libphonenumber-js"
const addSupplier = [
body('supplier_name').notEmpty().isString(),
body('contact')
.optional({ nullable: true })
.optional({ values: 'falsy' })
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
body('address').optional().isString(),
expressValidatorErrorHandler,
async (req: Request, res: Response, next: NextFunction) => {
const reqData = matchedData<ISupplierTable>(req)
const phone = parsePhoneNumberFromString(`+${reqData.contact}`);
if (!phone?.isValid())
next(createHttpError(400, 'Invalid phone number format. Please match international format.'))
if (!phone?.isValid() && !!reqData.contact)
return next(createHttpError(400, 'Invalid phone number format. Please match international format.'))
try {
const newSupplierId = await createSupplier({
...reqData,
contact: reqData.contact || null,
user_id: req.user!.id
})
const result: TAPIResponse = {
@ -78,7 +79,7 @@ const updateSupplierRoute = [
param('id'),
body('supplier_name').notEmpty().isString(),
body('contact')
.optional({ nullable: true })
.optional({ values: 'falsy' })
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
body('address').optional().isString(),
expressValidatorErrorHandler,
@ -87,9 +88,12 @@ const updateSupplierRoute = [
const { id } = matchedData(req, { locations: ['params'] })
const reqBody = matchedData<ISupplierTable>(req, { locations: ['body'] })
const phone = parsePhoneNumberFromString(`+${reqBody.contact}`);
if (!phone?.isValid())
next(createHttpError(400, 'Invalid phone number format. Please match international format.'))
await updateSupplier(id, reqBody)
if (!phone?.isValid() && !!reqBody.contact)
return next(createHttpError(400, 'Invalid phone number format. Please match international format.'))
await updateSupplier(id, {
...reqBody,
contact: reqBody.contact || null
})
const result: TAPIResponse = {
success: true,
message: 'Supplier data successfully updated'

View File

@ -4,7 +4,7 @@ import { body, param, matchedData } from "express-validator";
import createHttpError from "http-errors";
import validate from "../../middleware/expressValidatorErrorHandler"; // Validation middleware
import * as transactionsRepository from "../../repository/transaction"; // Repository untuk Transactions dalam bentuk mongoose
import * as productsRepository from "../../repository/products"; // Untuk cek produk
import * as productsRepository from "../../repository/productsRepository"; // Untuk cek produk
import { NextFunction, Request, Response } from "express";
import { ITransaction } from "../../types/db-model";

13
src/dev-core.ts Normal file
View File

@ -0,0 +1,13 @@
import os from 'os';
export function getLocalIP(): string {
const interfaces = os.networkInterfaces();
for (const name in interfaces) {
for (const iface of interfaces[name]!) {
if (iface.family === "IPv4" && !iface.internal) {
return iface.address;
}
}
}
return "127.0.0.1";
}

View File

@ -0,0 +1,19 @@
import { db } from "../database/MySQL";
import { IProductCategoryTable } from "../types/db-model";
export async function insertProductCategory(data: IProductCategoryTable) {
const [id] = await db<IProductCategoryTable>('product_categories').insert(data)
return id
}
export function selectAllProductCategory(user_id: IProductCategoryTable['user_id']) {
return db<IProductCategoryTable>('product_categories').where({ user_id })
}
export function selectProductCategoryById(id: IProductCategoryTable['id']) {
return db<IProductCategoryTable>('product_categories').where({ id })
}
export function selectProductCategoryByName(category_name: IProductCategoryTable['category_name']) {
return db<IProductCategoryTable>('product_categories').where({ category_name })
}

View File

@ -1,64 +0,0 @@
// repository/productsRepository.ts
import { db } from "../database/MySQL"; // Koneksi database menggunakan Knex
import { IProduct } from "../types/db-model";
const TABLE_NAME = "products";
// Tambah produk baru
export const createProduct = async (data: IProduct) => {
try {
const [product] = await db(TABLE_NAME).insert({
product_name: data.product_name,
price: data.price,
product_category_id: data.product_category_id,
}).returning("*");
return product;
} catch (err) {
throw new Error("Error creating product");
}
};
// Update produk by ID
export const updateProductById = async (id: number, data: Partial<IProduct>) => {
try {
const [updatedProduct] = await db(TABLE_NAME)
.where({ id })
.update(data)
.returning("*");
return updatedProduct;
} catch (err) {
throw new Error("Error updating product");
}
};
// Hapus produk by ID
export const deleteProductById = async (id: number) => {
try {
const deleted = await db(TABLE_NAME).where({ id }).del();
return deleted;
} catch (err) {
throw new Error("Error deleting product");
}
};
// Ambil produk by ID
export const getProductById = async (id: number) => {
try {
const product = await db(TABLE_NAME).where({ id }).first();
return product;
} catch (err) {
throw new Error("Error fetching product");
}
};
// Ambil semua produk
export const getAllProducts = async () => {
try {
return await db(TABLE_NAME);
} catch (err) {
throw new Error("Error fetching all products");
}
};

View File

@ -0,0 +1,58 @@
import { db } from "../database/MySQL";
import { IProductTable } from "../types/db-model";
export const insertProduct = async (data: IProductTable) => {
const [id] = await db<IProductTable>('products').insert(data)
return id
}
export const selectProductsByUserId = (user_id: IProductTable['user_id']) => {
return db<IProductTable>('products').where({ user_id })
}
export const selectProductById = (id: IProductTable['id']) => {
return db<IProductTable>('products').where({ id })
}
export const getAllProducts = (user_id: number, limit: number, offset: number) => {
return db<IProductTable>('products as p')
.leftJoin('product_categories as c', 'p.product_category_id', 'c.id')
.select('p.*', 'c.category_name')
.where('p.user_id', user_id)
.where('p.deleted', false)
.limit(limit)
.offset(offset)
}
export const countProducts = async (id_user: number) => {
const result = await db<IProductTable>('products')
.where({ user_id: id_user, deleted: false })
.count('id as count')
.first<{
count: string;
}>();
return parseInt(result?.count || '0');
};
export const selectProductByCategoryId = (product_category_id: IProductTable['product_category_id']) => {
return db<IProductTable>('products').where({ product_category_id })
}
export const countProductsByCategory = async (product_category_id: IProductTable['product_category_id']) => {
const result = await db<IProductTable>('products')
.where({ product_category_id })
.count('id as count')
.first<{
count: string;
}>();
return parseInt(result?.count || '0');
};
export const selectProductByProductCode = (product_code: IProductTable['product_code'], user_id: IProductTable['user_id']) => {
return db<IProductTable>('products').where({
product_code,
user_id,
})
}

View File

@ -1,6 +1,7 @@
import express from 'express'
var router = express.Router();
import authRoute from '../../controller/api/authController';
import authenticate from '../../middleware/authMiddleware';
router.post('/register', authRoute.register);
router.post('/login', authRoute.login);
@ -10,6 +11,6 @@ router.patch('/forgot-password/:token', authRoute.forgotPasswordChangePassword);
router.get('/verify/:token', authRoute.verify);
router.post('/re-send-email-activation/:token', authRoute.resendEmailVerification);
router.get('/refresh-token', authRoute.refreshToken);
router.get('/logout', authRoute.logout);
router.get('/logout', authenticate, authRoute.logout);
export default router

View File

@ -3,15 +3,9 @@ var router = express.Router();
import authEndpoint from './auth'
import supplierEndpoint from './supplier'
import productCategoryEndpoint from './product_category'
import productEndpoint from './product'
import authenticate from '../../middleware/authMiddleware';
// import productCategory from '../../controller/api/productCategoriesController';
// import product from '../../controller/api/productsController';
// import stockForecastingController from '../../controller/api/stockForecasting';
// import stockPurchaseController from '../../controller/api/stockPurchases';
// import supplierController from '../../controller/api/suppliers';
// import transactionController from '../../controller/api/transactions';
// import userFileController from '../../controller/api/userFiles';
// const {jwtInfluencerMiddleware} = require("../middleware/authMiddleware");
router.get('/', function (req, res, next) {
res.send('Read docs.');
@ -27,5 +21,7 @@ router.get('/is-authenticated', (req, res) => {
})
router.use('/', supplierEndpoint)
router.use('/', productEndpoint)
router.use('/', productCategoryEndpoint)
export default router;

14
src/routes/api/product.ts Normal file
View File

@ -0,0 +1,14 @@
import express from 'express'
var router = express.Router();
import productController from '../../controller/api/productsController';
import authenticate from '../../middleware/authMiddleware';
router.use('/', authenticate)
router.post('/product', productController.addProduct);
router.get('/product/:product_code', productController.getProductByProductCodeRoute);
router.get('/products', productController.showAllProducts);
router.get('/product/:id', productController.getProductDetail);
router.patch('/product/:id', productController.updateProductRoute);
router.delete('/product/:id', productController.deleteProductRoute);
export default router

View File

@ -0,0 +1,12 @@
import express from 'express'
var router = express.Router();
import productController from '../../controller/api/productCategoriesController';
import authenticate from '../../middleware/authMiddleware';
router.use('/', authenticate)
router.post('/product-category', productController.addProductCategory);
router.post('/product-category/:id', productController.addProductCategory);
router.get('/product-categories', productController.getProductCategories);
router.delete('/product-category/:id', productController.deleteProductCategoryRoute);
export default router

View File

@ -1,11 +1,14 @@
// src/server.ts
import app from './app';
import http from 'http';
import os from 'os';
import { getLocalIP } from './dev-core';
const PORT = process.env.PORT || 3000;
const PORT = parseInt(process.env.PORT || "5000", 10);
const localIP = getLocalIP();
const server = http.createServer(app);
server.listen(PORT, () => {
console.log(`🚀 Server running at http://localhost:${PORT}`);
server.listen(PORT, "0.0.0.0", () => {
console.log(`🚀 Server running at http://${localIP}:${PORT}`);
});

View File

@ -0,0 +1,26 @@
import { insertProductCategory, selectAllProductCategory, selectProductCategoryById, selectProductCategoryByName } from "../repository/productCategoriesRepository";
import { selectProductByCategoryId } from "../repository/productsRepository";
import { IProductCategoryTable } from "../types/db-model";
export const createProductCategory = (data: IProductCategoryTable) => insertProductCategory(data)
export const showAllProductCategory = (user_id: IProductCategoryTable['user_id']) => selectAllProductCategory(user_id)
export const deleteProductCategory = async (id: IProductCategoryTable['id'], user_id: IProductCategoryTable['user_id']) => {
const otherCategory = await selectProductCategoryByName('Other').first()
let otherCategoryId = otherCategory?.id
if (!otherCategoryId) {
otherCategoryId = await createProductCategory({
category_name: 'Other',
user_id,
} as IProductCategoryTable)
}
await selectProductByCategoryId(id).where({
product_category_id: id
}).update({
product_category_id: otherCategoryId
})
return selectProductCategoryById(id).delete()
}
export const updateProductCategoryById = async (id: IProductCategoryTable['id'], data: IProductCategoryTable) => selectProductCategoryById(id).update(data)

View File

@ -0,0 +1,48 @@
import { countProducts, getAllProducts, insertProduct, selectProductById, selectProductByProductCode, selectProductsByUserId } from "../repository/productsRepository"
import { IProductTable } from "../types/db-model"
export const createProduct = async (data: IProductTable) => {
const product = await selectProductByProductCode(data.product_code, data.user_id).first()
if (product) {
return await updateProductById(product.id, {
deleted: false
})
} else {
return await insertProduct(data)
}
}
export const getProducts = async (
id_user: number,
page = 1,
limit = 10
) => {
const safePage = Math.max(1, Number(page));
const safeLimit = Math.max(1, Number(limit));
const offset = (safePage - 1) * safeLimit;
const [data, total] = await Promise.all([
getAllProducts(id_user, safeLimit, offset),
countProducts(id_user)
]);
return {
data,
meta: {
page: safePage,
limit: safeLimit,
total,
totalPages: Math.ceil(total / safeLimit)
}
};
};
export const showProductById = async (id: IProductTable['id']) => selectProductById(id).first()
export const updateProductById = async (id: IProductTable['id'], data: Partial<IProductTable>) => selectProductById(id).update(data)
export const deleteProductById = async (id: IProductTable['id']) => {
return selectProductById(id).update({
deleted: true
})
}
export const getProductByProductCode = (
user_id: IProductTable['user_id'],
product_code: IProductTable['product_code']
) => selectProductByProductCode(product_code, user_id).first()

View File

@ -13,22 +13,24 @@ export interface IProductCategoryTable {
user_id: number;
category_name: string;
id: number;
description: string;
}
export interface IProductTable {
id: number;
product_name: string;
product_code: string;
stock: number;
price: number;
buying_price: number | null;
selling_price: number | null;
product_category_id: number | null;
user_id: number;
product_category_id: number;
deleted: boolean
}
export interface ISupplierTable {
id: number;
supplier_name: string;
contact: string;
contact: string | null;
address: string;
user_id: number;
}