diff --git a/src/cart/Cart.ts b/src/cart/Cart.ts new file mode 100644 index 0000000..9be3738 --- /dev/null +++ b/src/cart/Cart.ts @@ -0,0 +1,149 @@ +import establishedDatabase from "../database"; + +import ProductRepository from "../product"; + +import type { IProduct } from "../interfaces/IProduct"; +import type IDeliveryAddress from "../interfaces/IDeliveryAddress"; + +const productRepository = new ProductRepository(); + +class Cart { + clientId: string; + database: any; + + constructor(clientId: string) { + this.clientId = clientId; + this.database = establishedDatabase; + } + + async get(): Promise> { + const query = + "SELECT * FROM cart_detailed WHERE client_id = $1 ORDER BY lineitem_id"; + return this.database.all(query, [this.clientId]); + } + + async getLineItem(product_sku_no: number) { + const query = ` + SELECT product_no, quantity + FROM cart_lineitem + WHERE product_sku_no = $2 + AND cart_id = ( + SELECT cart_id + FROM cart + WHERE client_id = $1 + ) + `; + + return this.database.get(query, [this.clientId, product_sku_no]); + } + + async exists() { + const query = ` + SELECT cart_id + FROM cart + WHERE client_id = $1 + `; + + const exists = await this.database.get(query, [this.clientId]); + return exists !== undefined; + } + + create() { + const query = ` + INSERT INTO cart (client_id) values ($1) ON CONFLICT DO NOTHING + `; + + return this.database.update(query, [this.clientId]); + } + + async add(product_no: number, product_sku_no: number, quantity: number) { + if ((await this.exists()) === false) { + await this.create(); + } + + const existingLineItem = await this.getLineItem(product_sku_no); + + let query = ` + INSERT INTO cart_lineitem (cart_id, product_no, product_sku_no, quantity) values ( + ( + SELECT cart_id + FROM cart + WHERE client_id = $1 + ), + $2, + $3, + $4 + ) + `; + + if (existingLineItem) { + quantity = quantity + existingLineItem.quantity; + query = ` + UPDATE cart_lineitem + SET quantity = $4 + WHERE product_no = $2 + AND product_sku_no = $3 + AND cart_id = ( + SELECT cart_id + FROM cart + WHERE client_id = $1 + ) + `; + } + + return this.database.update(query, [ + this.clientId, + product_no, + product_sku_no, + quantity, + ]); + } + + remove(lineitem_id: number) { + // TODO should match w/ cart.client_id + const query = ` + DELETE FROM cart_lineitem + WHERE lineitem_id = $1 + `; + + return this.database.update(query, [lineitem_id]); + } + + decrement(lineitem_id: number) { + // TODO should match w/ cart.client_id + const query = ` + UPDATE cart_lineitem + SET quantity = quantity - 1 + WHERE lineitem_id = $1 + `; + + return this.database.update(query, [lineitem_id]); + } + + async increment(lineitem_id: number) { + // if (!productRepository.hasQuantityOfSkuInStock(lineitem_id)) { + // throw new SkuQuantityNotInStockError(""); + // } + // TODO should match w/ cart.client_id + const query = ` + UPDATE cart_lineitem + SET quantity = quantity + 1 + WHERE lineitem_id = $1 + `; + + return this.database.update(query, [lineitem_id]); + } + + // addItem(item: IProduct) { + // this.products.push(item); + // } + + removeItem(item: IProduct) {} + + destroy() { + const query = `DELETE FROM chart WHERE client_id = $1`; + this.database.update(query, [this.clientId]); + } +} + +export default Cart; diff --git a/src/cart/CartErrors.ts b/src/cart/CartErrors.ts new file mode 100644 index 0000000..ec37899 --- /dev/null +++ b/src/cart/CartErrors.ts @@ -0,0 +1,14 @@ +class SkuQuantityNotInStockError extends Error { + name: string; + statusCode: number; + + constructor(message) { + super(message); + this.name = "SkuQuantityNotInStockError"; + this.statusCode = 200; + } +} + +export default { + SkuQuantityNotInStockError, +}; diff --git a/src/cart/CartSession.ts b/src/cart/CartSession.ts new file mode 100644 index 0000000..5809042 --- /dev/null +++ b/src/cart/CartSession.ts @@ -0,0 +1,81 @@ +import type WSCart from "./WSCart"; + +let coldLog = 0; + +interface IGlobalCart { + sessionId: string; + wsCart: WSCart; +} + +class CartSession { + carts: Array; + + constructor() { + this.carts = []; + } + + getWSCartByClientId(clientId: string): WSCart { + const match = this.carts.find((cart) => cart.wsCart?.clientId === clientId); + if (!match) return null; + return match?.wsCart; + } + + add(sessionId: string, cart: WSCart) { + console.log( + `adding session ${sessionId} with cart id: ${cart?.cart?.clientId}` + ); + this.carts.push({ wsCart: cart, sessionId }); + } + + remove(sessionId) { + console.log(`removing session ${sessionId}`); + this.carts = this.carts.filter((cart) => cart.sessionId !== sessionId); + } + + removeIfNotAlive() { + this.carts.forEach((cart) => { + if (cart.wsCart.isAlive) return; + this.remove(cart); + }); + } + + async emitChangeToClients(wsCart: WSCart) { + const { clientId } = wsCart; + + const matchingCarts = this.carts.filter( + (cart) => cart.wsCart.clientId === clientId + ); + console.log( + `emit change to all carts with id ${clientId}:`, + matchingCarts.length + ); + + const cart = await matchingCarts[0]?.wsCart.cart.get(); + matchingCarts.forEach((_cart) => _cart.wsCart.emitCart(cart)); + } + + listCarts() { + if (this.carts.length === 0) { + if (coldLog < 4) { + console.log("No clients"); + coldLog = coldLog + 1; + } + + return; + } + + console.log(`Active clients: (${this.carts.length})`); + this.carts.forEach((cart: IGlobalCart) => { + console.table({ + isAlive: cart.wsCart?.isAlive, + clientId: cart.wsCart?.clientId, + sessionId: cart?.sessionId, + hasCart: cart.wsCart?.cart !== null, + }); + }); + + coldLog = 0; + } +} + +export default CartSession; diff --git a/src/cart/WSCart.ts b/src/cart/WSCart.ts new file mode 100644 index 0000000..1af9f81 --- /dev/null +++ b/src/cart/WSCart.ts @@ -0,0 +1,138 @@ +import Cart from "./Cart"; +import CartSession from "./CartSession"; +import type ICart from "../interfaces/ICart"; +import { IProduct } from "../interfaces/IProduct"; + +const cartSession = new CartSession(); + +class InvalidLineItemIdError extends Error { + statusCode: number; + + constructor() { + const message = "Fant ikke produktet som ble lagt til"; + super(message); + + this.statusCode = 400; + } +} + +function parseDataAsCartPayload(message: string): ICartPayload { + let json: ICartPayload = null; + + try { + json = JSON.parse(message); + } catch {} + + return json; +} + +interface ICartPayload { + command: string; + message: string; + product_no?: number; + product_sku_no?: number; + quantity?: number; + lineitem_id?: number; +} + +class WSCart { + ws: WebSocket; + clientId: string | null; + cart: Cart; + cartSession: CartSession; + + constructor(ws, clientId) { + this.ws = ws; + this.clientId = clientId; + this.cart = new Cart(clientId); + this.cartSession; + } + + get isAlive() { + return this.ws.readyState === 1; + } + + /* emitters */ + message(message: string, success = true) { + this.ws.send(JSON.stringify({ message, success })); + } + + async emitCart(cart: any[] | null = null) { + if (cart === null || cart?.length === 0) { + cart = await this.cart.get(); + } + + this.ws.send(JSON.stringify({ cart, success: true })); + } + + /* handle known commands */ + async addCartProduct(payload): Promise { + const { product_no, product_sku_no, quantity } = payload; + if (!product_no || !quantity) { + // throw here? + this.message("Missing product_no or quantity", false); + } + + await this.cart.add(product_no, product_sku_no, quantity); + return true; + } + + async removeCartProduct(lineitem_id: number): Promise { + if (isNaN(lineitem_id)) throw new InvalidLineItemIdError(); + await this.cart.remove(lineitem_id); + return true; + } + + async decrementProductInCart(lineitem_id: number): Promise { + if (isNaN(lineitem_id)) throw new InvalidLineItemIdError(); + await this.cart.decrement(lineitem_id); + return true; + } + + async incrementProductInCart(lineitem_id: number): Promise { + // TODO validate the quantity trying to be added here ?? + + if (isNaN(lineitem_id)) throw new InvalidLineItemIdError(); + await this.cart.increment(lineitem_id); + return true; + } + + /* main ws data/message handler */ + async handleMessage(data: Buffer | string, isBinary: boolean) { + const dataMessage = isBinary ? String(data) : data.toString(); + if (dataMessage === "heartbeat") return; + + const payload = parseDataAsCartPayload(dataMessage); + const { command } = payload; + + try { + let emitCart = false; + + if (command === "cart") this.emitCart(); + else if (command === "add") { + emitCart = await this.addCartProduct(payload); + } else if (command === "rm") { + emitCart = await this.removeCartProduct(payload?.lineitem_id); + } else if (command === "decrement") { + emitCart = await this.decrementProductInCart(payload?.lineitem_id); + } else if (command === "increment") { + emitCart = await this.incrementProductInCart(payload?.lineitem_id); + } else { + console.log(`client has sent us other/without command: ${dataMessage}`); + } + + if (emitCart) { + this.cartSession.emitChangeToClients(this); + } + } catch (error) { + // ???? + if (error.message) this.message(error?.message, false); + } + } + + handleError() {} + + destroy() {} +} + +export default WSCart; diff --git a/src/customer.ts b/src/customer.ts new file mode 100644 index 0000000..f28ab98 --- /dev/null +++ b/src/customer.ts @@ -0,0 +1,67 @@ +import establishedDatabase from "./database"; +import ICustomer from "./interfaces/ICustomer"; +import logger from "./logger"; + +class CustomerRepository { + database: typeof establishedDatabase; + + constructor(database = establishedDatabase) { + this.database = database || establishedDatabase; + } + + newCustomer(customer: ICustomer) { + const query = ` + INSERT INTO customer (email, first_name, last_name, street_address, zip_code, city) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING customer_no + `; + + const { + email, + first_name, + last_name, + street_address, + zip_code, + city, + } = customer; + logger.info("Creating customer", { customer }); + + return this.database.get(query, [ + email, + first_name, + last_name, + street_address, + zip_code, + city, + ]); + } + + getCustomer(customer_no) { + const query = ` + SELECT email, first_name, last_name, street_address, zip_code, city + FROM customer + WHERE customer_no = $1`; + + return this.database.get(query, [customer_no]); + } +} + +export default CustomerRepository; + +// ``` +// SELECT products.* +// FROM products +// INNER JOIN orders_lineitem +// ON products.product_no = orders_lineitem.product_no +// WHERE order_id = 'fb9a5910-0dcf-4c65-9c25-3fb3eb883ce5'; + +// SELECT orders.* +// FROM orders +// WHERE order_id = 'fb9a5910-0dcf-4c65-9c25-3fb3eb883ce5'; + +// SELECT customer.* +// FROM customer +// INNER JOIN orders +// ON customer.customer_no = orders.customer_no +// WHERE order_id = 'fb9a5910-0dcf-4c65-9c25-3fb3eb883ce5'; +// ```; diff --git a/src/order.ts b/src/order.ts new file mode 100644 index 0000000..cc772c2 --- /dev/null +++ b/src/order.ts @@ -0,0 +1,395 @@ +import logger from "./logger"; +import establishedDatabase from "./database"; +import { ORDER_STATUS_CODES } from "./enums/order"; +import type { IOrder } from "./interfaces/IOrder"; +import type { IProduct } from "./interfaces/IProduct"; + +class MissingOrderError extends Error { + statusCode: number; + + constructor() { + const message = "Requested order not found."; + super(message); + + this.name = "MissingOrderError"; + this.statusCode = 404; + } +} + +class PaymentType { + database: typeof establishedDatabase; + + constructor(database) { + this.database = database || establishedDatabase; + } + + getId(name) { + const query = `SELECT payment_id FROM payment_types WHERE name = $1`; + return this.database.get(query, [name]).then((resp) => resp["payment_id"]); + } +} + +class OrderRepository { + database: typeof establishedDatabase; + + constructor(database = establishedDatabase) { + this.database = database || establishedDatabase; + } + + async newOrder(customer_no: string) { + const query = ` + INSERT INTO orders (customer_no) VALUES ($1) + RETURNING order_id + `; + + logger.info("Creating order with customer", { customer_no }); + return await this.database.get(query, [customer_no]); + } + + addOrderLineItem( + order_id, + product_no: number, + sku_id: number, + price: number, + quantity: number + ) { + const query = ` + INSERT INTO orders_lineitem (order_id, product_no, product_sku_no, price, quantity) + VALUES ($1, $2, $3, $4, $5) + `; + + logger.info("Adding lineitem to order", { + order_id, + product_no, + sku_id, + price, + quantity, + }); + return this.database.get(query, [ + order_id, + product_no, + sku_id, + price, + quantity, + ]); + } + + getExistingOrdersOnProduct(product) { + const query = ` + SELECT order_id, product_no, end_time, start_time + FROM orders + WHERE product_no = $1 + AND NOT status = '' + AND end_time > now() + `; + + return this.database.all(query, [product.product_no]); + } + + getConflictingProductOrders(order) { + const query = ` + SELECT order_id, product_no, end_time, start_time, status, created, updated + FROM orders + WHERE product_no = $1 + AND NOT status = '' + AND NOT order_id = $2 + AND NOT status = $3 + AND NOT status = $4 + AND NOT status = $5 + `; + + return { + order, + conflicting: this.database.all(query, [ + order.product_no, + order.order_id, + ORDER_STATUS_CODES.INITIATED, + ORDER_STATUS_CODES.COMPLETED, + ORDER_STATUS_CODES.CANCELLED, + ]), + }; + } + + getAll() { + const query = ` +SELECT orders.created, orders.order_id, customer.first_name, customer.last_name, customer.email, status, orders_lines.order_sum +FROM orders +INNER JOIN ( + SELECT order_id, SUM(quantity * orders_lineitem.price) as order_sum + FROM orders_lineitem + INNER JOIN product_sku + ON orders_lineitem.product_sku_no = product_sku.sku_id + GROUP BY order_id +) AS orders_lines +ON orders.order_id = orders_lines.order_id +INNER JOIN customer +ON customer.customer_no = orders.customer_no;`; + + return this.database.all(query, []); + + // SELECT orders.created, orders.order_id, customer.first_name, customer.last_name, customer.email, status, orders_lines.sku_id, orders_lines.quantity, size, price, orders_lines.quantity * price as sum + // FROM orders + // INNER JOIN ( + // SELECT order_id, sku_id, product_sku.product_no, size, orders_lineitem.price, stock, quantity + // FROM orders_lineitem + // INNER JOIN product_sku + // ON orders_lineitem.product_sku_no = product_sku.sku_id + // ) AS orders_lines + // ON orders.order_id = orders_lines.order_id + // INNER JOIN customer + // ON customer.customer_no = orders.customer_no; + } + + async getOrderDetailed(orderId) { + const customerQuery = ` +SELECT customer.* +FROM orders +INNER JOIN ( + SELECT customer_no, email, first_name, last_name, street_address, zip_code, city + FROM customer +) AS customer +ON orders.customer_no = customer.customer_no +WHERE order_id = $1`; + + // const paymentQuery = `` + + const orderQuery = ` +SELECT order_id as orderId, created, updated, status +FROM orders +WHERE order_id = $1`; + + const lineItemsQuery = ` +SELECT product.name, product.image, orders_lineitem.quantity, product_sku.sku_id, product_sku.price, product_sku.size +FROM orders_lineitem +INNER JOIN product +ON orders_lineitem.product_no = product.product_no +INNER JOIN product_sku +ON orders_lineitem.product_sku_no = product_sku.sku_id +WHERE orders_lineitem.order_id = $1`; + + const shippingQuery = ` +SELECT shipping_company as company, tracking_code, tracking_link, user_notified +FROM shipping +WHERE shipping.order_id = $1`; + + const [order, customer, shipping, lineItems] = await Promise.all([ + this.database.get(orderQuery, [orderId]), + this.database.get(customerQuery, [orderId]), + this.database.get(shippingQuery, [orderId]), + this.database.all(lineItemsQuery, [orderId]), + ]); + + return { + ...order, + customer, + shipping, + lineItems, + }; + } + + async getOrder(orderId): Promise { + const orderQuery = ` +SELECT order_id, customer_no, created, updated, status +FROM orders +WHERE order_id = $1`; + + const lineItemsQuery = ` +SELECT product.name, product.image, orders_lineitem.quantity, product_sku.sku_id, product_sku.price, product_sku.size +FROM orders_lineitem +INNER JOIN product +ON orders_lineitem.product_no = product.product_no +INNER JOIN product_sku +ON orders_lineitem.product_sku_no = product_sku.sku_id +WHERE orders_lineitem.order_id = $1`; + + const [order, lineItems] = await Promise.all([ + this.database.get(orderQuery, [orderId]), + this.database.all(lineItemsQuery, [orderId]), + ]); + + return { + ...order, + lineItems, + }; + + // return this.database.get(query, [orderId]).then((row) => { + // if (row) { + // return row; + // } + + // throw new MissingOrderError(); + // }); + } + + getOrderWithProduct(orderId) { + const query = ` + SELECT order_id, product.price, product.product_no, product.name + FROM orders + INNER JOIN products as product + ON (orders.product_no = product.product_no) + WHERE orders.order_id = $1 + `; + + return this.database.get(query, [orderId]); + } + + rejectOrder(orderId) { + const timestamp = new Date(); + const query = ` + UPDATE orders + SET status = $1, updated = $2 + WHERE order_id = $3 + `; + + return this.database.update(query, [ + ORDER_STATUS_CODES.REJECTED, + timestamp, + orderId, + ]); + } + + refundOrder(orderId: string) { + const timestamp = new Date(); + const query = ` + UPDATE orders + SET status = $1, updated = $2 + WHERE order_id = $3 + `; + + return this.database.update(query, [ + ORDER_STATUS_CODES.REFUNDED, + timestamp, + orderId, + ]); + } + + rejectByVippsTimeout(orderId) { + const timestamp = new Date(); + const query = ` + UPDATE orders + SET status = $1, end_time = $2, updated = $3 + WHERE order_id = $4 + `; + + return this.database.update(query, [ + ORDER_STATUS_CODES.TIMED_OUT_REJECT, + timestamp, + timestamp, + orderId, + ]); + } + + getExtendedOrders(orderId) { + const query = ` + SELECT status, orders.end_time, start_time, vp.vipps_status, vp.amount, vp.hours, vp.captured, vp.refunded, vp.parent_order_id, vp.order_id + FROM orders + INNER JOIN vipps_payments as vp + ON (orders.order_id = vp.order_id OR orders.order_id = vp.parent_order_id) + WHERE (vp.order_id = $1 or vp.parent_order_id = $2) + `; + + return this.database.all(query, [orderId, orderId]); + } + + getOrderStatus(orderId: string) { + const query = `SELECT status + FROM orders + WHERE orders.order_id = $1`; + + return this.database.get(query, [orderId]).then((row) => { + if (row) return row; + + return null; + }); + } + + cancelOrder(orderId: string) { + const timestamp = new Date(); + const query = `UPDATE orders + SET status = $1, updated = $2 + WHERE order_id = $3`; + + return this.database.update(query, [ + ORDER_STATUS_CODES.CANCELLED, + timestamp, + orderId, + ]); + } + + async confirmOrder(orderId: string) { + const order = await this.getOrder(orderId); + + const orderSkuQuantity = order.lineItems.map((lineItem) => { + return { + sku_id: lineItem.sku_id, + quantity: lineItem.quantity, + }; + }); + + await Promise.all( + orderSkuQuantity.map((el) => + this.reduceSkuInStock(el.sku_id, el.quantity) + ) + ); + + const timestamp = new Date(); + const query = ` + UPDATE orders + SET status = $1, updated = $2 + WHERE order_id = $3`; + + return this.database.update(query, [ + ORDER_STATUS_CODES.CONFIRMED, + timestamp, + orderId, + ]); + } + + reduceSkuInStock(sku_id: number, quantity: number) { + console.log("reducing stock for sku_id:", sku_id, quantity); + const query = `UPDATE product_sku + SET stock = stock - $2 + WHERE sku_id = $1`; + + return this.database.update(query, [sku_id, quantity]); + } + + increaseSkuInStock(sku_id: number, quantity: number) { + const query = `UPDATE product_sku + SET stock = stock + $2 + WHERE sku_id = $1`; + + return this.database.update(query, [sku_id, quantity]); + } + + updateEndTime(orderId, endTime) { + const timestamp = new Date(); + const query = `UPDATE orders + SET status = $1, updated = $2, end_time = $3 + WHERE order_id = $4`; + + return this.database.update(query, [ + ORDER_STATUS_CODES.CONFIRMED, + timestamp, + endTime, + orderId, + ]); + } + + updateStartTime(orderId, startTime) { + const timestamp = new Date(); + const query = ` + UPDATE orders + SET status = $1, updated = $2, start_time = $3 + WHERE order_id = $4`; + + return this.database.update(query, [ + ORDER_STATUS_CODES.CONFIRMED, + timestamp, + startTime, + orderId, + ]); + } +} + +export default OrderRepository; diff --git a/src/product.ts b/src/product.ts new file mode 100644 index 0000000..636183f --- /dev/null +++ b/src/product.ts @@ -0,0 +1,203 @@ +import establishedDatabase from "./database"; + +class ProductRepository { + database: typeof establishedDatabase; + + constructor(database = establishedDatabase) { + this.database = database || establishedDatabase; + } + + async add( + name = "foo", + description = "foo", + image = "/static/no-product.png", + subtext = "foo", + primary_color = "foo" + ) { + const query = ` + INSERT INTO + product (name, description, image, subtext, primary_color) + VALUES ($1, $2, $3, $4, $5) + `; + + await this.database.update(query, [ + name, + description, + image, + subtext, + primary_color, + ]); + + const productIdQuery = `SELECT currval('product_product_no_seq')`; + const productCurrVal = await this.database.get(productIdQuery, []); + return productCurrVal.currval; + } + + getAllProducts() { + const query = ` + SELECT product.*, variations + FROM product + JOIN ( + SELECT product_no, count(size) AS variations + FROM product_sku + GROUP BY product_no + ) AS product_sku + ON product.product_no = product_sku.product_no`; + + return this.database.all(query, []); + } + + async get(productId) { + const productQuery = `SELECT * FROM product WHERE product_no = $1`; + const product = await this.database.get(productQuery, [productId]); + + const skuQuery = ` +SELECT sku_id, size, price, stock, default_price, updated, created +FROM product_sku +WHERE product_no = $1 +ORDER BY created`; + + const productSkus = await this.database.all(skuQuery, [productId]); + return Promise.resolve({ + ...product, + variations: productSkus, + }); + } + + async addSku( + productId, + price = 10, + size = "", + stock = 0, + defaultPrice = false + ) { + const query = ` +INSERT INTO +product_sku (product_no, price, size, stock, default_price) +VALUES ($1, $2, $3, $4, $5) +`; + + return this.database.update(query, [ + productId, + price, + size, + stock, + defaultPrice, + ]); + } + + getSkus(productId) { + const q = `SELECT sku_id, product_no, price, size, stock, default_price, created, updated + FROM product_sku + WHERE product_no = $1 + ORDER BY created`; + + return this.database.all(q, [productId]); + } + + async getSkuStock(skuId) { + const query = "SELECT stock FROM product_sku WHERE sku_id = $1"; + const stockResponse = await this.database.get(query, [skuId]); + return stockResponse?.stock || null; + } + + // helper + async hasQuantityOfSkuInStock(skuId, quantity) { + const stock = await this.getSkuStock(skuId); + if (!stock) return false; + + // if requested quantity is less or equal to current stock + return quantity <= stock; + } + + updateProduct(product) { + const { + product_no, + name, + description, + image, + subtext, + primary_color, + // new Date + } = product; + const query = ` +UPDATE product +SET name = $1, description = $2, image = $3, subtext = $4, primary_color = $5, updated = to_timestamp($6 / 1000.0) +WHERE product_no = $7 +`; + + return this.database.update(query, [ + name, + description, + image, + subtext, + primary_color, + new Date().getTime(), + product_no, + ]); + } + + updateSku(productId, skuId, stock = 0, size = 0, price = 10) { + const query = ` +UPDATE product_sku +SET + price = $1, + size = $2, + stock = $3, + updated = to_timestamp($4 / 1000.0) +WHERE product_no = $5 and sku_id = $6`; + + console.log("update sql:", query, [ + price, + size, + stock, + new Date().getTime(), + productId, + skuId, + ]); + + return this.database.update(query, [ + price, + size, + stock, + new Date().getTime(), + productId, + skuId, + ]); + } + + async setSkuDefaultPrice(productId, skuId) { + const resetOld = ` +UPDATE product_sku +SET default_price = false, updated = to_timestamp($1 / 1000.0) +WHERE product_no = $2 and default_price = true`; + const setNew = ` +UPDATE product_sku +SET default_price = true, updated = to_timestamp($1 / 1000.0) +WHERE product_no = $2 AND sku_id = $3 +`; + + await this.database.update(resetOld, [new Date().getTime(), productId]); + await this.database.update(setNew, [ + new Date().getTime(), + productId, + skuId, + ]); + } + + getDefaultPrice(productId, skuId) { + const query = ` +SELECT * +FROM product_sku +WHERE default_price = true and product_no = $1 and sku_id = $2`; + + return this.database.query(query, [productId, skuId]); + } + + deleteSku(productId, skuId) { + const query = `DELETE from product_sku WHERE product_no = $1 AND sku_id = $2`; + return this.database.update(query, [productId, skuId]); + } +} + +export default ProductRepository; diff --git a/src/stripe/stripeApi.ts b/src/stripe/stripeApi.ts new file mode 100644 index 0000000..289d028 --- /dev/null +++ b/src/stripe/stripeApi.ts @@ -0,0 +1,69 @@ +import Stripe from "stripe"; +import type ICustomer from "../interfaces/ICustomer"; + +/** + * Does calls to stripe API + */ + +class StripeApi { + publicKey: string; + secretKey: string; + stripe: Stripe; + + constructor(publicKey: string, secretKey: string) { + this.publicKey = publicKey; + this.secretKey = secretKey; + this.stripe = new Stripe(this.secretKey, { + apiVersion: "2022-08-01", + }); + } + + async createPaymentIntent( + clientId: string, + total: number, + orderId: string, + customer: ICustomer + ): Promise> { + const stripeCustomer = await this.createCustomer(clientId, customer); + const paymentIntent = await this.stripe.paymentIntents.create({ + customer: stripeCustomer?.id, + amount: total * 100, + currency: "NOK", + shipping: { + name: stripeCustomer.name, + address: stripeCustomer.address, + }, + metadata: { + clientId, + orderId, + }, + }); + + return paymentIntent; + } + + async createCustomer(clientId: string, customer: ICustomer) { + return await this.stripe.customers.create({ + email: customer.email, + name: `${customer.first_name} ${customer.last_name}`, + address: { + city: customer.city, + line1: customer.street_address, + postal_code: String(customer.zip_code), + }, + metadata: { + clientId, + }, + }); + } + + createProduct(cart) { + return; + } + + async createShipping() { + return; + } +} + +export default StripeApi; diff --git a/src/stripe/stripeRepository.ts b/src/stripe/stripeRepository.ts new file mode 100644 index 0000000..e6d3a41 --- /dev/null +++ b/src/stripe/stripeRepository.ts @@ -0,0 +1,87 @@ +import establishedDatabase from "../database"; +import Configuration from "../config/configuration"; +import StripeApi from "./stripeApi"; +import Stripe from "stripe"; +import type ICustomer from "../interfaces/ICustomer"; + +const configuration = Configuration.getInstance(); +const stripeApi = new StripeApi( + configuration.get("stripe", "publicKey"), + configuration.get("stripe", "secretKey") +); + +class StripeRepository { + database: typeof establishedDatabase; + + constructor(database = establishedDatabase) { + this.database = database || establishedDatabase; + } + + commitPaymentToDatabase( + orderId: string, + payload: Stripe.Response + ) { + const query = ` + INSERT INTO stripe_payments + (order_id, amount, stripe_initiation_response, stripe_transaction_id, stripe_status) + VALUES ($1,$2,$3,$4,$5)`; + + return this.database.query(query, [ + orderId, + payload.amount, + payload, + payload.id, + payload.status, + ]); + } + + updatePaymentIntent(payload: Stripe.Response) { + const query = ` + UPDATE stripe_payments + SET stripe_status = $2, amount_received = $3, updated = $4 + WHERE order_id = $1`; + + return this.database.update(query, [ + payload.metadata.orderId, + payload.status, + payload.amount_received, + new Date(), + ]); + } + + updatePaymentCharge(payload: Stripe.Response) { + const query = ` + UPDATE stripe_payments + SET stripe_status = $2, amount_captured = $3, amount_refunded = $4, updated = $5 + WHERE order_id = $1 + `; + + return this.database.update(query, [ + payload.metadata.orderId, + payload.status, + payload.amount_captured, + payload.amount_refunded, + new Date(), + ]); + } + + async createPayment( + clientId: string, + total: number, + orderId: string, + customer: ICustomer + ) { + const paymentIntent = await stripeApi.createPaymentIntent( + clientId, + total, + orderId, + customer + ); + + return this.commitPaymentToDatabase(orderId, paymentIntent).then( + () => paymentIntent.client_secret + ); + } +} + +export default StripeRepository; diff --git a/src/warehouse.ts b/src/warehouse.ts new file mode 100644 index 0000000..a9a2331 --- /dev/null +++ b/src/warehouse.ts @@ -0,0 +1,110 @@ +import establishedDatabase from "./database"; +import type { IProductWithSkus } from "./interfaces/IProduct"; + +// interface IProductSku { +// id: string +// size: string +// stock: number +// price: number +// created?: string +// updated?: string +// } + +// interface IProductWithSku extends IProduct { +// sku: IProductSku +// } + +class WarehouseRepository { + database: any; + + constructor(database = null) { + this.database = database || establishedDatabase; + } + + getAllProductIds() { + return this.database.get("SELECT product_no FROM product"); + } + + async getProduct(productId): Promise { + const productQuery = `SELECT * FROM product WHERE product_no = $1`; + const product = await this.database.get(productQuery, [productId]); + + const skuQuery = ` +SELECT sku_id, size, price, stock, default_price, updated, created +FROM product_sku +WHERE product_no = $1 +ORDER BY created`; + + const productSkus = await this.database.all(skuQuery, [productId]); + return Promise.resolve({ + ...product, + variations: productSkus, + }); + } + + all(): Promise { + const query = ` + SELECT product.*, variation_count, sum_stock + FROM product + INNER JOIN ( + SELECT product_no, count(size) AS variation_count, sum(stock) as sum_stock + FROM product_sku + GROUP BY product_no + ) AS product_sku + ON product.product_no = product_sku.product_no`; + + return this.database.all(query); + } + + getAvailableProducts() { + const query = `SELECT * from available_products`; + return this.database.all(query); + } + + checkSkuStock(skuId) { + const query = "SELECT stock FROM product_sku WHERE sku_id = $1"; + + return this.database.get(query, [skuId]); + } + + createWarehouseProduct(skuId, stock) { + const query = ` + INSERT INTO + warehouse (product_sku_id, stock) + VALUES ($1, $2) + `; + + return this.database.update(query, [skuId, stock]); + } + + updateWarehouseProductSkuStock(skuId, stock) { + const query = ` + UPDATE warehouse + SET stock = $1, updated = to_timestamp($2 / 1000.0) + WHERE product_sku_id = $3 + `; + + return this.database.update(query, [stock, new Date(), skuId]); + } + + updateWarehouseProductSkuStatus(skuId, status) { + const sqlStatus = status ? "TRUE" : "FALSE"; + + const query = ` + UPDATE warehouse + SET enabled = $1, updated = to_timestamp($2 / 1000.0) + WHERE product_sku_id = $3`; + + return this.database.query(query, [sqlStatus, new Date(), skuId]); + } + + disableWarehouseProductSku(skuId) { + return this.updateWarehouseProductSkuStatus(skuId, false); + } + + enableWarehouseProductSku(skuId) { + return this.updateWarehouseProductSkuStatus(skuId, true); + } +} + +export default WarehouseRepository;