This commit is contained in:
fhm 2025-05-24 13:49:21 +07:00
parent da539298fb
commit ecb30ae790
21 changed files with 438 additions and 248 deletions

106
package-lock.json generated
View File

@ -35,7 +35,8 @@
"node-schedule": "^2.1.1",
"nodemailer": "^6.10.0",
"nodemon": "^3.1.9",
"pg": "^8.11.1"
"pg": "^8.11.1",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
@ -694,6 +695,15 @@
"node": ">=0.4.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@ -1062,6 +1072,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -1111,6 +1134,15 @@
"node": ">=10"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -1330,6 +1362,18 @@
"node": ">= 0.10"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@ -1793,6 +1837,15 @@
"node": ">= 0.6"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@ -3886,6 +3939,18 @@
"node": ">= 0.6"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -4311,12 +4376,51 @@
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -36,7 +36,8 @@
"node-schedule": "^2.1.1",
"nodemailer": "^6.10.0",
"nodemon": "^3.1.9",
"pg": "^8.11.1"
"pg": "^8.11.1",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",

View File

@ -191,13 +191,13 @@ const login = [
await updateUserById(user.id, {
refresh_token: refreshToken
})
const isProd = process.env.NODE_ENV === 'production';
// Kirim token sebagai cookie
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 15 * 24 * 60 * 60 * 1000
secure: isProd,
sameSite: isProd ? 'none' : 'lax',
maxAge: 30 * 24 * 60 * 60 * 1000
});
const resultResponse: TAPIResponse = {

View File

@ -36,7 +36,6 @@ const getProductCategories = [
async (req: Request, res: Response, next: NextFunction) => {
try {
const productCategories = await showAllProductCategory(req.user!.id)
console.log(productCategories)
const result: TAPIResponse = {
success: true,
data: productCategories

View File

@ -38,7 +38,8 @@ const addProduct = [
success: true,
message: 'New product successfully created.',
data: {
productId: newProductId
...reqData,
id: newProductId,
}
}
res.json(result)
@ -86,8 +87,8 @@ const getProductDetail = [
const updateProductRoute = [
param('id').notEmpty(),
body('product_code').notEmpty().isString(),
body('product_name').notEmpty().isString(),
body('product_code').optional({ values: 'falsy' }).isString(),
body('product_name').optional({ values: 'falsy' }).isString(),
body('stock')
.optional({ values: 'falsy' })
.isNumeric({ no_symbols: true }).withMessage('Contact must contain only digits (0-9), no symbols allowed'),
@ -105,16 +106,11 @@ const updateProductRoute = [
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,
})
await updateProductById(id, reqBody)
const result: TAPIResponse = {
success: true,
message: 'Product data successfully updated'
message: 'Product data successfully updated',
data: await showProductById(id)
}
res.json(result)
} catch (error) {

View File

@ -1,115 +0,0 @@
import express, { NextFunction, Request, Response } from "express";
import { body, param, matchedData } from "express-validator";
import createHttpError from "http-errors";
import { purchases, products, suppliers } from "../../database/models";
import validate from "../../middleware/expressValidatorErrorHandler";
// 🟢 **POST: Tambah Pembelian Stok Baru**
const createStockPurchase = [
body("amount").notEmpty().isNumeric().withMessage("Amount must be a number"),
body("total_price").notEmpty().isNumeric().withMessage("Total price must be a number"),
body("user_id").notEmpty().isMongoId().withMessage("Invalid user ID"),
body("product_id").notEmpty().isMongoId().withMessage("Invalid product ID"),
body("supplier_id").notEmpty().isMongoId().withMessage("Invalid supplier ID"),
validate,
async (req: Request, res: Response, next: NextFunction) => {
try {
const data = matchedData(req);
// Cek apakah produk ada
const productExists = await products.findById(data.product_id);
if (!productExists) return next(createHttpError(404, "Product not found"));
// Cek apakah supplier ada
const supplierExists = await suppliers.findById(data.supplier_id);
if (!supplierExists) return next(createHttpError(404, "Supplier not found"));
// Simpan data pembelian
const purchase = new purchases(data);
await purchase.save();
res.status(201).json({ message: "Stock purchase created", purchase });
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
}
}
];
// 🟠 **PATCH: Perbarui Pembelian Stok**
const updateStockPurchase = [
param("id").isMongoId().withMessage("Invalid purchase ID"),
body("amount").optional().isNumeric().withMessage("Amount must be a number"),
body("total_price").optional().isNumeric().withMessage("Total price must be a number"),
body("supplier_id").optional().isMongoId().withMessage("Invalid supplier ID"),
validate,
async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const data = matchedData(req);
if (data.supplier_id) {
const supplierExists = await suppliers.findById(data.supplier_id);
if (!supplierExists) return next(createHttpError(404, "Supplier not found"));
}
const purchase = await purchases.findByIdAndUpdate(id, data, { new: true });
if (!purchase) return next(createHttpError(404, "Stock purchase not found"));
res.json({ message: "Stock purchase updated", purchase });
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
}
}
];
// 🔴 **DELETE: Hapus Pembelian Stok**
const deleteStockPurchase = [
param("id").isMongoId().withMessage("Invalid purchase ID"),
validate,
async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const purchase = await purchases.findByIdAndDelete(id);
if (!purchase) return next(createHttpError(404, "Stock purchase not found"));
res.json({ message: "Stock purchase deleted" });
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
}
}
];
// 🔵 **GET: Ambil Satu Pembelian Stok Berdasarkan ID**
const getStockPurchase = [
param("id").isMongoId().withMessage("Invalid purchase ID"),
validate,
async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const purchase = await purchases.findById(id).populate("product_id supplier_id user_id");
if (!purchase) return next(createHttpError(404, "Stock purchase not found"));
res.json(purchase);
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
}
}
];
// 🟡 **GET: Ambil Semua Pembelian Stok (Histori)**
const getStockPurchases = async (req: Request, res: Response, next: NextFunction) => {
try {
const allPurchases = await purchases.find().populate("product_id supplier_id user_id");
res.json(allPurchases);
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
}
};
export default {
createStockPurchase,
updateStockPurchase,
deleteStockPurchase,
getStockPurchase,
getStockPurchases
};

View File

@ -0,0 +1,120 @@
import { body, matchedData, query } from "express-validator";
import expressValidatorErrorHandler from "../../middleware/expressValidatorErrorHandler";
import { NextFunction, Request, Response } from "express";
import { TAPIResponse } from "../../types/core/http";
import { addRestocksRecords, showRestockHistory } from "../../services/restockServices";
import createHttpError from "http-errors";
import { addTransactionRecords, showSalesHistory } from "../../services/transactionServices";
const createRestockRecord = [
body('data').isArray().withMessage('Body must be array'),
body('data.*.product_code')
.isString().withMessage('product_code must be string')
.notEmpty().withMessage('product_code is required'),
body('data.*.product_name')
.isString().withMessage('product_name must be string')
.notEmpty().withMessage('product_name is required'),
body('data.*.price')
.isNumeric().withMessage('price must be number'),
body('data.*.amount')
.isNumeric().withMessage('amount must be number'),
expressValidatorErrorHandler,
async (req: Request, res: Response, next: NextFunction) => {
const { data }: {
data: {
product_code: string;
product_name: string;
price: number;
amount: number;
}[]
} = matchedData(req, { locations: ['body'] });
try {
await addRestocksRecords(data, req.user!.id)
const result: TAPIResponse = {
success: true,
message: 'Restock successful.',
}
res.json(result)
} catch (error) {
next(createHttpError(500, error as Error))
}
}
]
const createTransactionRecord = [
body('data').isArray().withMessage('Body must be array'),
body('data.*.product_code')
.isString().withMessage('product_code must be string')
.notEmpty().withMessage('product_code is required'),
body('data.*.product_name')
.isString().withMessage('product_name must be string')
.notEmpty().withMessage('product_name is required'),
body('data.*.price')
.isNumeric().withMessage('price must be number'),
body('data.*.amount')
.isNumeric().withMessage('amount must be number'),
expressValidatorErrorHandler,
async (req: Request, res: Response, next: NextFunction) => {
const { data }: {
data: {
product_code: string;
product_name: string;
price: number;
amount: number;
}[]
} = matchedData(req, { locations: ['body'] });
try {
await addTransactionRecords(data, req.user!.id)
const result: TAPIResponse = {
success: true,
message: 'Transaction successful.',
}
res.json(result)
} catch (error) {
next(createHttpError(500, error as Error))
}
}
]
const showRestockHistoryRoute = [
query('page').optional().isInt(),
query('limit').optional().isInt(),
async (req: Request, res: Response, next: NextFunction) => {
try {
const { page = 1, limit = 10 } = matchedData(req)
const restockHistory = await showRestockHistory(req.user!.id, page, limit)
const result: TAPIResponse = {
success: true,
data: restockHistory
}
res.json(result)
} catch (error) {
next(createHttpError(500, error as Error))
}
}
]
const showSalesHistoryRoute = [
query('page').optional().isInt(),
query('limit').optional().isInt(),
async (req: Request, res: Response, next: NextFunction) => {
try {
const { page = 1, limit = 10 } = matchedData(req)
const restockHistory = await showSalesHistory(req.user!.id, page, limit)
const result: TAPIResponse = {
success: true,
data: restockHistory
}
res.json(result)
} catch (error) {
next(createHttpError(500, error as Error))
}
}
]
export default {
createRestockRecord,
createTransactionRecord,
showRestockHistoryRoute,
showSalesHistoryRoute
}

View File

@ -1,108 +0,0 @@
// controllers/transactionsController.ts
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/productsRepository"; // Untuk cek produk
import { NextFunction, Request, Response } from "express";
import { ITransaction } from "../../types/db-model";
// 🟢 **POST: Tambah Transaksi Baru**
const createTransaction = [
body("amount").notEmpty().isNumeric().withMessage("Amount must be a number"),
body("total_price").notEmpty().isNumeric().withMessage("Total price must be a number"),
body("user_id").notEmpty().isInt().withMessage("Invalid user ID"), // isMongoId() diganti isInt()
body("product_id").notEmpty().isInt().withMessage("Invalid product ID"), // isMongoId() diganti isInt()
validate,
async (req: Request, res: Response, next: NextFunction) => {
try {
const data = matchedData<ITransaction>(req);
// Cek apakah produk ada
const productExists = await productsRepository.getProductById(data.product_id);
if (!productExists) return next(createHttpError(404, "Product not found"));
// Simpan data transaksi
const transaction = await transactionsRepository.createTransaction(data);
res.status(201).json({ message: "Transaction created", transaction });
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
}
}
];
// 🟠 **PATCH: Perbarui Transaksi**
const updateTransaction = [
param("id").isInt().withMessage("Invalid transaction ID"), // isMongoId() diganti isInt()
body("amount").optional().isNumeric().withMessage("Amount must be a number"),
body("total_price").optional().isNumeric().withMessage("Total price must be a number"),
validate,
async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const data = matchedData<ITransaction>(req);
const transaction = await transactionsRepository.updateTransactionById(Number(id), data);
if (!transaction) return next(createHttpError(404, "Transaction not found"));
res.json({ message: "Transaction updated", transaction });
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
}
}
];
// 🔴 **DELETE: Hapus Transaksi**
const deleteTransaction = [
param("id").isInt().withMessage("Invalid transaction ID"), // isMongoId() diganti isInt()
validate,
async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const transaction = await transactionsRepository.deleteTransactionById(Number(id));
if (!transaction) return next(createHttpError(404, "Transaction not found"));
res.json({ message: "Transaction deleted" });
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
}
}
];
// 🔵 **GET: Ambil Satu Transaksi Berdasarkan ID**
const getTransaction = [
param("id").isInt().withMessage("Invalid transaction ID"), // isMongoId() diganti isInt()
validate,
async (req: Request, res: Response, next: NextFunction) => {
try {
const { id } = req.params;
const transaction = await transactionsRepository.getTransactionById(Number(id));
if (!transaction) return next(createHttpError(404, "Transaction not found"));
res.json(transaction);
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
}
}
];
// 🟡 **GET: Ambil Semua Transaksi**
const getTransactions = async (req: Request, res: Response, next: NextFunction) => {
try {
const allTransactions = await transactionsRepository.getAllTransactions();
res.json(allTransactions);
} catch (err) {
next(createHttpError(500, "Internal Server Error", { cause: err }));
}
};
export default {
createTransaction,
updateTransaction,
deleteTransaction,
getTransaction,
getTransactions
};

View File

@ -8,7 +8,6 @@ const authenticate: RequestHandler = (req, res, next) => {
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return next(createHttpError(401, "Access token required"));
}
const token = authHeader.split(" ")[1];
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {

View File

@ -55,4 +55,10 @@ export const selectProductByProductCode = (product_code: IProductTable['product_
product_code,
user_id,
})
}
export const selectProductByProductCodes = (product_codes: IProductTable['product_code'][], user_id: IProductTable['user_id']) => {
return db<IProductTable>('products').where({
user_id,
}).whereIn('product_code', product_codes)
}

View File

@ -0,0 +1,29 @@
import { db } from "../database/MySQL";
import { IPurchaseTable } from "../types/db-model";
export function insertsDataToRestock(data: Partial<IPurchaseTable>[]) {
return db<IPurchaseTable>('restocks').insert(data)
}
export function selectAllRestockHistory(
user_id: IPurchaseTable['user_id'],
limit: number, offset: number
) {
return db<IPurchaseTable>('restocks as r')
.leftJoin('products as p', 'p.id', 'r.product_id')
.select('r.id', 'p.product_name', 'r.buying_date', 'r.price', 'r.amount', 'r.total_price')
.where('p.user_id', user_id)
.limit(limit)
.offset(offset);
}
export const countRestocks = async (id_user: number) => {
const result = await db('restocks')
.where({ user_id: id_user })
.count('id as count')
.first<{
count: string;
}>();
return parseInt(result?.count || '0');
};

View File

@ -22,7 +22,6 @@ export const getAllSupplier = (user_id: number, limit: number, offset: number) =
.offset(offset);
}
export const countSuppliers = async (id_user: number) => {
const result = await db('suppliers')
.where({ user_id: id_user })
@ -34,7 +33,6 @@ export const countSuppliers = async (id_user: number) => {
return parseInt(result?.count || '0');
};
export const updateSupplierById = (id: number, data: ISupplierTable) => {
return getSupplierById(id).update(data)
}

View File

@ -0,0 +1,29 @@
import { db } from "../database/MySQL";
import { ITransactionTable } from "../types/db-model";
export function insertsDataToTransaction(data: Partial<ITransactionTable>[]) {
return db<ITransactionTable>('transactions').insert(data)
}
export function selectAllSalesHistory(
user_id: ITransactionTable['user_id'],
limit: number, offset: number
) {
return db<ITransactionTable>('transactions as t')
.leftJoin('products as p', 'p.id', 't.product_id')
.select('t.id', 'p.product_name', 't.transaction_date', 't.price', 't.amount', 't.total_price')
.where('p.user_id', user_id)
.limit(limit)
.offset(offset);
}
export const countSales = async (id_user: number) => {
const result = await db('transactions')
.where({ user_id: id_user })
.count('id as count')
.first<{
count: string;
}>();
return parseInt(result?.count || '0');
};

View File

@ -11,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', authenticate, authRoute.logout);
router.get('/logout', authRoute.logout);
export default router

View File

@ -4,6 +4,7 @@ var router = express.Router();
import authEndpoint from './auth'
import supplierEndpoint from './supplier'
import productCategoryEndpoint from './product_category'
import trxEndpoint from './trx'
import productEndpoint from './product'
import authenticate from '../../middleware/authMiddleware';
@ -23,5 +24,6 @@ router.get('/is-authenticated', (req, res) => {
router.use('/', supplierEndpoint)
router.use('/', productEndpoint)
router.use('/', productCategoryEndpoint)
router.use('/', trxEndpoint)
export default router;

12
src/routes/api/trx.ts Normal file
View File

@ -0,0 +1,12 @@
import express from 'express'
var router = express.Router();
import transactionController from '../../controller/api/transactionController';
import authenticate from '../../middleware/authMiddleware';
router.use('/', authenticate)
router.post('/restocks', transactionController.createRestockRecord);
router.post('/transactions', transactionController.createTransactionRecord);
router.get('/restocks-history', transactionController.showRestockHistoryRoute);
router.get('/sales-history', transactionController.showSalesHistoryRoute);
export default router

View File

@ -1,4 +1,4 @@
import { countProducts, getAllProducts, insertProduct, selectProductById, selectProductByProductCode, selectProductsByUserId } from "../repository/productsRepository"
import { countProducts, getAllProducts, insertProduct, selectProductById, selectProductByProductCode, selectProductByProductCodes, selectProductsByUserId } from "../repository/productsRepository"
import { IProductTable } from "../types/db-model"
export const createProduct = async (data: IProductTable) => {
@ -45,4 +45,9 @@ export const deleteProductById = async (id: IProductTable['id']) => {
export const getProductByProductCode = (
user_id: IProductTable['user_id'],
product_code: IProductTable['product_code']
) => selectProductByProductCode(product_code, user_id).first()
) => selectProductByProductCode(product_code, user_id).first()
export const getProductByProductCodes = (
user_id: IProductTable['user_id'],
product_codes: IProductTable['product_code'][]
) => selectProductByProductCodes(product_codes, user_id)

View File

@ -0,0 +1,54 @@
import { countRestocks, insertsDataToRestock, selectAllRestockHistory } from "../repository/restockRepository";
import { IProductTable, IPurchaseTable } from "../types/db-model";
import { getProductByProductCodes, updateProductById } from "./productServices";
export async function addRestocksRecords(data: {
product_code: string;
product_name: string;
price: number;
amount: number;
}[], user_id: IProductTable['user_id']) {
const product_codes = data.map(v => v.product_code)
const products = await getProductByProductCodes(user_id, product_codes)
const mergedData: Partial<IPurchaseTable>[] = []
products.forEach((p) => {
const pCode = p.product_code
const dataMatched = data.find(v => v.product_code === pCode)
mergedData.push({
amount: dataMatched!.amount,
product_id: p.id,
user_id,
price: Number(p.buying_price),
})
updateProductById(p.id, {
stock: p.stock + (Number(dataMatched?.amount) || 0)
})
})
return insertsDataToRestock(mergedData)
}
export async function showRestockHistory(
user_id: IPurchaseTable['user_id'],
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([
selectAllRestockHistory(user_id, safeLimit, offset),
countRestocks(user_id)
]);
return {
data,
meta: {
page: safePage,
limit: safeLimit,
total,
totalPages: Math.ceil(total / safeLimit)
}
};
return
}

View File

@ -0,0 +1,55 @@
import { countSales, insertsDataToTransaction, selectAllSalesHistory } from "../repository/transactionRepository";
import { IProductTable, ITransactionTable } from "../types/db-model";
import { getProductByProductCodes, updateProductById } from "./productServices";
export async function addTransactionRecords(data: {
product_code: string;
product_name: string;
price: number;
amount: number;
}[], user_id: IProductTable['user_id']) {
const product_codes = data.map(v => v.product_code)
const products = await getProductByProductCodes(user_id, product_codes)
const mergedData: Partial<ITransactionTable>[] = []
products.forEach((p) => {
const pCode = p.product_code
const dataMatched = data.find(v => v.product_code === pCode)
mergedData.push({
amount: dataMatched!.amount,
product_id: p.id,
user_id,
price: Number(p.selling_price),
})
updateProductById(p.id, {
stock: (Number(dataMatched?.amount) || 0) - p.stock
})
})
return insertsDataToTransaction(mergedData)
}
export async function showSalesHistory(
user_id: ITransactionTable['user_id'],
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([
selectAllSalesHistory(user_id, safeLimit, offset),
countSales(user_id)
]);
return {
data,
meta: {
page: safePage,
limit: safeLimit,
total,
totalPages: Math.ceil(total / safeLimit)
}
};
return
}

View File

@ -39,6 +39,7 @@ export interface IPurchaseTable {
id: number;
buying_date: Date;
amount: number;
price: number;
total_price: number;
user_id: number;
product_id: number;
@ -52,6 +53,7 @@ export interface ITransactionTable {
total_price: number;
user_id: number;
product_id: number;
price: number
}
export interface IPredictionTable {

View File

@ -4,11 +4,13 @@ import { TAccessToken, TRefreshToken } from '../../types/jwt';
import { IUserTable } from '../../types/db-model';
dotenv.config()
const isProd = process.env.NODE_ENV === 'production';
const generateAccessToken = (user: IUserTable) => {
return jwt.sign(
{ id: user.id, email: user.email } as TAccessToken,
process.env.JWT_SECRET,
{ expiresIn: "15m" }
{ expiresIn: isProd ? "15m" : '30d' }
);
};