mirror of
https://github.com/KevinMidboe/planetposen-backend.git
synced 2025-10-29 00:10:12 +00:00
Cart, order, warehouse, product, customer & stripe payment files
This commit is contained in:
149
src/cart/Cart.ts
Normal file
149
src/cart/Cart.ts
Normal file
@@ -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<Array<object>> {
|
||||
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;
|
||||
14
src/cart/CartErrors.ts
Normal file
14
src/cart/CartErrors.ts
Normal file
@@ -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,
|
||||
};
|
||||
81
src/cart/CartSession.ts
Normal file
81
src/cart/CartSession.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type WSCart from "./WSCart";
|
||||
|
||||
let coldLog = 0;
|
||||
|
||||
interface IGlobalCart {
|
||||
sessionId: string;
|
||||
wsCart: WSCart;
|
||||
}
|
||||
|
||||
class CartSession {
|
||||
carts: Array<IGlobalCart>;
|
||||
|
||||
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;
|
||||
138
src/cart/WSCart.ts
Normal file
138
src/cart/WSCart.ts
Normal file
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
if (isNaN(lineitem_id)) throw new InvalidLineItemIdError();
|
||||
await this.cart.remove(lineitem_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
async decrementProductInCart(lineitem_id: number): Promise<boolean> {
|
||||
if (isNaN(lineitem_id)) throw new InvalidLineItemIdError();
|
||||
await this.cart.decrement(lineitem_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
async incrementProductInCart(lineitem_id: number): Promise<boolean> {
|
||||
// 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;
|
||||
67
src/customer.ts
Normal file
67
src/customer.ts
Normal file
@@ -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';
|
||||
// ```;
|
||||
395
src/order.ts
Normal file
395
src/order.ts
Normal file
@@ -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<IOrder> {
|
||||
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;
|
||||
203
src/product.ts
Normal file
203
src/product.ts
Normal file
@@ -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;
|
||||
69
src/stripe/stripeApi.ts
Normal file
69
src/stripe/stripeApi.ts
Normal file
@@ -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<Stripe.Response<Stripe.PaymentIntent>> {
|
||||
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;
|
||||
87
src/stripe/stripeRepository.ts
Normal file
87
src/stripe/stripeRepository.ts
Normal file
@@ -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<Stripe.PaymentIntent>
|
||||
) {
|
||||
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<Stripe.PaymentIntent>) {
|
||||
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<Stripe.Charge>) {
|
||||
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;
|
||||
110
src/warehouse.ts
Normal file
110
src/warehouse.ts
Normal file
@@ -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<IProductWithSkus> {
|
||||
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<IProductWithSkus[]> {
|
||||
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;
|
||||
Reference in New Issue
Block a user