From 2fd0f7cf6bd382c8ac85b6b50600cc3266a47855 Mon Sep 17 00:00:00 2001 From: Aji Kamaludin Date: Mon, 9 Aug 2021 17:31:01 +0700 Subject: [PATCH] sales impemented --- ...628181389101_create-company-transaction.js | 20 +++ src/api/authentications/handler.js | 2 + src/api/sales/handler.js | 74 +++++++++++ src/api/sales/index.js | 11 ++ src/api/sales/routes.js | 28 ++++ src/server.js | 13 ++ src/services/postgres/SalesServive.js | 121 ++++++++++++++++++ src/services/postgres/UsersService.js | 22 ++++ src/validator/sales/index.js | 20 +++ src/validator/sales/schema.js | 24 ++++ 10 files changed, 335 insertions(+) create mode 100644 src/api/sales/handler.js create mode 100644 src/api/sales/index.js create mode 100644 src/api/sales/routes.js create mode 100644 src/services/postgres/SalesServive.js create mode 100644 src/validator/sales/index.js create mode 100644 src/validator/sales/schema.js diff --git a/migrations/1628181389101_create-company-transaction.js b/migrations/1628181389101_create-company-transaction.js index d0d59c7..4703cb8 100644 --- a/migrations/1628181389101_create-company-transaction.js +++ b/migrations/1628181389101_create-company-transaction.js @@ -31,6 +31,10 @@ exports.up = (pgm) => { type: 'numeric(16,2)', notNull: false, }, + created_by: { + type: 'uuid', + notNull: true, + }, created_at: { type: 'timestamp', notNull: true, @@ -49,6 +53,11 @@ exports.up = (pgm) => { columns: 'office_id', onDelete: 'CASCADE', }, + { + references: 'users(id)', + columns: 'created_by', + onDelete: 'CASCADE', + }, ], }, }); @@ -57,6 +66,7 @@ exports.up = (pgm) => { id: { type: 'uuid', primaryKey: true, + default: pgm.func('uuid_generate_v4()'), }, sale_id: { type: 'uuid', @@ -130,6 +140,10 @@ exports.up = (pgm) => { type: 'numeric(16,2)', notNull: false, }, + created_by: { + type: 'uuid', + notNull: true, + }, created_at: { type: 'timestamp', notNull: true, @@ -148,6 +162,11 @@ exports.up = (pgm) => { columns: 'office_id', onDelete: 'CASCADE', }, + { + references: 'users(id)', + columns: 'created_by', + onDelete: 'CASCADE', + }, ], }, }); @@ -156,6 +175,7 @@ exports.up = (pgm) => { id: { type: 'uuid', primaryKey: true, + default: pgm.func('uuid_generate_v4()'), }, purchase_id: { type: 'uuid', diff --git a/src/api/authentications/handler.js b/src/api/authentications/handler.js index 0a256c3..7fb36c9 100644 --- a/src/api/authentications/handler.js +++ b/src/api/authentications/handler.js @@ -21,6 +21,7 @@ class AuthenticationsHandler { const refreshToken = this._tokenManager.generateRefreshToken({ id, companyId }); await this._authenticationsService.addRefreshToken(refreshToken); + const user = await this._usersService.getMe(id); const response = h.response({ status: 'success', @@ -28,6 +29,7 @@ class AuthenticationsHandler { data: { accessToken, refreshToken, + user, }, }); response.code(201); diff --git a/src/api/sales/handler.js b/src/api/sales/handler.js new file mode 100644 index 0000000..b1f8599 --- /dev/null +++ b/src/api/sales/handler.js @@ -0,0 +1,74 @@ +class SalesHandler { + constructor(service, validator) { + this._service = service; + this._validator = validator; + + this.postSaleHandler = this.postSaleHandler.bind(this); + this.getSalesHandler = this.getSalesHandler.bind(this); + this.getSaleByIdHandler = this.getSaleByIdHandler.bind(this); + } + + async postSaleHandler(request, h) { + try { + this._validator.validatePostSalePayload(request.payload); + const { id: userId } = request.auth.credentials; + const { + date, invoice, description, amount, discount, items, officeId, + } = request.payload; + + const saleId = await this._service.createTransaction({ + date, invoice, description, amount, discount, items, userId, officeId, + }); + + const response = h.response({ + status: 'success', + message: 'transaksi ditambahkan', + data: { + saleId, + }, + }); + response.code(201); + return response; + } catch (error) { + return error; + } + } + + async getSalesHandler(request) { + try { + this._validator.validateGetSalesPayload(request.query); + + const { companyId } = request.auth.credentials; + const { startDate, endDate } = request.query; + + const sales = await this._service.getSales(companyId, { startDate, endDate }); + + return { + status: 'success', + data: { + sales, + }, + }; + } catch (error) { + return error; + } + } + + async getSaleByIdHandler(request) { + try { + const { id: saleId } = request.params; + const sale = await this._service.getSaleById(saleId); + + return { + status: 'success', + data: { + sale, + }, + }; + } catch (error) { + return error; + } + } +} + +module.exports = SalesHandler; diff --git a/src/api/sales/index.js b/src/api/sales/index.js new file mode 100644 index 0000000..493aa80 --- /dev/null +++ b/src/api/sales/index.js @@ -0,0 +1,11 @@ +const SalesHandler = require('./handler'); +const routes = require('./routes'); + +module.exports = { + name: 'sales', + version: '1.0.0', + register: async (server, { service, validator }) => { + const salesHandler = new SalesHandler(service, validator); + server.route(routes(salesHandler)); + }, +}; diff --git a/src/api/sales/routes.js b/src/api/sales/routes.js new file mode 100644 index 0000000..15a28e0 --- /dev/null +++ b/src/api/sales/routes.js @@ -0,0 +1,28 @@ +const routes = (handler) => [ + { + method: 'POST', + path: '/sales', + handler: handler.postSaleHandler, + options: { + auth: 'kasiraja_jwt', + }, + }, + { + method: 'GET', + path: '/sales', + handler: handler.getSalesHandler, + options: { + auth: 'kasiraja_jwt', + }, + }, + { + method: 'GET', + path: '/sales/{id}', + handler: handler.getSaleByIdHandler, + options: { + auth: 'kasiraja_jwt', + }, + }, +]; + +module.exports = routes; diff --git a/src/server.js b/src/server.js index 91bce7a..2c43c84 100644 --- a/src/server.js +++ b/src/server.js @@ -35,6 +35,11 @@ const products = require('./api/products'); const ProductsService = require('./services/postgres/ProductsService'); const ProductsValidator = require('./validator/products'); +// sale transaction +const sales = require('./api/sales'); +const SalesService = require('./services/postgres/SalesServive'); +const SalesValidator = require('./validator/sales'); + const init = async () => { // instances const usersService = new UsersService(); @@ -43,6 +48,7 @@ const init = async () => { const unitsService = new UnitsService(); const categoriesService = new CategoriesService(); const productsService = new ProductsService(); + const salesService = new SalesService(); // server const server = Hapi.server({ @@ -153,6 +159,13 @@ const init = async () => { validator: CategoriesValidator, }, }, + { + plugin: sales, + options: { + service: salesService, + validator: SalesValidator, + }, + }, ]); await server.start(); diff --git a/src/services/postgres/SalesServive.js b/src/services/postgres/SalesServive.js new file mode 100644 index 0000000..99e59af --- /dev/null +++ b/src/services/postgres/SalesServive.js @@ -0,0 +1,121 @@ +const { Pool } = require('pg'); +const uuid = require('uuid-random'); +const InvariantError = require('../../exceptions/InvariantError'); +const NotFoundError = require('../../exceptions/NotFoundError'); +const { validateUuid } = require('../../utils'); + +class SalesService { + constructor() { + this._pool = new Pool(); + } + + async createTransaction({ + date, invoice, description, amount, discount, items, userId, officeId, + }) { + // check stock + const stocksQuery = await this._pool.query(` + SELECT product_id, stock, sale FROM stocks + WHERE product_id IN (${items.map((i) => `'${i.productId}'`).join()})`); + const stocks = stocksQuery.rows; + const itemsWithStock = items.map((item) => ({ + ...item, + stock: stocks.find((sp) => sp.product_id === item.productId).stock, + sale: stocks.find((sp) => sp.product_id === item.productId).sale, + })); + const checkStock = itemsWithStock + .map((iws) => +iws.stock - +iws.quantity).every((i) => i >= 0); + if (!checkStock) { + throw new InvariantError('transaksi gagal: stock tidak cukup'); + } + + const client = await this._pool.connect(); + try { + await client.query('BEGIN'); // transaction + + const id = uuid(); + const saleQuery = { + text: `INSERT INTO + sales(id, date, invoice, description, amount, discount, created_by, office_id) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`, + values: [id, date, invoice, description, amount, discount, userId, officeId], + }; + + const sale = await client.query(saleQuery); + const saleId = sale.rows[0].id; + + await itemsWithStock.map(async (item) => { + await client.query(`UPDATE stocks SET stock = '${+item.stock - +item.quantity}', sale = '${+item.sale + +item.quantity}' WHERE product_id = '${item.productId}'`); + + const itemQuery = { + text: `INSERT INTO sale_items(sale_id, product_id, quantity, price) VALUES ('${saleId}', '${item.productId}', '${item.quantity}', '${item.price}')`, + }; + + await client.query(itemQuery); + }); + + await client.query('COMMIT'); + + return saleId; + } catch (error) { + await client.query('ROLLBACK'); + throw new InvariantError(`transaksi gagal: ${error.message}`); + } finally { + client.release(); + } + } + + async getSales(companyId, { startDate, endDate }) { + const query = { + text: `SELECT + invoice, date, amount, offices.name as office_name + FROM sales + LEFT JOIN offices ON offices.id = sales.office_id + WHERE + sales.office_id = (SELECT id FROM offices WHERE company_id = $1 LIMIT 1) + AND date BETWEEN $2 AND $3`, + values: [companyId, startDate, endDate], + }; + + const results = await this._pool.query(query); + + return results.rows; + } + + async getSaleById(saleId) { + validateUuid(saleId); + + const query = { + text: `SELECT + date, invoice, sales.description, amount, discount, users.name as creator, offices.name as office_name + FROM sales + LEFT JOIN offices ON offices.id = sales.office_id + LEFT JOIN users ON users.id = sales.created_by + WHERE sales.id = $1`, + values: [saleId], + }; + + const results = await this._pool.query(query); + + if (results.rowCount < 1) { + throw new NotFoundError('transaksi tidak ditemukan'); + } + + const itemsQuery = { + text: `SELECT + products.name, quantity, sale_items.price + FROM sale_items + LEFT JOIN products ON products.id = sale_items.product_id + WHERE sale_id = $1`, + values: [saleId], + }; + + const items = await this._pool.query(itemsQuery); + + return { + ...results.rows[0], + items: items.rows, + }; + } +} + +module.exports = SalesService; diff --git a/src/services/postgres/UsersService.js b/src/services/postgres/UsersService.js index 9f9c5b6..59f01f0 100644 --- a/src/services/postgres/UsersService.js +++ b/src/services/postgres/UsersService.js @@ -92,6 +92,28 @@ class UsersService { return result.rows[0]; } + async getMe(userId) { + validateUuid(userId); + + const query = { + text: `SELECT + users.name, users.email, offices.id as officeId, companies.id as companyId + FROM users + LEFT JOIN companies ON companies.id = users.company_id + LEFT JOIN offices ON companies.id = offices.company_id + WHERE users.id = $1`, + values: [userId], + }; + + const result = await this._pool.query(query); + + if (result.rowCount < 1) { + throw new NotFoundError('User tidak ditemukan'); + } + + return result.rows[0]; + } + async updateUserById(userId, { name, email, password }) { validateUuid(userId); diff --git a/src/validator/sales/index.js b/src/validator/sales/index.js new file mode 100644 index 0000000..3770af9 --- /dev/null +++ b/src/validator/sales/index.js @@ -0,0 +1,20 @@ +const { PostSalePayloadSchema, GetSalesPayloadSchema } = require('./schema'); +const InvariantError = require('../../exceptions/InvariantError'); + +const SaleValidator = { + validatePostSalePayload: (payload) => { + const validationResult = PostSalePayloadSchema.validate(payload); + if (validationResult.error) { + throw new InvariantError(validationResult.error.message); + } + }, + + validateGetSalesPayload: (payload) => { + const validationResult = GetSalesPayloadSchema.validate(payload); + if (validationResult.error) { + throw new InvariantError(validationResult.error.message); + } + }, +}; + +module.exports = SaleValidator; diff --git a/src/validator/sales/schema.js b/src/validator/sales/schema.js new file mode 100644 index 0000000..25c0aec --- /dev/null +++ b/src/validator/sales/schema.js @@ -0,0 +1,24 @@ +const Joi = require('joi'); + +const PostSalePayloadSchema = Joi.object({ + officeId: Joi.string().guid().required(), + date: Joi.date().required(), + invoice: Joi.string().required(), + amount: Joi.number().required(), + discount: Joi.number().required(), + description: Joi.string(), + items: Joi.array().items( + Joi.object({ + productId: Joi.string().guid().required(), + quantity: Joi.number().required(), + price: Joi.number().required(), + }), + ), +}); + +const GetSalesPayloadSchema = Joi.object({ + startDate: Joi.date().required(), + endDate: Joi.date().required(), +}); + +module.exports = { PostSalePayloadSchema, GetSalesPayloadSchema };