mirror of
https://github.com/KevinMidboe/planetposen-backend.git
synced 2025-10-28 16:00:12 +00:00
Improvements to order, product, checkout & added shipping (#2)
* planet_id variable casing consistent with database field Renames all clientId variables to planet_id * Store stripe json response from latest payment & charge webhook Also added db seed file for stripe payments * Image support! Can now add, update & delete images from products Images moved away from product schema to it's own table. Accepts a images string and relates it to a product. Does not store the images or verify the existance. * Instead of deleting a product set field unlisted to true Endpoints with current inventory checks for unlisted = false * order_id & customer_no gets enhanced base64 function for generating id * Implemented shipping for orders using Post tracking api Added CRUD for making changes to a order's shippment. Split shipping table into shipment_courier, shipment & shipment_event. * Updated and add product & product_sku functions updated * Cart increment funciton checks stock before updating * Endpoint for getting product audit log using 91pluss trigger Read more about usage here: https://wiki.postgresql.org/wiki/Audit_trigger_91plus * On stripe charge successfull send email to user with planetposen-mail * More product seed data, linting & formatting * Log file at /var/log/planetposen_logs & rotate max 3 files 100MB each This will prob throw a error if folder does not exist, run: `(sudo) mkdir -p /var/log/planetposen_logs`. * All endpoints now prefixed with /api/v1 * Linting
This commit is contained in:
@@ -1,25 +1,23 @@
|
||||
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;
|
||||
planet_id: string;
|
||||
database: any;
|
||||
|
||||
constructor(clientId: string) {
|
||||
this.clientId = clientId;
|
||||
constructor(planet_id: string) {
|
||||
this.planet_id = planet_id;
|
||||
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]);
|
||||
"SELECT * FROM cart_detailed WHERE planet_id = $1 ORDER BY lineitem_id";
|
||||
return this.database.all(query, [this.planet_id]);
|
||||
}
|
||||
|
||||
async getLineItem(product_sku_no: number) {
|
||||
@@ -30,30 +28,30 @@ class Cart {
|
||||
AND cart_id = (
|
||||
SELECT cart_id
|
||||
FROM cart
|
||||
WHERE client_id = $1
|
||||
WHERE planet_id = $1
|
||||
)
|
||||
`;
|
||||
|
||||
return this.database.get(query, [this.clientId, product_sku_no]);
|
||||
return this.database.get(query, [this.planet_id, product_sku_no]);
|
||||
}
|
||||
|
||||
async exists() {
|
||||
const query = `
|
||||
SELECT cart_id
|
||||
FROM cart
|
||||
WHERE client_id = $1
|
||||
WHERE planet_id = $1
|
||||
`;
|
||||
|
||||
const exists = await this.database.get(query, [this.clientId]);
|
||||
const exists = await this.database.get(query, [this.planet_id]);
|
||||
return exists !== undefined;
|
||||
}
|
||||
|
||||
create() {
|
||||
const query = `
|
||||
INSERT INTO cart (client_id) values ($1) ON CONFLICT DO NOTHING
|
||||
INSERT INTO cart (planet_id) values ($1) ON CONFLICT DO NOTHING
|
||||
`;
|
||||
|
||||
return this.database.update(query, [this.clientId]);
|
||||
return this.database.update(query, [this.planet_id]);
|
||||
}
|
||||
|
||||
async add(product_no: number, product_sku_no: number, quantity: number) {
|
||||
@@ -68,7 +66,7 @@ class Cart {
|
||||
(
|
||||
SELECT cart_id
|
||||
FROM cart
|
||||
WHERE client_id = $1
|
||||
WHERE planet_id = $1
|
||||
),
|
||||
$2,
|
||||
$3,
|
||||
@@ -86,13 +84,13 @@ class Cart {
|
||||
AND cart_id = (
|
||||
SELECT cart_id
|
||||
FROM cart
|
||||
WHERE client_id = $1
|
||||
WHERE planet_id = $1
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
return this.database.update(query, [
|
||||
this.clientId,
|
||||
this.planet_id,
|
||||
product_no,
|
||||
product_sku_no,
|
||||
quantity,
|
||||
@@ -100,7 +98,7 @@ class Cart {
|
||||
}
|
||||
|
||||
remove(lineitem_id: number) {
|
||||
// TODO should match w/ cart.client_id
|
||||
// TODO should match w/ cart.planet_id
|
||||
const query = `
|
||||
DELETE FROM cart_lineitem
|
||||
WHERE lineitem_id = $1
|
||||
@@ -110,7 +108,7 @@ class Cart {
|
||||
}
|
||||
|
||||
decrement(lineitem_id: number) {
|
||||
// TODO should match w/ cart.client_id
|
||||
// TODO should match w/ cart.planet_id
|
||||
const query = `
|
||||
UPDATE cart_lineitem
|
||||
SET quantity = quantity - 1
|
||||
@@ -121,10 +119,23 @@ class Cart {
|
||||
}
|
||||
|
||||
async increment(lineitem_id: number) {
|
||||
const inStockQuery = `
|
||||
SELECT product_sku.stock >= cart_lineitem.quantity + 1 as in_stock
|
||||
FROM product_sku
|
||||
INNER JOIN cart_lineitem
|
||||
ON cart_lineitem.product_sku_no = product_sku.sku_id
|
||||
WHERE cart_lineitem.lineitem_id = $1;`;
|
||||
const inStockResponse = await this.database.get(inStockQuery, [
|
||||
lineitem_id,
|
||||
]);
|
||||
if (!inStockResponse?.in_stock || inStockResponse?.in_stock === false) {
|
||||
throw Error("Unable to add product, no more left in stock");
|
||||
}
|
||||
|
||||
// if (!productRepository.hasQuantityOfSkuInStock(lineitem_id)) {
|
||||
// throw new SkuQuantityNotInStockError("");
|
||||
// }
|
||||
// TODO should match w/ cart.client_id
|
||||
// TODO should match w/ cart.planet_id
|
||||
const query = `
|
||||
UPDATE cart_lineitem
|
||||
SET quantity = quantity + 1
|
||||
@@ -141,8 +152,8 @@ class Cart {
|
||||
removeItem(item: IProduct) {}
|
||||
|
||||
destroy() {
|
||||
const query = `DELETE FROM chart WHERE client_id = $1`;
|
||||
this.database.update(query, [this.clientId]);
|
||||
const query = `DELETE FROM cart WHERE planet_id = $1`;
|
||||
this.database.update(query, [this.planet_id]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,15 +14,17 @@ class CartSession {
|
||||
this.carts = [];
|
||||
}
|
||||
|
||||
getWSCartByClientId(clientId: string): WSCart {
|
||||
const match = this.carts.find((cart) => cart.wsCart?.clientId === clientId);
|
||||
getWSCartByplanet_id(planet_id: string): WSCart {
|
||||
const match = this.carts.find(
|
||||
(cart) => cart.wsCart?.planet_id === planet_id
|
||||
);
|
||||
if (!match) return null;
|
||||
return match?.wsCart;
|
||||
}
|
||||
|
||||
add(sessionId: string, cart: WSCart) {
|
||||
console.log(
|
||||
`adding session ${sessionId} with cart id: ${cart?.cart?.clientId}`
|
||||
`adding session ${sessionId} with cart id: ${cart?.cart?.planet_id}`
|
||||
);
|
||||
this.carts.push({ wsCart: cart, sessionId });
|
||||
}
|
||||
@@ -40,13 +42,13 @@ class CartSession {
|
||||
}
|
||||
|
||||
async emitChangeToClients(wsCart: WSCart) {
|
||||
const { clientId } = wsCart;
|
||||
const { planet_id } = wsCart;
|
||||
|
||||
const matchingCarts = this.carts.filter(
|
||||
(cart) => cart.wsCart.clientId === clientId
|
||||
(cart) => cart.wsCart.planet_id === planet_id
|
||||
);
|
||||
console.log(
|
||||
`emit change to all carts with id ${clientId}:`,
|
||||
`emit change to all carts with id ${planet_id}:`,
|
||||
matchingCarts.length
|
||||
);
|
||||
|
||||
@@ -68,7 +70,7 @@ class CartSession {
|
||||
this.carts.forEach((cart: IGlobalCart) => {
|
||||
console.table({
|
||||
isAlive: cart.wsCart?.isAlive,
|
||||
clientId: cart.wsCart?.clientId,
|
||||
planet_id: cart.wsCart?.planet_id,
|
||||
sessionId: cart?.sessionId,
|
||||
hasCart: cart.wsCart?.cart !== null,
|
||||
});
|
||||
|
||||
@@ -37,14 +37,14 @@ interface ICartPayload {
|
||||
|
||||
class WSCart {
|
||||
ws: WebSocket;
|
||||
clientId: string | null;
|
||||
planet_id: string | null;
|
||||
cart: Cart;
|
||||
cartSession: CartSession;
|
||||
|
||||
constructor(ws, clientId) {
|
||||
constructor(ws, planet_id) {
|
||||
this.ws = ws;
|
||||
this.clientId = clientId;
|
||||
this.cart = new Cart(clientId);
|
||||
this.planet_id = planet_id;
|
||||
this.cart = new Cart(planet_id);
|
||||
this.cartSession;
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ class WSCart {
|
||||
} catch (error) {
|
||||
// ????
|
||||
if (error.message) this.message(error?.message, false);
|
||||
this.cartSession.emitChangeToClients(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ class CustomerRepository {
|
||||
|
||||
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)
|
||||
INSERT INTO customer (customer_no, email, first_name, last_name, street_address, zip_code, city)
|
||||
VALUES (NULL, $1, $2, $3, $4, $5, $6)
|
||||
RETURNING customer_no
|
||||
`;
|
||||
|
||||
@@ -47,21 +47,3 @@ class CustomerRepository {
|
||||
}
|
||||
|
||||
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';
|
||||
// ```;
|
||||
|
||||
126
src/database/schemas/0000_functions.sql
Normal file
126
src/database/schemas/0000_functions.sql
Normal file
@@ -0,0 +1,126 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- Create a trigger function that takes no arguments.
|
||||
-- Trigger functions automatically have OLD, NEW records
|
||||
-- and TG_TABLE_NAME as well as others.
|
||||
CREATE OR REPLACE FUNCTION unique_order_id()
|
||||
RETURNS TRIGGER AS $$
|
||||
|
||||
-- Declare the variables we'll be using.
|
||||
DECLARE
|
||||
key TEXT;
|
||||
qry TEXT;
|
||||
found TEXT;
|
||||
BEGIN
|
||||
|
||||
-- generate the first part of a query as a string with safely
|
||||
-- escaped table name, using || to concat the parts
|
||||
qry := 'SELECT order_id FROM ' || quote_ident(TG_TABLE_NAME) || ' WHERE order_id=';
|
||||
|
||||
IF NEW.order_id IS NOT NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- This loop will probably only run once per call until we've generated
|
||||
-- millions of ids.
|
||||
LOOP
|
||||
|
||||
-- Generate our string bytes and re-encode as a base64 string.
|
||||
key := encode(gen_random_bytes(14), 'base64');
|
||||
|
||||
-- Base64 encoding contains 2 URL unsafe characters by default.
|
||||
-- The URL-safe version has these replacements.
|
||||
key := replace(key, '/', '_'); -- url safe replacement
|
||||
key := replace(key, '+', '-'); -- url safe replacement
|
||||
|
||||
-- Concat the generated key (safely quoted) with the generated query
|
||||
-- and run it.
|
||||
-- SELECT id FROM "test" WHERE id='blahblah' INTO found
|
||||
-- Now "found" will be the duplicated id or NULL.
|
||||
EXECUTE qry || quote_literal(key) INTO found;
|
||||
|
||||
-- Check to see if found is NULL.
|
||||
-- If we checked to see if found = NULL it would always be FALSE
|
||||
-- because (NULL = NULL) is always FALSE.
|
||||
IF found IS NULL THEN
|
||||
|
||||
-- If we didn't find a collision then leave the LOOP.
|
||||
EXIT;
|
||||
END IF;
|
||||
|
||||
-- We haven't EXITed yet, so return to the top of the LOOP
|
||||
-- and try again.
|
||||
END LOOP;
|
||||
|
||||
-- NEW and OLD are available in TRIGGER PROCEDURES.
|
||||
-- NEW is the mutated row that will actually be INSERTed.
|
||||
-- We're replacing id, regardless of what it was before
|
||||
-- with our key variable.
|
||||
NEW.order_id = key;
|
||||
|
||||
-- The RECORD returned here is what will actually be INSERTed,
|
||||
-- or what the next trigger will get if there is one.
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
CREATE OR REPLACE FUNCTION unique_customer_no()
|
||||
RETURNS TRIGGER AS $$
|
||||
|
||||
-- Declare the variables we'll be using.
|
||||
DECLARE
|
||||
key TEXT;
|
||||
qry TEXT;
|
||||
found TEXT;
|
||||
BEGIN
|
||||
|
||||
-- generate the first part of a query as a string with safely
|
||||
-- escaped table name, using || to concat the parts
|
||||
qry := 'SELECT customer_no FROM ' || quote_ident(TG_TABLE_NAME) || ' WHERE customer_no=';
|
||||
|
||||
IF NEW.customer_no IS NOT NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
-- This loop will probably only run once per call until we've generated
|
||||
-- millions of ids.
|
||||
LOOP
|
||||
|
||||
|
||||
-- Generate our string bytes and re-encode as a base64 string.
|
||||
key := encode(gen_random_bytes(14), 'base64');
|
||||
|
||||
-- Base64 encoding contains 2 URL unsafe characters by default.
|
||||
-- The URL-safe version has these replacements.
|
||||
key := replace(key, '/', '_'); -- url safe replacement
|
||||
key := replace(key, '+', '-'); -- url safe replacement
|
||||
|
||||
-- Concat the generated key (safely quoted) with the generated query
|
||||
-- and run it.
|
||||
-- SELECT id FROM "test" WHERE id='blahblah' INTO found
|
||||
-- Now "found" will be the duplicated id or NULL.
|
||||
EXECUTE qry || quote_literal(key) INTO found;
|
||||
|
||||
-- Check to see if found is NULL.
|
||||
-- If we checked to see if found = NULL it would always be FALSE
|
||||
-- because (NULL = NULL) is always FALSE.
|
||||
IF found IS NULL THEN
|
||||
|
||||
-- If we didn't find a collision then leave the LOOP.
|
||||
EXIT;
|
||||
END IF;
|
||||
|
||||
-- We haven't EXITed yet, so return to the top of the LOOP
|
||||
-- and try again.
|
||||
END LOOP;
|
||||
|
||||
-- NEW and OLD are available in TRIGGER PROCEDURES.
|
||||
-- NEW is the mutated row that will actually be INSERTed.
|
||||
-- We're replacing id, regardless of what it was before
|
||||
-- with our key variable.
|
||||
NEW.customer_no = key;
|
||||
|
||||
-- The RECORD returned here is what will actually be INSERTed,
|
||||
-- or what the next trigger will get if there is one.
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
@@ -2,13 +2,21 @@ CREATE TABLE IF NOT EXISTS product (
|
||||
product_no serial PRIMARY KEY,
|
||||
name text,
|
||||
description text,
|
||||
image text,
|
||||
subtext text,
|
||||
primary_color text,
|
||||
created timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated timestamp DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS image (
|
||||
image_id serial PRIMARY KEY,
|
||||
product_no integer REFERENCES product,
|
||||
url text,
|
||||
default_image boolean DEFAULT FALSE,
|
||||
created timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated timestamp DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS product_sku (
|
||||
sku_id serial PRIMARY KEY,
|
||||
product_no integer REFERENCES product,
|
||||
@@ -16,15 +24,19 @@ CREATE TABLE IF NOT EXISTS product_sku (
|
||||
size text,
|
||||
stock real,
|
||||
default_price boolean DEFAULT FALSE,
|
||||
unlisted boolean DEFAULT FALSE,
|
||||
created timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated timestamp DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE OR REPLACE VIEW product_info AS
|
||||
SELECT product.product_no, product_sku.sku_id, name, image, description, subtext, primary_color, price, size, stock, default_price
|
||||
SELECT product.product_no, product_sku.sku_id, name, image.url as image, description, subtext, primary_color, price, size, stock, default_price
|
||||
FROM product
|
||||
INNER JOIN product_sku
|
||||
ON product.product_no = product_sku.product_no;
|
||||
ON product.product_no = product_sku.product_no
|
||||
LEFT JOIN image
|
||||
ON product.product_no = image.product_no
|
||||
WHERE default_image = TRUE AND product_sku.unlisted != FALSE;
|
||||
|
||||
CREATE OR REPLACE VIEW available_products AS
|
||||
SELECT *
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
CREATE TABLE IF NOT EXISTS cart (
|
||||
cart_id serial PRIMARY KEY,
|
||||
client_id text,
|
||||
planet_id text,
|
||||
created timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated timestamp DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -14,14 +14,19 @@ CREATE TABLE IF NOT EXISTS cart_lineitem (
|
||||
);
|
||||
|
||||
CREATE OR REPLACE VIEW cart_detailed AS
|
||||
SELECT cart.client_id, cart.cart_id,
|
||||
SELECT cart.planet_id, cart.cart_id,
|
||||
cart_lineitem.lineitem_id, cart_lineitem.quantity,
|
||||
product_sku.sku_id, product_sku.size, product_sku.price,
|
||||
product.product_no, product.name, product.description, product.subtext, product.image, product.primary_color
|
||||
-- product.product_no, product.name, product.description, product.subtext, product.image, product.primary_color
|
||||
product.product_no, product.name, product.description, product.subtext, product.primary_color, image.url as image
|
||||
FROM cart
|
||||
INNER JOIN cart_lineitem
|
||||
ON cart.cart_id = cart_lineitem.cart_id
|
||||
INNER JOIN product_sku
|
||||
ON cart_lineitem.product_sku_no = product_sku.sku_id
|
||||
INNER JOIN product
|
||||
ON product.product_no = cart_lineitem.product_no;
|
||||
ON product.product_no = cart_lineitem.product_no
|
||||
LEFT JOIN image
|
||||
ON product.product_no = image.product_no
|
||||
WHERE image.default_image = TRUE;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS customer (
|
||||
customer_no varchar(36) PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
customer_no varchar(36) PRIMARY KEY DEFAULT unique_customer_no(),
|
||||
email text,
|
||||
first_name text,
|
||||
last_name text,
|
||||
@@ -8,4 +8,8 @@ CREATE TABLE IF NOT EXISTS customer (
|
||||
city text,
|
||||
created timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated timestamp DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TRIGGER trigger_customerno_genid
|
||||
BEFORE INSERT ON customer
|
||||
FOR EACH ROW EXECUTE PROCEDURE unique_customer_no();
|
||||
@@ -1,5 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
order_id varchar(36) PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
order_id varchar(36) PRIMARY KEY DEFAULT unique_order_id(),
|
||||
customer_no varchar(36) REFERENCES customer ON DELETE SET NULL,
|
||||
status text DEFAULT 'INITIATED',
|
||||
created timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -17,24 +17,14 @@ CREATE TABLE IF NOT EXISTS orders_lineitem (
|
||||
updated timestamp DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shipping (
|
||||
shipping_id serial PRIMARY KEY,
|
||||
order_id varchar(36) REFERENCES orders,
|
||||
shipping_company text,
|
||||
tracking_code text,
|
||||
tracking_link text,
|
||||
user_notified timestamp,
|
||||
created timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated timestamp DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TRIGGER trigger_orderid_genid
|
||||
BEFORE INSERT ON orders
|
||||
FOR EACH ROW EXECUTE PROCEDURE unique_order_id();
|
||||
|
||||
CREATE OR REPLACE VIEW orders_detailed AS
|
||||
SELECT
|
||||
orders.order_id as order_id, orders.status as order_status, orders.created as order_created, orders.updated as order_updated,
|
||||
customer.customer_no, customer.email, customer.first_name, customer.last_name, customer.street_address, customer.zip_code, customer.city,
|
||||
shipping.shipping_id, shipping.shipping_company, shipping.tracking_code, shipping.tracking_link, shipping.user_notified, shipping.created as shipping_created, shipping.updated as shipping_updated
|
||||
customer.customer_no, customer.email, customer.first_name, customer.last_name, customer.street_address, customer.zip_code, customer.city
|
||||
FROM orders
|
||||
INNER JOIN customer
|
||||
ON orders.customer_no = customer.customer_no
|
||||
JOIN shipping
|
||||
ON orders.order_id = shipping.order_id;
|
||||
ON orders.customer_no = customer.customer_no;
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
CREATE TABLE IF NOT EXISTS stripe_payments (
|
||||
order_id varchar(127) PRIMARY KEY,
|
||||
payment_id serial PRIMARY KEY,
|
||||
order_id varchar(36) REFERENCES orders ON DELETE SET NULL,
|
||||
created timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
-- transaction_text text,
|
||||
-- merchant_serial_number text,
|
||||
-- payment_payload json,
|
||||
stripe_initiation_response json,
|
||||
stripe_payment_response json,
|
||||
stripe_charge_response json,
|
||||
stripe_transaction_id text,
|
||||
stripe_status text DEFAULT 'CREATED',
|
||||
-- stripe_failed_payment_status text,
|
||||
|
||||
31
src/database/schemas/0012_shipping.sql
Normal file
31
src/database/schemas/0012_shipping.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
CREATE TABLE IF NOT EXISTS shipment_courier (
|
||||
shipment_courier_id serial PRIMARY KEY,
|
||||
name text,
|
||||
website text,
|
||||
has_api boolean,
|
||||
created timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated timestamp DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shipment (
|
||||
shipment_id serial PRIMARY KEY,
|
||||
order_id text REFERENCES orders ON DELETE CASCADE,
|
||||
courier_id int DEFAULT 0,
|
||||
tracking_code text,
|
||||
tracking_link text,
|
||||
user_notified boolean DEFAULT FALSE,
|
||||
created timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated timestamp DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shipment_event (
|
||||
shipment_event_id serial PRIMARY KEY,
|
||||
shipment_id serial REFERENCES shipment ON DELETE CASCADE,
|
||||
description text,
|
||||
status text,
|
||||
location text,
|
||||
event_time timestamp,
|
||||
api_payload json,
|
||||
created timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated timestamp DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -91,16 +91,22 @@ const readSeedFiles = () => {
|
||||
const seedFolder = path.join(__base, "database/seeds/");
|
||||
console.log(`Reading seeds from folder: ${seedFolder}\n`);
|
||||
|
||||
return fsPromises
|
||||
.readdir(seedFolder)
|
||||
.then((files) =>
|
||||
files.reverse().map((filePath) => {
|
||||
return fsPromises.readdir(seedFolder).then((files) => {
|
||||
let lastFileRead: string;
|
||||
try {
|
||||
return files.reverse().map((filePath) => {
|
||||
lastFileRead = filePath;
|
||||
const seedStep = new SeedStep(path.join(seedFolder, filePath));
|
||||
seedStep.readData();
|
||||
return seedStep;
|
||||
})
|
||||
)
|
||||
.catch(console.log);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`Unexpected error while reading seed files. File: ${lastFileRead} causing error`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
async function runAllSteps(seedSteps) {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"name": "Floral",
|
||||
"subtext": "By W.H Auden",
|
||||
"description": "Package include 10 pieces, Choose between A-F Key Value",
|
||||
"image": "https://storage.googleapis.com/planetposen-images/floral.jpg",
|
||||
"primary_color": "#E6E0DC"
|
||||
}
|
||||
},
|
||||
@@ -28,7 +27,6 @@
|
||||
"name": "Forrest",
|
||||
"subtext": "By W.H Auden",
|
||||
"description": "Package include 10 pieces, Choose between A-F Key Value",
|
||||
"image": "https://storage.googleapis.com/planetposen-images/forrest.jpg",
|
||||
"primary_color": "#C9B2A9"
|
||||
}
|
||||
},
|
||||
@@ -70,7 +68,6 @@
|
||||
"name": "Mush",
|
||||
"subtext": "By W.H Auden",
|
||||
"description": "Package include 10 pieces, Choose between A-F Key Value",
|
||||
"image": "https://storage.googleapis.com/planetposen-images/mush.jpg",
|
||||
"primary_color": "#231B1D"
|
||||
}
|
||||
},
|
||||
@@ -92,7 +89,6 @@
|
||||
"name": "The Buried Life",
|
||||
"subtext": "By W.H Auden",
|
||||
"description": "Package include 10 pieces, Choose between A-F Key Value",
|
||||
"image": "https://storage.googleapis.com/planetposen-images/the-buried-life.jpg",
|
||||
"primary_color": "#C9B2A9"
|
||||
}
|
||||
},
|
||||
@@ -114,7 +110,6 @@
|
||||
"name": "Cookie-Man Forrest",
|
||||
"subtext": "By W.H Auden",
|
||||
"description": "Package include 10 pieces, Choose between A-F Key Value",
|
||||
"image": "https://storage.googleapis.com/planetposen-images/cookie-man-forrest.jpg",
|
||||
"primary_color": "#E6E0DC"
|
||||
}
|
||||
},
|
||||
@@ -136,7 +131,6 @@
|
||||
"name": "Yorkshire Shoreline",
|
||||
"subtext": "By W.H Auden",
|
||||
"description": "Package include 10 pieces, Choose between A-F Key Value",
|
||||
"image": "https://storage.googleapis.com/planetposen-images/yorkshire-shoreline.jpg",
|
||||
"primary_color": "#E6E0DC"
|
||||
}
|
||||
},
|
||||
@@ -158,7 +152,6 @@
|
||||
"name": "Kneeling in Yemen",
|
||||
"subtext": "By W.H Auden",
|
||||
"description": "Package include 10 pieces, Choose between A-F Key Value",
|
||||
"image": "https://storage.googleapis.com/planetposen-images/kneeling-in-yemen.jpg",
|
||||
"primary_color": "#E6E0DC"
|
||||
}
|
||||
},
|
||||
@@ -180,7 +173,6 @@
|
||||
"name": "Spectural Forrest",
|
||||
"subtext": "By W.H Auden",
|
||||
"description": "Package include 10 pieces, Choose between A-F Key Value",
|
||||
"image": "https://storage.googleapis.com/planetposen-images/spectural-forrest.jpg",
|
||||
"primary_color": "#E6E0DC"
|
||||
}
|
||||
},
|
||||
@@ -194,5 +186,79 @@
|
||||
"default_price": true,
|
||||
"size": "Set"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "product",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"name": "Dino Camping",
|
||||
"subtext": "Snuggle Flannel Fabric",
|
||||
"description": " - Width: 43 inches\n - Content: 100% Cotton\n - Care: Machine wash normal cold, no bleach, tumble dry, do not iron\n - Imported",
|
||||
"primary_color": "#E6E0DC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "product_sku",
|
||||
"pk": 11,
|
||||
"fields": {
|
||||
"product_no": 9,
|
||||
"price": 150.0,
|
||||
"stock": 14,
|
||||
"default_price": true,
|
||||
"size": "Set"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "product",
|
||||
"pk": 10,
|
||||
"fields": {
|
||||
"name": "White Dinosaur Patches",
|
||||
"subtext": "",
|
||||
"description": "The power of cotton is an almighty force for kids attire. Our cotton spandex interlock is a thicker fabric that's durable and naturally comfortable. Plus with breathability and slight stretch, children's tops and dresses are free-moving during play time. Choose from various designs like this dinosaur patch pattern, and you have a loving sewing project for awesome practicality and personality.\n\n - Width: 57 Inches\n - Content: 98% Cotton 2% Spandex\n - Care: Machine wash gentle cold, nonchlorine bleach, line dry, cool iron.\n - Imported",
|
||||
"primary_color": "#B1E0DC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "product_sku",
|
||||
"pk": 12,
|
||||
"fields": {
|
||||
"product_no": 10,
|
||||
"price": 99.0,
|
||||
"stock": 14,
|
||||
"default_price": true,
|
||||
"size": "Medium"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "product_sku",
|
||||
"pk": 13,
|
||||
"fields": {
|
||||
"product_no": 10,
|
||||
"price": 140.0,
|
||||
"stock": 4,
|
||||
"default_price": false,
|
||||
"size": "Large"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "product",
|
||||
"pk": 11,
|
||||
"fields": {
|
||||
"name": "Oversized Blue And Yellow Swirls",
|
||||
"subtext": "",
|
||||
"description": " - 44'' Fabric by the Yard\n - 100% Cottton\n - Fabric Care: Machine Wash Normal, No Bleach, Tumble Dry Low\n - Printed in the USA from Imported Material.",
|
||||
"primary_color": "#E6E0DC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "product_sku",
|
||||
"pk": 14,
|
||||
"fields": {
|
||||
"product_no": 11,
|
||||
"price": 34.0,
|
||||
"stock": 8,
|
||||
"default_price": true,
|
||||
"size": "Small"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"model": "customer",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"customer_no": "7CB9A6B8-A526-4836-BF4E-67E1075F8B83",
|
||||
"customer_no": "fexQy5Q-UtCgzDxixNw=",
|
||||
"email": "kevin.midboe@gmail.com",
|
||||
"first_name": "kevin",
|
||||
"last_name": "midbøe",
|
||||
@@ -16,7 +16,7 @@
|
||||
"model": "customer",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"customer_no": "FFF49A98-0A2F-437D-9069-9664ADB2FFFE",
|
||||
"customer_no": "JEpfvCBrpxa4c1R7S8E=",
|
||||
"email": "Maia.Neteland@gmail.com",
|
||||
"first_name": "Maia",
|
||||
"last_name": "Neteland",
|
||||
@@ -29,7 +29,7 @@
|
||||
"model": "customer",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"customer_no": "DFC94AB1-9BB6-4ECF-8747-3E12751892AB",
|
||||
"customer_no": "-M_O4M1mvlu5nIVkeZk=",
|
||||
"email": "Aksel.Engeset@gmail.com",
|
||||
"first_name": "Aksel",
|
||||
"last_name": "Engeset",
|
||||
@@ -42,7 +42,7 @@
|
||||
"model": "customer",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"customer_no": "E235456D-C884-4828-BB0F-5065056BD57A",
|
||||
"customer_no": "XNHXUnjFK2Q7bqqbDrU=",
|
||||
"email": "Thomas.Langemyr@gmail.com",
|
||||
"first_name": "Thomas",
|
||||
"last_name": "Langemyr",
|
||||
@@ -55,7 +55,7 @@
|
||||
"model": "customer",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"customer_no": "3C1C1952-87E3-46A8-8B22-383B2F566E26",
|
||||
"customer_no": "Z9NB1MGeb_as6vScolY=",
|
||||
"email": "Frida.Nilsen@gmail.com",
|
||||
"first_name": "Frida",
|
||||
"last_name": "Nilsen",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"model": "cart",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"client_id": "800020696e96800f8904ea"
|
||||
"planet_id": "800020696e96800f8904ea"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
"model": "orders",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"order_id": "fb9a5910-0dcf-4c65-9c25-3fb3eb883ce5",
|
||||
"customer_no": "7CB9A6B8-A526-4836-BF4E-67E1075F8B83"
|
||||
"order_id": "ok4jI9EehsRLDHlS6cU=",
|
||||
"customer_no": "fexQy5Q-UtCgzDxixNw="
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "orders_lineitem",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"order_id": "fb9a5910-0dcf-4c65-9c25-3fb3eb883ce5",
|
||||
"order_id": "ok4jI9EehsRLDHlS6cU=",
|
||||
"product_no": 2,
|
||||
"product_sku_no": 2,
|
||||
"price": 30,
|
||||
@@ -22,7 +22,7 @@
|
||||
"model": "orders_lineitem",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"order_id": "fb9a5910-0dcf-4c65-9c25-3fb3eb883ce5",
|
||||
"order_id": "ok4jI9EehsRLDHlS6cU=",
|
||||
"product_no": 2,
|
||||
"product_sku_no": 3,
|
||||
"price": 50,
|
||||
@@ -33,31 +33,90 @@
|
||||
"model": "orders_lineitem",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"order_id": "fb9a5910-0dcf-4c65-9c25-3fb3eb883ce5",
|
||||
"order_id": "ok4jI9EehsRLDHlS6cU=",
|
||||
"product_no": 6,
|
||||
"product_sku_no": 8,
|
||||
"price": 98,
|
||||
"quantity": 18
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"model": "orders",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"order_id": "2E9EB68E-4224-46C8-9AA2-3A13A55005BA",
|
||||
"customer_no": "3C1C1952-87E3-46A8-8B22-383B2F566E26"
|
||||
"order_id": "30TfxKFRd3BeEIeF94M=",
|
||||
"customer_no": "Z9NB1MGeb_as6vScolY="
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "orders_lineitem",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"order_id": "2E9EB68E-4224-46C8-9AA2-3A13A55005BA",
|
||||
"order_id": "30TfxKFRd3BeEIeF94M=",
|
||||
"product_no": 2,
|
||||
"product_sku_no": 2,
|
||||
"price": 30,
|
||||
"quantity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "orders",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"order_id": "ezN6SQ6I5EEtSgCKmxE=",
|
||||
"customer_no": "-M_O4M1mvlu5nIVkeZk=",
|
||||
"status": "REFUNDED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "orders_lineitem",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"order_id": "ezN6SQ6I5EEtSgCKmxE=",
|
||||
"product_no": 2,
|
||||
"product_sku_no": 2,
|
||||
"price": 30,
|
||||
"quantity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "orders",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"order_id": "XTMt-bSA_wQnwfam5KM=",
|
||||
"customer_no": "XNHXUnjFK2Q7bqqbDrU=",
|
||||
"status": "CONFIRMED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "orders_lineitem",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"order_id": "XTMt-bSA_wQnwfam5KM=",
|
||||
"product_no": 7,
|
||||
"product_sku_no": 9,
|
||||
"price": 78,
|
||||
"quantity": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "orders",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"order_id": "0upJLUYPEYaOCeQMxPc=",
|
||||
"customer_no": "JEpfvCBrpxa4c1R7S8E=",
|
||||
"status": "CONFIRMED"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "orders_lineitem",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"order_id": "0upJLUYPEYaOCeQMxPc=",
|
||||
"product_no": 6,
|
||||
"product_sku_no": 8,
|
||||
"price": 98,
|
||||
"quantity": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,13 +1,55 @@
|
||||
[
|
||||
{
|
||||
"model": "shipping",
|
||||
"pk": 1,
|
||||
"model": "shipment_courier",
|
||||
"pk": 100001,
|
||||
"fields": {
|
||||
"shipping_id": 1,
|
||||
"order_id": "fb9a5910-0dcf-4c65-9c25-3fb3eb883ce5",
|
||||
"shipping_company": "Posten BRING",
|
||||
"shipment_courier_id": 100001,
|
||||
"name": "Posten BRING",
|
||||
"website": "http://posten.no",
|
||||
"has_api": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "shipment_courier",
|
||||
"pk": 100002,
|
||||
"fields": {
|
||||
"shipment_courier_id": 100002,
|
||||
"name": "HeltHjem",
|
||||
"website": "http://helthjem.no",
|
||||
"has_api": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "shipment",
|
||||
"pk": 100001,
|
||||
"fields": {
|
||||
"shipment_id": 100001,
|
||||
"order_id": "ok4jI9EehsRLDHlS6cU=",
|
||||
"courier_id": 100001,
|
||||
"tracking_code": "CS111111111NO",
|
||||
"tracking_link": "https://sporing.posten.no/sporing/CS111111111NO"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "shipment",
|
||||
"pk": 100002,
|
||||
"fields": {
|
||||
"shipment_id": 100002,
|
||||
"order_id": "XTMt-bSA_wQnwfam5KM=",
|
||||
"courier_id": 100001,
|
||||
"tracking_code": "LS395378164NL",
|
||||
"tracking_link": "https://sporing.posten.no/sporing/LS395378164NL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "shipment",
|
||||
"pk": 100003,
|
||||
"fields": {
|
||||
"shipment_id": 100003,
|
||||
"order_id": "0upJLUYPEYaOCeQMxPc=",
|
||||
"courier_id": 100001,
|
||||
"tracking_code": "LW138879799DE",
|
||||
"tracking_link": "https://sporing.posten.no/sporing/LW138879799DE"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
34
src/database/seeds/0007_stripe_payments.json
Normal file
34
src/database/seeds/0007_stripe_payments.json
Normal file
@@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"model": "stripe_payments",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"order_id": "ezN6SQ6I5EEtSgCKmxE=",
|
||||
"stripe_initiation_response": "{}",
|
||||
"stripe_payment_response": "{}",
|
||||
"stripe_charge_response": "{}",
|
||||
"stripe_transaction_id": "pi_3MEEltLueS5Cdr0C09n8AMEW",
|
||||
"stripe_status": "succeeded",
|
||||
"amount": "5000",
|
||||
"amount_received": "5000",
|
||||
"amount_captured": "5000",
|
||||
"amount_refunded": "5000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "stripe_payments",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"order_id": "XTMt-bSA_wQnwfam5KM=",
|
||||
"stripe_initiation_response": "{}",
|
||||
"stripe_payment_response": "{}",
|
||||
"stripe_charge_response": "{}",
|
||||
"stripe_transaction_id": "pi_3MEEzeLueS5Cdr0C0pBoFzW9",
|
||||
"stripe_status": "succeeded",
|
||||
"amount": "15600",
|
||||
"amount_received": "15600",
|
||||
"amount_captured": "15600",
|
||||
"amount_refunded": "0"
|
||||
}
|
||||
}
|
||||
]
|
||||
155
src/database/seeds/0008_image.json
Normal file
155
src/database/seeds/0008_image.json
Normal file
@@ -0,0 +1,155 @@
|
||||
[
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"product_no": 1,
|
||||
"default_image": true,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/floral.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"product_no": 2,
|
||||
"default_image": true,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/forrest.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"product_no": 3,
|
||||
"default_image": true,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/mush.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"product_no": 4,
|
||||
"default_image": true,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/the-buried-life.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"product_no": 5,
|
||||
"default_image": true,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/cookie-man-forrest.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 6,
|
||||
"fields": {
|
||||
"product_no": 6,
|
||||
"default_image": true,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/yorkshire-shoreline.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"product_no": 7,
|
||||
"default_image": false,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/spectural-forrest.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 8,
|
||||
"fields": {
|
||||
"product_no": 7,
|
||||
"default_image": true,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/kneeling-in-yemen.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 9,
|
||||
"fields": {
|
||||
"product_no": 8,
|
||||
"default_image": true,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/spectural-forrest.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 10,
|
||||
"fields": {
|
||||
"product_no": 9,
|
||||
"default_image": true,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/aef3eb500a6b12b896b7c567b85eded6301d5c4a.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 11,
|
||||
"fields": {
|
||||
"product_no": 9,
|
||||
"default_image": false,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/cc7632566fcda3dc6659b74fd57246f743f4050f.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 12,
|
||||
"fields": {
|
||||
"product_no": 10,
|
||||
"default_image": true,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/838074447f08f03c4b75ac2030dcd01201c0656c.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 13,
|
||||
"fields": {
|
||||
"product_no": 10,
|
||||
"default_image": false,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/76c9ed808f016de3c91fbe28ced51af027fd98b1.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 14,
|
||||
"fields": {
|
||||
"product_no": 10,
|
||||
"default_image": false,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/e84afa63c52194029a89fa5b14542480a5528e12.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 15,
|
||||
"fields": {
|
||||
"product_no": 10,
|
||||
"default_image": false,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/da851171b63d624e5f50c69c8f5a737e49b6b15b.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 16,
|
||||
"fields": {
|
||||
"product_no": 11,
|
||||
"default_image": true,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/2c47ed96b5e061d85f688849b998aa5e76c55c2a.jpg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "image",
|
||||
"pk": 17,
|
||||
"fields": {
|
||||
"product_no": 11,
|
||||
"default_image": false,
|
||||
"url": "https://storage.googleapis.com/planetposen-images/4329b24795654e2c57af859f39081a3891ba695a.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
56
src/email.ts
Normal file
56
src/email.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type ICustomer from "./interfaces/ICustomer";
|
||||
import type { IProduct } from "./interfaces/IProduct";
|
||||
import logger from "./logger";
|
||||
|
||||
class Email {
|
||||
baseUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = "http://localhost:8005/api/v1";
|
||||
}
|
||||
|
||||
sendConfirmation(orderId: string, customer: ICustomer, products: IProduct[]) {
|
||||
const url = this.baseUrl + "/send-confirmation";
|
||||
let sum = 75; // shipping
|
||||
let options = {};
|
||||
|
||||
try {
|
||||
products.forEach(
|
||||
(product: IProduct) => (sum = sum + product.price * product.quantity)
|
||||
);
|
||||
|
||||
options = {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
email: customer?.email,
|
||||
orderId,
|
||||
customer: {
|
||||
FirstName: customer.first_name,
|
||||
LastName: customer.last_name,
|
||||
StreetAddress: customer.street_address,
|
||||
ZipCode: String(customer.zip_code),
|
||||
city: customer.city,
|
||||
},
|
||||
products,
|
||||
sum,
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Unable to parse send confirmation input data", {
|
||||
error,
|
||||
orderId,
|
||||
});
|
||||
// throw error
|
||||
return;
|
||||
}
|
||||
|
||||
return fetch(url, options)
|
||||
.then((resp) => resp.json())
|
||||
.catch((error) => {
|
||||
logger.error("Unexpected error from send confirmation", { error });
|
||||
// throw error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default Email;
|
||||
29
src/errors/posten.ts
Normal file
29
src/errors/posten.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export class PostenNotFoundError extends Error {
|
||||
reason: string;
|
||||
apiError: object;
|
||||
statusCode: number;
|
||||
|
||||
constructor(apiError) {
|
||||
const message = "Tracking code not found at Posten BRING";
|
||||
super(message);
|
||||
|
||||
this.statusCode = 404;
|
||||
this.reason = apiError?.errorMessage;
|
||||
this.apiError = apiError;
|
||||
}
|
||||
}
|
||||
|
||||
export class PostenInvalidQueryError extends Error {
|
||||
reason: string;
|
||||
apiError: object;
|
||||
statusCode: number;
|
||||
|
||||
constructor(apiError) {
|
||||
const message = "Unable to search for tracking code";
|
||||
super(message);
|
||||
|
||||
this.statusCode = apiError?.consignmentSet?.[0]?.error?.code || 500;
|
||||
this.reason = apiError?.consignmentSet?.[0]?.error?.message;
|
||||
this.apiError = apiError;
|
||||
}
|
||||
}
|
||||
10
src/errors/product.ts
Normal file
10
src/errors/product.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export class ProductNotFoundError extends Error {
|
||||
statusCode: number;
|
||||
|
||||
constructor() {
|
||||
const message = "Could not find a product with that id";
|
||||
super(message);
|
||||
|
||||
this.statusCode = 404;
|
||||
}
|
||||
}
|
||||
2
src/index.d.ts
vendored
2
src/index.d.ts
vendored
@@ -3,7 +3,7 @@ export {};
|
||||
declare global {
|
||||
namespace Express {
|
||||
export interface Request {
|
||||
planetId?: string;
|
||||
planet_id?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default interface ICart {
|
||||
client_id: string;
|
||||
planet_id: string;
|
||||
cart_id: number;
|
||||
lineitem_id: number;
|
||||
quantity: number;
|
||||
|
||||
24
src/interfaces/IShipping.ts
Normal file
24
src/interfaces/IShipping.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface IShippingCourier {
|
||||
id: string;
|
||||
name: string;
|
||||
website: string;
|
||||
has_api: boolean;
|
||||
}
|
||||
|
||||
export interface IShipment {
|
||||
id: string;
|
||||
courier_id: string;
|
||||
tracking_code: string;
|
||||
tracking_link: string;
|
||||
}
|
||||
|
||||
export interface IShipmentEvent {
|
||||
event_id: string;
|
||||
shipment_id: string;
|
||||
description: string;
|
||||
status: string;
|
||||
location: string;
|
||||
event_time: Date;
|
||||
updated: Date;
|
||||
created: Date;
|
||||
}
|
||||
@@ -25,7 +25,7 @@ const customLevels = {
|
||||
};
|
||||
|
||||
const appendPlanetId = winston.format((log) => {
|
||||
log.planetId = httpContext.get("planetId");
|
||||
log.planet_id = httpContext.get("planet_id");
|
||||
return log;
|
||||
});
|
||||
|
||||
@@ -39,7 +39,10 @@ const logger = winston.createLogger({
|
||||
levels: customLevels.levels,
|
||||
transports: [
|
||||
new winston.transports.File({
|
||||
filename: "./logs/all-logs.log",
|
||||
filename: "/var/log/planetposen_logs/planetposen-backend.log",
|
||||
maxsize: 100000000, // 100 MB
|
||||
tailable: true,
|
||||
maxFiles: 3,
|
||||
format: winston.format.combine(appendPlanetId(), appName(), ecsFormat()),
|
||||
}),
|
||||
],
|
||||
|
||||
131
src/order.ts
131
src/order.ts
@@ -1,9 +1,12 @@
|
||||
import logger from "./logger";
|
||||
import establishedDatabase from "./database";
|
||||
import ShippingRepository from "./shipping";
|
||||
import { ORDER_STATUS_CODES } from "./enums/order";
|
||||
import type { IOrder } from "./interfaces/IOrder";
|
||||
import type { IProduct } from "./interfaces/IProduct";
|
||||
|
||||
const shippingRepository = new ShippingRepository();
|
||||
|
||||
class MissingOrderError extends Error {
|
||||
statusCode: number;
|
||||
|
||||
@@ -38,9 +41,8 @@ class OrderRepository {
|
||||
|
||||
async newOrder(customer_no: string) {
|
||||
const query = `
|
||||
INSERT INTO orders (customer_no) VALUES ($1)
|
||||
RETURNING order_id
|
||||
`;
|
||||
INSERT INTO orders (order_id, customer_no) VALUES (NULL, $1)
|
||||
RETURNING order_id`;
|
||||
|
||||
logger.info("Creating order with customer", { customer_no });
|
||||
return await this.database.get(query, [customer_no]);
|
||||
@@ -55,8 +57,7 @@ class OrderRepository {
|
||||
) {
|
||||
const query = `
|
||||
INSERT INTO orders_lineitem (order_id, product_no, product_sku_no, price, quantity)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`;
|
||||
VALUES ($1, $2, $3, $4, $5)`;
|
||||
|
||||
logger.info("Adding lineitem to order", {
|
||||
order_id,
|
||||
@@ -80,8 +81,7 @@ class OrderRepository {
|
||||
FROM orders
|
||||
WHERE product_no = $1
|
||||
AND NOT status = ''
|
||||
AND end_time > now()
|
||||
`;
|
||||
AND end_time > now()`;
|
||||
|
||||
return this.database.all(query, [product.product_no]);
|
||||
}
|
||||
@@ -95,8 +95,7 @@ class OrderRepository {
|
||||
AND NOT order_id = $2
|
||||
AND NOT status = $3
|
||||
AND NOT status = $4
|
||||
AND NOT status = $5
|
||||
`;
|
||||
AND NOT status = $5`;
|
||||
|
||||
return {
|
||||
order,
|
||||
@@ -112,95 +111,88 @@ class OrderRepository {
|
||||
|
||||
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;`;
|
||||
SELECT orders.created, orders.order_id, customer.first_name, customer.last_name, customer.email, status, orders_lines.order_sum
|
||||
FROM orders
|
||||
INNER JOIN customer
|
||||
ON customer.customer_no = orders.customer_no
|
||||
LEFT 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
|
||||
ORDER BY orders.updated DESC;`;
|
||||
|
||||
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`;
|
||||
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 paymentQuery = `
|
||||
SELECT 'stripe' as type, stripe_transaction_id, created, updated, stripe_status, amount, amount_received, amount_captured, amount_refunded
|
||||
FROM stripe_payments
|
||||
WHERE order_id = $1`;
|
||||
|
||||
const orderQuery = `
|
||||
SELECT order_id as orderId, created, updated, status
|
||||
FROM orders
|
||||
WHERE order_id = $1`;
|
||||
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`;
|
||||
SELECT product.name, orders_lineitem.quantity, image.url as image, orders_lineitem.price, product_sku.sku_id, 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
|
||||
LEFT JOIN image
|
||||
ON product.product_no = image.product_no
|
||||
WHERE orders_lineitem.order_id = $1 AND image.default_image = TRUE`;
|
||||
|
||||
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([
|
||||
const [order, customer, shipping, lineItems, payment] = await Promise.all([
|
||||
this.database.get(orderQuery, [orderId]),
|
||||
this.database.get(customerQuery, [orderId]),
|
||||
this.database.get(shippingQuery, [orderId]),
|
||||
shippingRepository.getByOrderId(orderId),
|
||||
this.database.all(lineItemsQuery, [orderId]),
|
||||
this.database.get(paymentQuery, [orderId]),
|
||||
]);
|
||||
|
||||
return {
|
||||
...order,
|
||||
customer,
|
||||
shipping,
|
||||
payment,
|
||||
lineItems,
|
||||
};
|
||||
}
|
||||
|
||||
async getOrder(orderId): Promise<IOrder> {
|
||||
const orderQuery = `
|
||||
SELECT order_id, customer_no, created, updated, status
|
||||
FROM orders
|
||||
WHERE order_id = $1`;
|
||||
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`;
|
||||
SELECT product.name, image.url as 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
|
||||
LEFT JOIN image
|
||||
ON product.product_no = image.product_no
|
||||
WHERE orders_lineitem.order_id = $1 AND image.default_image = TRUE`;
|
||||
|
||||
const [order, lineItems] = await Promise.all([
|
||||
this.database.get(orderQuery, [orderId]),
|
||||
@@ -346,7 +338,6 @@ WHERE orders_lineitem.order_id = $1`;
|
||||
}
|
||||
|
||||
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`;
|
||||
|
||||
258
src/product.ts
258
src/product.ts
@@ -9,76 +9,84 @@ class ProductRepository {
|
||||
|
||||
async add(
|
||||
name = "foo",
|
||||
description = "foo",
|
||||
image = "/static/no-product.png",
|
||||
subtext = "foo",
|
||||
primary_color = "foo"
|
||||
description = "foo baz",
|
||||
subtext = "foo bar",
|
||||
primary_color = "#E6E0DC"
|
||||
) {
|
||||
const query = `
|
||||
INSERT INTO
|
||||
product (name, description, image, subtext, primary_color)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`;
|
||||
INSERT INTO
|
||||
product (name, description, subtext, primary_color)
|
||||
VALUES ($1, $2, $3, $4)`;
|
||||
|
||||
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, []);
|
||||
const productNumberQuery = `SELECT currval('product_product_no_seq')`;
|
||||
const productCurrVal = await this.database.get(productNumberQuery, []);
|
||||
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`;
|
||||
SELECT product.*, image.url as image, variations
|
||||
FROM product
|
||||
JOIN (
|
||||
SELECT product_no, count(size) AS variations
|
||||
FROM product_sku
|
||||
WHERE unlisted = FALSE
|
||||
GROUP BY product_no
|
||||
) AS product_sku
|
||||
ON product.product_no = product_sku.product_no
|
||||
LEFT JOIN image
|
||||
ON product.product_no = image.product_no
|
||||
WHERE default_image = TRUE
|
||||
ORDER BY product.updated DESC`;
|
||||
|
||||
return this.database.all(query, []);
|
||||
}
|
||||
|
||||
async get(productId) {
|
||||
async get(product_no) {
|
||||
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`;
|
||||
SELECT sku_id, size, price, stock, default_price, updated, created
|
||||
FROM product_sku
|
||||
WHERE product_no = $1 AND unlisted = FALSE
|
||||
ORDER BY created`;
|
||||
const imageQuery = `
|
||||
SELECT image_id, url, default_image
|
||||
FROM image
|
||||
WHERE product_no = $1
|
||||
ORDER BY default_image DESC`;
|
||||
|
||||
const product = await this.database.get(productQuery, [product_no]);
|
||||
const productSkus = await this.database.all(skuQuery, [product_no]);
|
||||
const images = await this.database.all(imageQuery, [product_no]);
|
||||
|
||||
const productSkus = await this.database.all(skuQuery, [productId]);
|
||||
return Promise.resolve({
|
||||
...product,
|
||||
variations: productSkus,
|
||||
images,
|
||||
});
|
||||
}
|
||||
|
||||
async addSku(
|
||||
productId,
|
||||
product_no,
|
||||
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)
|
||||
`;
|
||||
INSERT INTO
|
||||
product_sku (product_no, price, size, stock, default_price)
|
||||
VALUES ($1, $2, $3, $4, $5)`;
|
||||
|
||||
return this.database.update(query, [
|
||||
productId,
|
||||
product_no,
|
||||
price,
|
||||
size,
|
||||
stock,
|
||||
@@ -86,50 +94,45 @@ VALUES ($1, $2, $3, $4, $5)
|
||||
]);
|
||||
}
|
||||
|
||||
getSkus(productId) {
|
||||
getSkus(product_no) {
|
||||
const q = `SELECT sku_id, product_no, price, size, stock, default_price, created, updated
|
||||
FROM product_sku
|
||||
WHERE product_no = $1
|
||||
ORDER BY created`;
|
||||
FROM product_sku
|
||||
WHERE product_no = $1 AND unlisted = FALSE
|
||||
ORDER BY created`;
|
||||
|
||||
return this.database.all(q, [productId]);
|
||||
return this.database.all(q, [product_no]);
|
||||
}
|
||||
|
||||
async getSkuStock(skuId) {
|
||||
async getSkuStock(sku_id) {
|
||||
const query = "SELECT stock FROM product_sku WHERE sku_id = $1";
|
||||
const stockResponse = await this.database.get(query, [skuId]);
|
||||
const stockResponse = await this.database.get(query, [sku_id]);
|
||||
return stockResponse?.stock || null;
|
||||
}
|
||||
|
||||
// helper
|
||||
async hasQuantityOfSkuInStock(skuId, quantity) {
|
||||
const stock = await this.getSkuStock(skuId);
|
||||
async hasQuantityOfSkuInStock(sku_id, quantity) {
|
||||
const stock = await this.getSkuStock(sku_id);
|
||||
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;
|
||||
updateProduct(
|
||||
product_no: string,
|
||||
name: string,
|
||||
description: string,
|
||||
subtext: string,
|
||||
primary_color: string
|
||||
) {
|
||||
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
|
||||
`;
|
||||
UPDATE product
|
||||
SET name = $1, description = $2, subtext = $3, primary_color = $4, updated = to_timestamp($5 / 1000.0)
|
||||
WHERE product_no = $6`;
|
||||
|
||||
return this.database.update(query, [
|
||||
name,
|
||||
description,
|
||||
image,
|
||||
subtext,
|
||||
primary_color,
|
||||
new Date().getTime(),
|
||||
@@ -137,66 +140,131 @@ WHERE product_no = $7
|
||||
]);
|
||||
}
|
||||
|
||||
updateSku(productId, skuId, stock = 0, size = 0, price = 10) {
|
||||
updateSku(product_no, sku_id, 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,
|
||||
]);
|
||||
UPDATE product_sku
|
||||
SET
|
||||
price = $1,
|
||||
size = $2,
|
||||
stock = $3,
|
||||
updated = to_timestamp($4 / 1000.0)
|
||||
WHERE product_no = $5 and sku_id = $6`;
|
||||
|
||||
return this.database.update(query, [
|
||||
price,
|
||||
size,
|
||||
stock,
|
||||
new Date().getTime(),
|
||||
productId,
|
||||
skuId,
|
||||
product_no,
|
||||
sku_id,
|
||||
]);
|
||||
}
|
||||
|
||||
async setSkuDefaultPrice(productId, skuId) {
|
||||
async setSkuDefaultPrice(product_no, sku_id) {
|
||||
const resetOld = `
|
||||
UPDATE product_sku
|
||||
SET default_price = false, updated = to_timestamp($1 / 1000.0)
|
||||
WHERE product_no = $2 and default_price = true`;
|
||||
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
|
||||
`;
|
||||
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(resetOld, [new Date().getTime(), product_no]);
|
||||
await this.database.update(setNew, [
|
||||
new Date().getTime(),
|
||||
productId,
|
||||
skuId,
|
||||
product_no,
|
||||
sku_id,
|
||||
]);
|
||||
}
|
||||
|
||||
getDefaultPrice(productId, skuId) {
|
||||
getDefaultPrice(product_no, sku_id) {
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM product_sku
|
||||
WHERE default_price = true and product_no = $1 and sku_id = $2`;
|
||||
SELECT *
|
||||
FROM product_sku
|
||||
WHERE default_price = true and product_no = $1 and sku_id = $2`;
|
||||
|
||||
return this.database.query(query, [productId, skuId]);
|
||||
return this.database.query(query, [product_no, sku_id]);
|
||||
}
|
||||
|
||||
deleteSku(productId, skuId) {
|
||||
const query = `DELETE from product_sku WHERE product_no = $1 AND sku_id = $2`;
|
||||
return this.database.update(query, [productId, skuId]);
|
||||
deleteSku(product_no, sku_id) {
|
||||
const query = `
|
||||
UPDATE product_sku
|
||||
SET unlisted = TRUE
|
||||
WHERE product_no = $1 AND sku_id = $2`;
|
||||
|
||||
return this.database.update(query, [product_no, sku_id]);
|
||||
}
|
||||
|
||||
addImage(product_no, url: string) {
|
||||
const query = `
|
||||
INSERT INTO image (product_no, url)
|
||||
VALUES ($1, $2)
|
||||
RETURNING image_id`;
|
||||
|
||||
return this.database.get(query, [product_no, url]);
|
||||
}
|
||||
|
||||
getImages(product_no) {
|
||||
const query = `
|
||||
SELECT image_id, url, default_image
|
||||
FROM image
|
||||
WHERE product_no = $1
|
||||
ORDER BY created`;
|
||||
|
||||
return this.database.all(query, [product_no]);
|
||||
}
|
||||
|
||||
async setDefaultImage(product_no, image_id) {
|
||||
const resetDefaultImageQuery = `
|
||||
UPDATE image
|
||||
SET default_image = false, updated = to_timestamp($1 / 1000.0)
|
||||
WHERE product_no = $2 and default_image = true`;
|
||||
|
||||
const setNewDefaultImageQuery = `
|
||||
UPDATE image
|
||||
SET default_image = true, updated = to_timestamp($1 / 1000.0)
|
||||
WHERE product_no = $2 AND image_id = $3`;
|
||||
|
||||
await this.database.update(resetDefaultImageQuery, [
|
||||
new Date().getTime(),
|
||||
product_no,
|
||||
]);
|
||||
await this.database.update(setNewDefaultImageQuery, [
|
||||
new Date().getTime(),
|
||||
product_no,
|
||||
image_id,
|
||||
]);
|
||||
}
|
||||
|
||||
async deleteImage(product_no, image_id) {
|
||||
const isDefaultImageQuery = `
|
||||
SELECT default_image
|
||||
FROM image
|
||||
WHERE product_no = $1 AND image_id = $2`;
|
||||
|
||||
const isDefaultImage = await this.database.get(isDefaultImageQuery, [
|
||||
product_no,
|
||||
image_id,
|
||||
]);
|
||||
if (isDefaultImage) {
|
||||
const images = await this.getImages(product_no);
|
||||
const imagesWithoutAboutToDelete = images.filter(
|
||||
(image) => image.image_id !== image_id
|
||||
);
|
||||
if (imagesWithoutAboutToDelete.length > 0) {
|
||||
await this.setDefaultImage(
|
||||
product_no,
|
||||
imagesWithoutAboutToDelete[0].image_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const query = `
|
||||
DELETE FROM image
|
||||
WHERE product_no = $1 AND image_id = $2`;
|
||||
|
||||
return this.database.update(query, [product_no, image_id]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
90
src/shipping/index.ts
Normal file
90
src/shipping/index.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import logger from "../logger";
|
||||
import establishedDatabase from "../database";
|
||||
import PostenRepository from "./posten";
|
||||
|
||||
const posten = new PostenRepository();
|
||||
|
||||
class ShippingRepository {
|
||||
database: typeof establishedDatabase;
|
||||
|
||||
constructor(database = establishedDatabase) {
|
||||
this.database = database || establishedDatabase;
|
||||
}
|
||||
|
||||
async track(trackingCode: string) {
|
||||
const trackResponse = await posten.track(trackingCode);
|
||||
console.log("trackResponse:", trackResponse);
|
||||
|
||||
return trackResponse;
|
||||
}
|
||||
|
||||
async create(orderId: string) {
|
||||
const query = `
|
||||
INSERT INTO shipment (order_id, courier_id)
|
||||
VALUES ($1, NULL)
|
||||
RETURNING shipment_id`;
|
||||
|
||||
return this.database.get(query, [orderId]);
|
||||
}
|
||||
|
||||
async update(
|
||||
shipmentId: string,
|
||||
courierId: string,
|
||||
trackingCode: string,
|
||||
trackingLink: string
|
||||
) {
|
||||
const query = `
|
||||
UPDATE shipment
|
||||
SET courier_id = $1, tracking_code = $2, tracking_link = $3
|
||||
WHERE shipment_id = $4
|
||||
RETURNING shipment_id`;
|
||||
|
||||
return this.database.get(query, [
|
||||
courierId,
|
||||
trackingCode,
|
||||
trackingLink,
|
||||
shipmentId,
|
||||
]);
|
||||
}
|
||||
|
||||
async get(shipmentId: string) {
|
||||
const query = `
|
||||
SELECT shipment_id, order_id, shipment_courier.name as courier, shipment_courier.has_api, shipment_courier.shipment_courier_id as courier_id, tracking_code, tracking_link, user_notified
|
||||
FROM shipment
|
||||
LEFT JOIN shipment_courier
|
||||
ON shipment.courier_id = shipment_courier.shipment_courier_id
|
||||
WHERE shipment_id = $1`;
|
||||
|
||||
return this.database.get(query, [shipmentId]);
|
||||
}
|
||||
|
||||
async getByOrderId(orderId: string) {
|
||||
const query = `
|
||||
SELECT shipment_id, order_id, shipment_courier.name as courier, shipment_courier.has_api, shipment_courier.shipment_courier_id as courier_id, tracking_code, tracking_link, user_notified
|
||||
FROM shipment
|
||||
INNER JOIN shipment_courier
|
||||
ON shipment.courier_id = shipment_courier.shipment_courier_id
|
||||
WHERE order_id = $1`;
|
||||
|
||||
return this.database.get(query, [orderId]);
|
||||
}
|
||||
|
||||
getCourier(courierId: string) {
|
||||
const query = `
|
||||
SELECT shipment_courier_id AS courier_id, name, website, has_api
|
||||
FROM shipment_courier
|
||||
WHERE shipment_courier_id = $1`;
|
||||
|
||||
return this.database.get(query, [courierId]);
|
||||
}
|
||||
|
||||
getAllCouriers() {
|
||||
const query = `
|
||||
SELECT shipment_courier_id AS courier_id, name, website, has_api
|
||||
FROM shipment_courier`;
|
||||
|
||||
return this.database.all(query, []);
|
||||
}
|
||||
}
|
||||
|
||||
export default ShippingRepository;
|
||||
59
src/shipping/posten.ts
Normal file
59
src/shipping/posten.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { PostenInvalidQueryError, PostenNotFoundError } from "../errors/posten";
|
||||
|
||||
class Posten {
|
||||
trackUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.trackUrl = "https://sporing.posten.no/tracking/api/fetch";
|
||||
}
|
||||
|
||||
track(id: string) {
|
||||
const url = new URL(this.trackUrl);
|
||||
url.searchParams.append("query", id);
|
||||
url.searchParams.append("lang", "no");
|
||||
|
||||
return fetch(url.href)
|
||||
.then((resp) => resp.json())
|
||||
.then((response) => {
|
||||
if (response?.errorCode && response?.errorCode === "2404") {
|
||||
throw new PostenNotFoundError(response);
|
||||
}
|
||||
|
||||
if (
|
||||
response?.consignmentSet?.[0]?.error &&
|
||||
response?.consignmentSet?.[0]?.error?.code === 400
|
||||
) {
|
||||
throw new PostenInvalidQueryError(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
})
|
||||
.then((response) => Posten.transformPostenResponse(response));
|
||||
}
|
||||
|
||||
static transformPostenResponse(response: object) {
|
||||
const l = response?.consignmentSet?.[0];
|
||||
// delete l.packageSet
|
||||
return {
|
||||
weight: l.totalWeightInKgs,
|
||||
name: l.packageSet?.[0].productName,
|
||||
events: l.packageSet?.[0].eventSet?.map((event) => {
|
||||
return {
|
||||
description: event.description,
|
||||
city: event.city,
|
||||
country: event.country,
|
||||
countryCode: event.countryCode,
|
||||
date: event.dateIso,
|
||||
status: Posten.formatEventStatus(event.status),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
static formatEventStatus(status: string) {
|
||||
const s = status.toLowerCase()?.replaceAll("_", " ");
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
export default Posten;
|
||||
@@ -19,12 +19,12 @@ class StripeApi {
|
||||
}
|
||||
|
||||
async createPaymentIntent(
|
||||
clientId: string,
|
||||
planet_id: string,
|
||||
total: number,
|
||||
orderId: string,
|
||||
customer: ICustomer
|
||||
): Promise<Stripe.Response<Stripe.PaymentIntent>> {
|
||||
const stripeCustomer = await this.createCustomer(clientId, customer);
|
||||
const stripeCustomer = await this.createCustomer(planet_id, customer);
|
||||
const paymentIntent = await this.stripe.paymentIntents.create({
|
||||
customer: stripeCustomer?.id,
|
||||
amount: total * 100,
|
||||
@@ -34,7 +34,7 @@ class StripeApi {
|
||||
address: stripeCustomer.address,
|
||||
},
|
||||
metadata: {
|
||||
clientId,
|
||||
planet_id,
|
||||
orderId,
|
||||
},
|
||||
});
|
||||
@@ -42,7 +42,7 @@ class StripeApi {
|
||||
return paymentIntent;
|
||||
}
|
||||
|
||||
async createCustomer(clientId: string, customer: ICustomer) {
|
||||
async createCustomer(planet_id: string, customer: ICustomer) {
|
||||
return await this.stripe.customers.create({
|
||||
email: customer.email,
|
||||
name: `${customer.first_name} ${customer.last_name}`,
|
||||
@@ -52,7 +52,7 @@ class StripeApi {
|
||||
postal_code: String(customer.zip_code),
|
||||
},
|
||||
metadata: {
|
||||
clientId,
|
||||
planet_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import establishedDatabase from "../database";
|
||||
import Configuration from "../config/configuration";
|
||||
import StripeApi from "./stripeApi";
|
||||
import Stripe from "stripe";
|
||||
import logger from "../logger";
|
||||
import type ICustomer from "../interfaces/ICustomer";
|
||||
|
||||
const configuration = Configuration.getInstance();
|
||||
@@ -38,46 +39,59 @@ class StripeRepository {
|
||||
updatePaymentIntent(payload: Stripe.Response<Stripe.PaymentIntent>) {
|
||||
const query = `
|
||||
UPDATE stripe_payments
|
||||
SET stripe_status = $2, amount_received = $3, updated = $4
|
||||
SET stripe_status = $2, amount_received = $3, updated = $4, stripe_payment_response = $5
|
||||
WHERE order_id = $1`;
|
||||
|
||||
logger.info("Updating stripe payment intent", { payment_intent: payload });
|
||||
|
||||
return this.database.update(query, [
|
||||
payload.metadata.orderId,
|
||||
payload.status,
|
||||
payload.amount_received,
|
||||
new Date(),
|
||||
payload,
|
||||
]);
|
||||
}
|
||||
|
||||
updatePaymentCharge(payload: Stripe.Response<Stripe.Charge>) {
|
||||
const query = `
|
||||
UPDATE stripe_payments
|
||||
SET stripe_status = $2, amount_captured = $3, amount_refunded = $4, updated = $5
|
||||
SET stripe_status = $2, amount_captured = $3, amount_refunded = $4, updated = $5, stripe_charge_response = $6
|
||||
WHERE order_id = $1
|
||||
`;
|
||||
|
||||
logger.info("Updating stripe payment charge", { payment_charge: payload });
|
||||
|
||||
return this.database.update(query, [
|
||||
payload.metadata.orderId,
|
||||
payload.status,
|
||||
payload.amount_captured,
|
||||
payload.amount_refunded,
|
||||
new Date(),
|
||||
payload,
|
||||
]);
|
||||
}
|
||||
|
||||
async createPayment(
|
||||
clientId: string,
|
||||
planet_id: string,
|
||||
total: number,
|
||||
orderId: string,
|
||||
customer: ICustomer
|
||||
) {
|
||||
const paymentIntent = await stripeApi.createPaymentIntent(
|
||||
clientId,
|
||||
planet_id,
|
||||
total,
|
||||
orderId,
|
||||
customer
|
||||
);
|
||||
|
||||
logger.info("Payment intent from stripe", {
|
||||
payment_intent: paymentIntent,
|
||||
planet_id,
|
||||
order_id: orderId,
|
||||
customer_no: customer.customer_no,
|
||||
});
|
||||
|
||||
return this.commitPaymentToDatabase(orderId, paymentIntent).then(
|
||||
() => paymentIntent.client_secret
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const hex = "0123456789abcdef";
|
||||
|
||||
export default function generateClientId(len = 22) {
|
||||
export default function generateUUID(len = 22) {
|
||||
let output = "";
|
||||
for (let i = 0; i < len; ++i) {
|
||||
output += hex.charAt(Math.floor(Math.random() * hex.length));
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import establishedDatabase from "./database";
|
||||
import type { IProductWithSkus } from "./interfaces/IProduct";
|
||||
import ProductRepository from "./product";
|
||||
|
||||
const productRepository = new ProductRepository();
|
||||
|
||||
// interface IProductSku {
|
||||
// id: string
|
||||
@@ -26,32 +29,35 @@ class WarehouseRepository {
|
||||
}
|
||||
|
||||
async getProduct(productId): Promise<IProductWithSkus> {
|
||||
const productQuery = `SELECT * FROM product WHERE product_no = $1`;
|
||||
const product = await this.database.get(productQuery, [productId]);
|
||||
return productRepository.get(productId);
|
||||
}
|
||||
|
||||
const skuQuery = `
|
||||
SELECT sku_id, size, price, stock, default_price, updated, created
|
||||
FROM product_sku
|
||||
WHERE product_no = $1
|
||||
ORDER BY created`;
|
||||
async getProductAudit(productId) {
|
||||
const query = `
|
||||
SELECT table_name, row_data, changed_fields
|
||||
FROM audit.logged_actions
|
||||
WHERE table_name = 'product'
|
||||
ORDER BY action_tstamp_stm DESC`;
|
||||
|
||||
const productSkus = await this.database.all(skuQuery, [productId]);
|
||||
return Promise.resolve({
|
||||
...product,
|
||||
variations: productSkus,
|
||||
});
|
||||
// TODO need to filter by product_id
|
||||
|
||||
return this.database.all(query, []);
|
||||
}
|
||||
|
||||
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`;
|
||||
SELECT product.*, image.url as image, variation_count, sum_stock
|
||||
FROM product
|
||||
LEFT 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
|
||||
LEFT JOIN image
|
||||
ON product.product_no = image.product_no
|
||||
WHERE default_image = TRUE
|
||||
ORDER BY product.updated DESC`;
|
||||
|
||||
return this.database.all(query);
|
||||
}
|
||||
@@ -69,20 +75,18 @@ ORDER BY created`;
|
||||
|
||||
createWarehouseProduct(skuId, stock) {
|
||||
const query = `
|
||||
INSERT INTO
|
||||
warehouse (product_sku_id, stock)
|
||||
VALUES ($1, $2)
|
||||
`;
|
||||
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
|
||||
`;
|
||||
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]);
|
||||
}
|
||||
@@ -91,9 +95,9 @@ ORDER BY created`;
|
||||
const sqlStatus = status ? "TRUE" : "FALSE";
|
||||
|
||||
const query = `
|
||||
UPDATE warehouse
|
||||
SET enabled = $1, updated = to_timestamp($2 / 1000.0)
|
||||
WHERE product_sku_id = $3`;
|
||||
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]);
|
||||
}
|
||||
|
||||
50
src/webserver/controllerResponses.ts
Normal file
50
src/webserver/controllerResponses.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import logger from "../logger";
|
||||
|
||||
export function genericFailedResponse(
|
||||
res,
|
||||
message,
|
||||
error,
|
||||
status = 500,
|
||||
attributes = {}
|
||||
) {
|
||||
logger.error(message, {
|
||||
controller_error: error?.message,
|
||||
controller_stack: error?.stack,
|
||||
status_code: error?.statusCode || status,
|
||||
...attributes,
|
||||
});
|
||||
|
||||
console.log("we have this:", {
|
||||
controller_error: error?.message,
|
||||
controller_stack: error?.stack,
|
||||
status_code: error?.statusCode || status,
|
||||
...attributes,
|
||||
});
|
||||
|
||||
return res.status(error?.statusCode || status).send({
|
||||
success: false,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
export function postenApiFailedResponse(
|
||||
res,
|
||||
message,
|
||||
error,
|
||||
status = 500,
|
||||
attributes = {}
|
||||
) {
|
||||
logger.error(message, {
|
||||
controller_error: error?.message,
|
||||
controller_stack: error?.stack,
|
||||
posten_error: error?.apiError,
|
||||
status_code: error?.statusCode || status,
|
||||
...attributes,
|
||||
});
|
||||
|
||||
return res.status(error?.statusCode || status).send({
|
||||
success: false,
|
||||
message: error?.message || message,
|
||||
reason: error?.reason,
|
||||
});
|
||||
}
|
||||
@@ -3,12 +3,11 @@ import OrderRepository from "../../order";
|
||||
import CustomerRepository from "../../customer";
|
||||
import ProductRepository from "../../product";
|
||||
import WarehouseRepository from "../../warehouse";
|
||||
import Cart from "../../cart/Cart";
|
||||
|
||||
import ICustomer from "../../interfaces/ICustomer";
|
||||
|
||||
import { validEmail } from "../../utils/formValidation";
|
||||
import ICart from "../../interfaces/ICart";
|
||||
import type ICart from "../../interfaces/ICart";
|
||||
|
||||
const orderRepository = new OrderRepository();
|
||||
const customerRepository = new CustomerRepository();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import logger from "../../logger";
|
||||
import ProductRepository from "../../product";
|
||||
const productRepository = new ProductRepository();
|
||||
import { ProductNotFoundError } from "../../errors/product";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
const productRepository = new ProductRepository();
|
||||
|
||||
async function add(req: Request, res: Response) {
|
||||
logger.info("Adding new product");
|
||||
try {
|
||||
@@ -25,28 +27,44 @@ async function add(req: Request, res: Response) {
|
||||
}
|
||||
}
|
||||
|
||||
function update(req: Request, res: Response) {
|
||||
async function update(req: Request, res: Response) {
|
||||
const { product_id } = req.params;
|
||||
logger.info("Updating product", { product_id });
|
||||
const { name, description, subtext, primary_color } = req.body;
|
||||
logger.info("Updating product", {
|
||||
product_id,
|
||||
name,
|
||||
description,
|
||||
subtext,
|
||||
primary_color,
|
||||
});
|
||||
|
||||
return productRepository
|
||||
.get(product_id)
|
||||
.then((product) => {
|
||||
logger.info("Updated product", { product, product_id });
|
||||
try {
|
||||
const product = await productRepository.get(product_id);
|
||||
if (!product) {
|
||||
throw new ProductNotFoundError();
|
||||
}
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
product: product,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Error while updating product", { error, product_id });
|
||||
res.statusCode = error.statusCode || 500;
|
||||
return res.send({
|
||||
success: false,
|
||||
message: error?.message || "Unexpected error while updating product",
|
||||
});
|
||||
await productRepository.updateProduct(
|
||||
product_id,
|
||||
name || product.name,
|
||||
description || product.description,
|
||||
subtext || product.subtext,
|
||||
primary_color || product.primary_color
|
||||
);
|
||||
const updatedProduct = await productRepository.get(product_id);
|
||||
logger.info("Updated product", { product: updatedProduct, product_id });
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
product: updatedProduct,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error while updating product", { error, product_id });
|
||||
res.status(error.statusCode || 500).send({
|
||||
success: false,
|
||||
message: error?.message || "Unexpected error while updating product",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getAll(req: Request, res: Response) {
|
||||
@@ -224,6 +242,99 @@ async function setSkuDefaultPrice(req: Request, res: Response) {
|
||||
}
|
||||
}
|
||||
|
||||
async function addImage(req: Request, res: Response) {
|
||||
const { product_id } = req.params;
|
||||
const { url } = req.body;
|
||||
logger.info("Adding new image", { product_id, url });
|
||||
|
||||
try {
|
||||
await productRepository.addImage(product_id, url);
|
||||
let images = await productRepository.getImages(product_id);
|
||||
console.log("found images::::", images);
|
||||
|
||||
if (!images?.find((image) => image.default_image === true)) {
|
||||
await productRepository.setDefaultImage(
|
||||
product_id,
|
||||
images[images.length - 1].image_id
|
||||
);
|
||||
|
||||
images[images.length - 1].default_image = true;
|
||||
}
|
||||
|
||||
logger.info("New images after add", { images, product_id });
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
product_id,
|
||||
images,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error adding image", { error, product_id });
|
||||
res.statusCode = error?.statusCode || 500;
|
||||
res.send({
|
||||
success: false,
|
||||
message: error?.message || "Unexpected error while adding new image",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function removeImage(req: Request, res: Response) {
|
||||
const { product_id, image_id } = req.params;
|
||||
|
||||
try {
|
||||
await productRepository.deleteImage(product_id, image_id);
|
||||
const images = await productRepository.getImages(product_id);
|
||||
logger.info("New images after delete", { images, product_id, image_id });
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
images,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error deleting image", { product_id, image_id, error });
|
||||
res.statusCode = error?.statusCode || 500;
|
||||
|
||||
res.send({
|
||||
success: false,
|
||||
message: error?.message || "Unexpected error while deleting image",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function setDefaultImage(req: Request, res: Response) {
|
||||
const { product_id, image_id } = req.params;
|
||||
const { url } = req.body;
|
||||
logger.info("Updating new default image", { product_id, image_id });
|
||||
|
||||
try {
|
||||
await productRepository.setDefaultImage(product_id, image_id);
|
||||
let images = await productRepository.getImages(product_id);
|
||||
logger.info("New images after update default image", {
|
||||
images,
|
||||
product_id,
|
||||
image_id,
|
||||
});
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
product_id,
|
||||
images,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Unexpected error while setting default image", {
|
||||
error,
|
||||
product_id,
|
||||
image_id,
|
||||
});
|
||||
res.statusCode = error?.statusCode || 500;
|
||||
res.send({
|
||||
success: false,
|
||||
message:
|
||||
error?.message || "Unexpected error while adding setting default image",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
add,
|
||||
update,
|
||||
@@ -234,4 +345,7 @@ export default {
|
||||
updateSku,
|
||||
deleteSku,
|
||||
setSkuDefaultPrice,
|
||||
addImage,
|
||||
removeImage,
|
||||
setDefaultImage,
|
||||
};
|
||||
|
||||
242
src/webserver/controllers/shipmentController.ts
Normal file
242
src/webserver/controllers/shipmentController.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import logger from "../../logger";
|
||||
import ShippingRepository from "../../shipping";
|
||||
import {
|
||||
genericFailedResponse,
|
||||
postenApiFailedResponse,
|
||||
} from "../controllerResponses";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
const shippingRepository = new ShippingRepository();
|
||||
|
||||
async function create(req: Request, res: Response) {
|
||||
const { order_id } = req.params;
|
||||
logger.info("Creating shipment for order", { order_id });
|
||||
|
||||
try {
|
||||
const { shipment_id } = await shippingRepository.create(order_id);
|
||||
logger.info("Shipment created", { shipment_id, order_id });
|
||||
|
||||
const shipment = await shippingRepository.get(shipment_id);
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
message: "Successfully created shipment on order",
|
||||
shipment,
|
||||
});
|
||||
} catch (error) {
|
||||
genericFailedResponse(
|
||||
res,
|
||||
"Unexpected error creating shipment on order",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function update(req: Request, res: Response) {
|
||||
const { shipment_id } = req.params;
|
||||
const { courier_id, tracking_code, tracking_link } = req.body;
|
||||
logger.info("Updating shipment", {
|
||||
shipment_id,
|
||||
courier_id,
|
||||
tracking_code,
|
||||
tracking_link,
|
||||
});
|
||||
|
||||
try {
|
||||
const shipment = await shippingRepository.get(shipment_id);
|
||||
if (!shipment) {
|
||||
return genericFailedResponse(
|
||||
res,
|
||||
"Shipment not found, unable to update",
|
||||
null,
|
||||
404,
|
||||
{ shipment_id }
|
||||
);
|
||||
}
|
||||
|
||||
const courier = await shippingRepository.getCourier(courier_id);
|
||||
if (!courier) {
|
||||
return genericFailedResponse(
|
||||
res,
|
||||
"Unable to update shipment, selected courier not found",
|
||||
null,
|
||||
404,
|
||||
{ shipment_id }
|
||||
);
|
||||
}
|
||||
|
||||
await shippingRepository.update(
|
||||
shipment_id,
|
||||
courier_id,
|
||||
tracking_code,
|
||||
tracking_link
|
||||
);
|
||||
const newShipment = await shippingRepository.get(shipment_id);
|
||||
|
||||
return res.send({
|
||||
success: true,
|
||||
message: "Successfully updated shipment",
|
||||
shipment: newShipment,
|
||||
});
|
||||
} catch (error) {
|
||||
genericFailedResponse(
|
||||
res,
|
||||
"Unexpected error while updating shipment",
|
||||
error,
|
||||
null,
|
||||
{ shipment_id }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function get(req, res) {
|
||||
const { shipment_id } = req.params;
|
||||
logger.info("Getting shipment by id", { shipment_id });
|
||||
|
||||
// get a shipment
|
||||
let shipment = null;
|
||||
try {
|
||||
shipment = await shippingRepository.get(shipment_id);
|
||||
logger.info("Found shipment", { shipment });
|
||||
|
||||
if (shipment === undefined) {
|
||||
return genericFailedResponse(
|
||||
res,
|
||||
`No shipment with id ${shipment_id} found`,
|
||||
null,
|
||||
404,
|
||||
{ shipment_id }
|
||||
);
|
||||
}
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
shipment,
|
||||
});
|
||||
} catch (error) {
|
||||
genericFailedResponse(
|
||||
res,
|
||||
`Unexpected error while looking for shipment`,
|
||||
error,
|
||||
500,
|
||||
{ shipment_id }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function track(req, res) {
|
||||
const { shipment_id } = req.params;
|
||||
logger.info("Tracking shipment by id", { shipment_id });
|
||||
|
||||
if (isNaN(Number(shipment_id))) {
|
||||
return genericFailedResponse(
|
||||
res,
|
||||
"Shipment id must be a number",
|
||||
null,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// get a shipment
|
||||
let shipment = null;
|
||||
let trackedShipment = null;
|
||||
try {
|
||||
shipment = await shippingRepository.get(shipment_id);
|
||||
|
||||
if (!shipment?.tracking_code) {
|
||||
return genericFailedResponse(
|
||||
res,
|
||||
"No tracking code registered on shipment",
|
||||
null,
|
||||
404,
|
||||
{ shipment_id }
|
||||
);
|
||||
}
|
||||
|
||||
logger.info("Found tracking code", {
|
||||
tracking_code: shipment.tracking_code,
|
||||
});
|
||||
try {
|
||||
trackedShipment = await shippingRepository.track(shipment?.tracking_code);
|
||||
logger.info("Found tracked shipment", {
|
||||
tracked_shipment: trackedShipment,
|
||||
});
|
||||
} catch (error) {
|
||||
return postenApiFailedResponse(
|
||||
res,
|
||||
"Unexpected error from posten API while tracking shipment",
|
||||
error,
|
||||
500,
|
||||
{ shipment_id }
|
||||
);
|
||||
}
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
shipment: trackedShipment,
|
||||
});
|
||||
} catch (error) {
|
||||
genericFailedResponse(
|
||||
res,
|
||||
"Unexpected error while tracking shipment",
|
||||
error,
|
||||
500,
|
||||
{ shipment_id }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function allCouriers(req: Request, res: Response) {
|
||||
return shippingRepository.getAllCouriers().then((couriers) =>
|
||||
res.send({
|
||||
success: true,
|
||||
message: "All registered shipment couriers",
|
||||
couriers,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function getCourier(req: Request, res: Response) {
|
||||
const { courier_id } = req.params;
|
||||
|
||||
if (isNaN(Number(courier_id))) {
|
||||
return genericFailedResponse(res, "Courier id must be a number", null, 400);
|
||||
}
|
||||
|
||||
return shippingRepository
|
||||
.getCourier(courier_id)
|
||||
.then((courier) => {
|
||||
if (courier === undefined) {
|
||||
return genericFailedResponse(
|
||||
res,
|
||||
`No courier with that id found`,
|
||||
null,
|
||||
404,
|
||||
{ courier_id }
|
||||
);
|
||||
}
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
courier,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
genericFailedResponse(
|
||||
res,
|
||||
"Unexpected error happend while trying to get courier by id",
|
||||
error,
|
||||
500,
|
||||
{ courier_id }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
create,
|
||||
update,
|
||||
get,
|
||||
track,
|
||||
allCouriers,
|
||||
getCourier,
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import StripeApi from "../../stripe/stripeApi";
|
||||
import StripeRepository from "../../stripe/stripeRepository";
|
||||
import OrderRepository from "../../order";
|
||||
import CustomerRepository from "../../customer";
|
||||
import EmailRepository from "../../email";
|
||||
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import type { IOrder, ILineItem } from "../../interfaces/IOrder";
|
||||
@@ -18,13 +19,14 @@ const stripeApi = new StripeApi(stripePublicKey, stripeSecretKey);
|
||||
const stripeRepository = new StripeRepository();
|
||||
const orderRepository = new OrderRepository();
|
||||
const customerRepository = new CustomerRepository();
|
||||
const emailRepository = new EmailRepository();
|
||||
|
||||
async function create(req, res) {
|
||||
const clientId = req?.planetId;
|
||||
const planet_id = req?.planet_id;
|
||||
const { order_id, customer_no } = req.body;
|
||||
|
||||
logger.info("Creating stripe payment intent", {
|
||||
client_id: clientId,
|
||||
planet_id,
|
||||
order_id,
|
||||
customer_no,
|
||||
});
|
||||
@@ -51,10 +53,10 @@ async function create(req, res) {
|
||||
);
|
||||
|
||||
stripeRepository
|
||||
.createPayment(clientId, sum, order_id, customer)
|
||||
.createPayment(planet_id, sum, order_id, customer)
|
||||
.then((clientSecret) => {
|
||||
logger.info("New stripe payment", {
|
||||
client_id: clientId,
|
||||
planet_id,
|
||||
client_secret: clientSecret,
|
||||
});
|
||||
|
||||
@@ -67,7 +69,7 @@ async function create(req, res) {
|
||||
res.statusCode = error?.statusCode || 500;
|
||||
logger.error("Error creating stripe payment intent", {
|
||||
error,
|
||||
client_id: clientId,
|
||||
planet_id,
|
||||
customer_no,
|
||||
order_id,
|
||||
});
|
||||
@@ -82,7 +84,6 @@ async function create(req, res) {
|
||||
}
|
||||
|
||||
async function updatePayment(req: Request, res: Response) {
|
||||
console.log("STRIPE WEBHOOK body:", req.body);
|
||||
const { type, data } = req.body;
|
||||
const { object } = data;
|
||||
const { orderId } = object?.metadata;
|
||||
@@ -93,28 +94,36 @@ async function updatePayment(req: Request, res: Response) {
|
||||
}
|
||||
|
||||
if (!orderId) {
|
||||
console.log("no order_id found in webhook, nothing to do");
|
||||
logger.warning("no order_id found in webhook from stripe, nothing to do", {
|
||||
stripe_webhook_type: type,
|
||||
[type]: object,
|
||||
});
|
||||
return res.status(200).send("ok");
|
||||
}
|
||||
|
||||
if (type === "payment_intent.created") {
|
||||
console.log("handle payment intent created... doing nothing");
|
||||
} else if (type === "payment_intent.succeeded") {
|
||||
console.log("handle payment succeeded", object);
|
||||
await stripeRepository.updatePaymentIntent(object);
|
||||
orderRepository.confirmOrder(orderId);
|
||||
await orderRepository.confirmOrder(orderId);
|
||||
const { customer, lineItems } = await orderRepository.getOrderDetailed(
|
||||
orderId
|
||||
);
|
||||
await emailRepository.sendConfirmation(orderId, customer, lineItems);
|
||||
} else if (type === "payment_intent.payment_failed") {
|
||||
console.log("handle payment failed", object);
|
||||
await stripeRepository.updatePaymentIntent(object);
|
||||
orderRepository.cancelOrder(orderId);
|
||||
await orderRepository.cancelOrder(orderId);
|
||||
} else if (type === "charge.succeeded") {
|
||||
console.log("handle charge succeeded", object);
|
||||
await stripeRepository.updatePaymentCharge(object);
|
||||
} else if (type === "charge.refunded") {
|
||||
console.log("handle charge refunded", object);
|
||||
await stripeRepository.updatePaymentCharge(object);
|
||||
await orderRepository.refundOrder(orderId);
|
||||
} else {
|
||||
logger.warning("unhandled webhook from stripe", {
|
||||
stripe_webhook_type: type,
|
||||
[type]: object,
|
||||
});
|
||||
console.log(`webhook for ${type}, not setup yet`);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,15 +30,15 @@ function getAll(req: Request, res: Response) {
|
||||
}
|
||||
|
||||
function getProduct(req: Request, res: Response) {
|
||||
const { productId } = req.params;
|
||||
logger.info("Fetching warehouse product", { product_id: productId });
|
||||
const { product_id } = req.params;
|
||||
logger.info("Fetching warehouse product", { product_id });
|
||||
|
||||
return warehouseRepository
|
||||
.getProduct(productId)
|
||||
.getProduct(product_id)
|
||||
.then((product) => {
|
||||
logger.info("Found warehouse product", {
|
||||
product,
|
||||
product_id: productId,
|
||||
product_id,
|
||||
});
|
||||
|
||||
res.send({
|
||||
@@ -49,7 +49,7 @@ function getProduct(req: Request, res: Response) {
|
||||
.catch((error) => {
|
||||
logger.error("Error fetching warehouse product:", {
|
||||
error,
|
||||
product_id: productId,
|
||||
product_id,
|
||||
});
|
||||
res.statusCode = error.statusCode || 500;
|
||||
|
||||
@@ -57,9 +57,31 @@ function getProduct(req: Request, res: Response) {
|
||||
success: false,
|
||||
message:
|
||||
error?.message ||
|
||||
`Unexpected error while fetching product with id: ${productId}`,
|
||||
`Unexpected error while fetching product with id: ${product_id}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default { getAll, getProduct };
|
||||
function getProductAudit(req: Request, res: Response) {
|
||||
const { product_id } = req.params;
|
||||
logger.info("Fetching audit logs for product", { product_id });
|
||||
|
||||
return warehouseRepository
|
||||
.getProductAudit(product_id)
|
||||
.then((auditLogs) =>
|
||||
res.send({
|
||||
success: true,
|
||||
logs: auditLogs,
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error("Unexpected error while fetching product audit log", error);
|
||||
|
||||
res.status(error?.statusCode || 500).send({
|
||||
success: false,
|
||||
message: "Unexpected error while fetching product audit log",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default { getAll, getProduct, getProductAudit };
|
||||
|
||||
@@ -3,14 +3,14 @@ import generateUUID from "../../utils/generateUUID";
|
||||
import httpContext from "express-http-context";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
|
||||
const cookieClientKey = "planetId";
|
||||
const cookieClientKey = "planet_id";
|
||||
const cookieOptions = {
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
};
|
||||
|
||||
function setClientIdCookieHeader(res: Response, value: string) {
|
||||
const setCookie = cookie.serialize("planetId", value, cookieOptions);
|
||||
function setplanet_idCookieHeader(res: Response, value: string) {
|
||||
const setCookie = cookie.serialize("planet_id", value, cookieOptions);
|
||||
return res.setHeader("Set-Cookie", setCookie);
|
||||
}
|
||||
|
||||
@@ -20,16 +20,16 @@ const getOrSetCookieForClient = (
|
||||
next: NextFunction
|
||||
) => {
|
||||
const cookies = cookie.parse(req.headers.cookie || "");
|
||||
const planetId = cookies[cookieClientKey];
|
||||
let planet_id = cookies[cookieClientKey];
|
||||
|
||||
if (planetId) {
|
||||
req.planetId = planetId;
|
||||
httpContext.set("planetId", planetId);
|
||||
if (planet_id) {
|
||||
req.planet_id = planet_id;
|
||||
httpContext.set("planet_id", planet_id);
|
||||
return next();
|
||||
}
|
||||
|
||||
const clientId = generateUUID();
|
||||
setClientIdCookieHeader(res, clientId);
|
||||
planet_id = generateUUID();
|
||||
setplanet_idCookieHeader(res, planet_id);
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ const mapFeaturePolicyToString = (features) => {
|
||||
|
||||
const setupHeaders = (req: Request, res: Response, next: NextFunction) => {
|
||||
res.set("Access-Control-Allow-Headers", "Content-Type");
|
||||
res.set("Access-Control-Allow-Methods", "POST, PATCH, DELETE");
|
||||
res.set("Access-Control-Allow-Methods", "POST, PATCH, DELETE, PUT");
|
||||
|
||||
// Security
|
||||
res.set("X-Content-Type-Options", "nosniff");
|
||||
|
||||
@@ -11,6 +11,7 @@ import ProductController from "./controllers/productController";
|
||||
import WarehouseController from "./controllers/warehouseController";
|
||||
import StripePaymentController from "./controllers/stripePaymentController";
|
||||
import LoginController from "./controllers/loginController";
|
||||
import ShipmentController from "./controllers/shipmentController";
|
||||
|
||||
// middleware
|
||||
import httpContext from "express-http-context";
|
||||
@@ -35,31 +36,50 @@ router.post("/logout", LoginController.logout);
|
||||
router.get("/products", ProductController.getAll);
|
||||
router.post("/product", ProductController.add);
|
||||
router.get("/product/:product_id", ProductController.getById);
|
||||
router.put("/product/:product_id", ProductController.update);
|
||||
router.post("/product/:product_id/sku", ProductController.addSku);
|
||||
router.patch("/product/:product_id/sku/:sku_id", ProductController.updateSku);
|
||||
router.put("/product/:product_id/sku/:sku_id", ProductController.updateSku);
|
||||
router.delete("/product/:product_id/sku/:sku_id", ProductController.deleteSku);
|
||||
router.post(
|
||||
"/product/:product_id/sku/:sku_id/default_price",
|
||||
"/product/:product_id/sku/:sku_id/default",
|
||||
ProductController.setSkuDefaultPrice
|
||||
);
|
||||
router.post("/product/:product_id/image", ProductController.addImage);
|
||||
router.delete(
|
||||
"/product/:product_id/image/:image_id",
|
||||
ProductController.removeImage
|
||||
);
|
||||
router.post(
|
||||
"/product/:product_id/image/:image_id/default",
|
||||
ProductController.setDefaultImage
|
||||
);
|
||||
|
||||
router.get("/orders", OrderController.getAll);
|
||||
router.post("/order", OrderController.createOrder);
|
||||
router.get("/order/:order_id", OrderController.get);
|
||||
router.get("/order/:order_id/status", OrderController.getOrderStatus);
|
||||
// router.post("/order/:order_id/confirm", adminMiddleware, OrderController.confirmOrder);
|
||||
|
||||
router.get("/shipment/couriers", ShipmentController.allCouriers);
|
||||
router.get("/shipment/courier/:courier_id", ShipmentController.getCourier);
|
||||
router.get("/shipment/:shipment_id", ShipmentController.get);
|
||||
router.get("/shipment/:shipment_id/track", ShipmentController.track);
|
||||
router.post("/shipment/:order_id", ShipmentController.create);
|
||||
router.put("/shipment/:shipment_id", ShipmentController.update);
|
||||
|
||||
router.get("/warehouse", WarehouseController.getAll);
|
||||
router.get("/warehouse/:productId", WarehouseController.getProduct);
|
||||
// router.get("/api/order/:id", OrderController.getOrderById);
|
||||
// router.post("/api/order/:id/cancel", OrderController.cancelOrder);
|
||||
// router.post("/api/order/:id/extend", OrderController.extendOrder);
|
||||
router.get("/warehouse/:product_id", WarehouseController.getProduct);
|
||||
router.get("/warehouse/:product_id/audit", WarehouseController.getProductAudit);
|
||||
// router.get("/order/:id", OrderController.getOrderById);
|
||||
// router.post("/order/:id/cancel", OrderController.cancelOrder);
|
||||
// router.post("/order/:id/extend", OrderController.extendOrder);
|
||||
|
||||
router.post("/payment/stripe", StripePaymentController.create);
|
||||
router.post("/webhook/stripe", StripePaymentController.updatePayment);
|
||||
|
||||
router.get("/", (req, res) => res.send("hello"));
|
||||
router.get("/", (req, res) => res.send("hi"));
|
||||
|
||||
app.use("/api", router);
|
||||
app.use("/api/v1", router);
|
||||
|
||||
const server = createServer(app);
|
||||
server.listen(port, () => logger.info(`Server started, listening at :${port}`));
|
||||
|
||||
@@ -27,13 +27,13 @@ function setupCartWebsocketServer(server) {
|
||||
|
||||
wss.on("connection", (ws: Websocket, req: Request) => {
|
||||
const sessionId = generateUUID();
|
||||
const clientId =
|
||||
getCookieValue(req.headers.cookie, "planetId") ||
|
||||
getHeaderValue(req.url, "planetId");
|
||||
const planet_id =
|
||||
getCookieValue(req.headers.cookie, "planet_id") ||
|
||||
getHeaderValue(req.url, "planet_id");
|
||||
|
||||
if (clientId === null) return;
|
||||
if (planet_id === null) return;
|
||||
|
||||
const wsCart = new WSCart(ws, clientId);
|
||||
const wsCart = new WSCart(ws, planet_id);
|
||||
wsCart.cartSession = cartSession;
|
||||
cartSession.add(sessionId, wsCart);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user