diff --git a/README.md b/README.md index 1fa4ed9..fe17397 100644 Binary files a/README.md and b/README.md differ diff --git a/client/public/basket.mjs b/client/public/basket.mjs index c7b3fbc..b8dbf5f 100644 --- a/client/public/basket.mjs +++ b/client/public/basket.mjs @@ -1,5 +1,4 @@ // Basket is stored locally only and is not persisted to the server. -// It is used to store the current basket and is used to calculate the total price of the basket. // It is also used to store the current user's basket. // The structure of the basket is in local storage and is as follows: // { @@ -13,9 +12,6 @@ // }, // } -let BasketPriceRelavant = false; -let KnownBasketPrice = 0; - // TODO: Does the localstorage have a problem with mutual exclusion? // TODO: Should the basket be persisted to the server? export function GetBasketItems() { @@ -25,6 +21,10 @@ export function GetBasketItems() { return JSON.parse(localStorage.getItem('basket')).items; } +export function ClearBasket() { + localStorage.removeItem('basket'); +} + export function AddProductToBasket(product, type, amount, brickModifier = 'none') { if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { localStorage.setItem('basket', JSON.stringify({ diff --git a/client/public/components/basket.mjs b/client/public/components/basket.mjs index cec5d10..340b337 100644 --- a/client/public/components/basket.mjs +++ b/client/public/components/basket.mjs @@ -204,6 +204,13 @@ class Basket extends Component { outline: 2px solid #222; color: #222; } + + .button-disabled { + background-color: #ccc; + color: #222; + cursor: not-allowed; + pointer-events: all !important; + } `, }; } @@ -214,6 +221,17 @@ class Basket extends Component { if (basketSubtotal) { basketSubtotal.innerText = parseFloat(subtotal).toFixed(2); } + if (parseFloat(basketSubtotal.innerText) === 0.0) { + // gray out checkout button + const checkoutButton = this.root.querySelector('.checkout-button'); + checkoutButton.classList.add('button-disabled'); + checkoutButton.disabled = true; + } else { + // un-gray checkout button + const checkoutButton = this.root.querySelector('.checkout-button'); + checkoutButton.classList.remove('button-disabled'); + checkoutButton.disabled = false; + } } OnRender() { diff --git a/client/public/components/checkout.mjs b/client/public/components/checkout.mjs index 5b7f096..d048c52 100644 --- a/client/public/components/checkout.mjs +++ b/client/public/components/checkout.mjs @@ -1,5 +1,6 @@ import { RegisterComponent, Component, SideLoad } from './components.mjs'; import * as Basket from '../basket.mjs'; +import * as Auth from '../auth.mjs'; class Checkout extends Component { static __IDENTIFY() { return 'checkout'; } @@ -315,6 +316,59 @@ class Checkout extends Component { codeApplied: offerText, }); }); + + // submit + this.root.querySelector('.checkout-place-order-button').addEventListener('click', async () => { + // BLACK BOX - PAYMENT GATEWAY WILL BE CALLED HERE + // BLACK BOX - PAYMENT GATEWAY WILL BE CALLED HERE + + // get everything needed to send to the server + const basket = await Basket.GetBasketItems(); + const discountCode = this.state.codeApplied || ''; + if (basket.length === 0) { + alert('How did you get here?'); + } + + let req; + if (localStorage.loggedIn === 'true') { + // send to server + req = await fetch('/api/auth/order', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${await Auth.GetToken()}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + basket, + discountCode, + }), + }).then((res) => res.json()); + } else { + // send to server NO AUTH + req = await fetch('/api/order', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + basket, + discountCode, + }), + }).then((res) => res.json()); + } + + if (req.error) { + alert(req.error); + return; + } + + // clear basket + await Basket.ClearBasket(); + + // redirect to receipt + window.location.href = `/order/${req.data.receipt_id}`; + // we're done ! + }); } } diff --git a/db/schema.sql b/db/schema.sql index 48c8a6d..83cb8c9 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -92,7 +92,7 @@ CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS order_log ( id VARCHAR (50) NOT NULL PRIMARY KEY, - user_id VARCHAR (50) NOT NULL, -- 0 if guest + user_id VARCHAR (50), -- null if guest offer_code SERIAL, subtotal_paid DECIMAL NOT NULL, discount DECIMAL, diff --git a/docs/API.md b/docs/API.md index df19757..a314694 100644 --- a/docs/API.md +++ b/docs/API.md @@ -19,7 +19,7 @@ automatically every request | GET | /api/cdn/:id | | ❌ | | | GET | /api/basket/price/ | | ❌ | | | GET | /api/discount/ | offer code | ❌ | | -| POST | /api/order/ | | ❌ | | +| POST | /api/order/ | | ❌ | IF user is authenticated, auth/bearer will be sent and done manually without middleware | | GET | /api/auth/order/:id | | ❌ | Security By Obscurity | | GET | /api/auth/login/ | | ✔️ | | | GET | /api/auth/orders/ | | ✔️ | | @@ -33,20 +33,18 @@ a subset for product listing pages For all endpoints that query, the following parameters are supported: +q: string to search for (fuzzy) + tags: tags to include in search +type: type of entity to return (set / brick) + total: total results (not pageified) per_page: results to include per page page: page requested -q: string to search for (fuzzy) - -brick: brick to search for (absolute type, fuzzy string) - -set: brick to search for (absolute, fuzzy string) - ## Response Structure ```js @@ -64,7 +62,7 @@ set: brick to search for (absolute, fuzzy string) ```js { error: "Error doing x", - long: "y needs to be z", + long: "y needs to be z", // not always present } ``` diff --git a/src/controllers/brick-controller.js b/src/controllers/brick-controller.js index 4b788fb..f25df07 100644 --- a/src/controllers/brick-controller.js +++ b/src/controllers/brick-controller.js @@ -4,6 +4,10 @@ const Logger = require('../logger.js'); const PgFormat = require('pg-format'); +// C + +// R + async function Search(fuzzyStrings) { await Database.Query('BEGIN TRANSACTION;'); const dbres = await Database.Query(PgFormat(` @@ -209,6 +213,10 @@ async function GetBrick(brickId) { return brick; } +// U + +// D + module.exports = { Search, SumPrices, diff --git a/src/controllers/order-controller.js b/src/controllers/order-controller.js new file mode 100644 index 0000000..de728cb --- /dev/null +++ b/src/controllers/order-controller.js @@ -0,0 +1,14 @@ +const Database = require('../database/database.js'); +const Logger = require('../logger.js'); + +// C + +// R + +// U + +// D + +module.exports = { + +}; diff --git a/src/controllers/set-controller.js b/src/controllers/set-controller.js index 6fbc263..63df1a9 100644 --- a/src/controllers/set-controller.js +++ b/src/controllers/set-controller.js @@ -4,6 +4,9 @@ const Logger = require('../logger.js'); const PgFormat = require('pg-format'); +// C + +// R async function Search(fuzzyStrings) { await Database.Query('BEGIN TRANSACTION;'); const dbres = await Database.Query(PgFormat(` @@ -224,6 +227,10 @@ async function GetSets(page, resPerPage) { return { total, sets }; } +// U + +// D + module.exports = { Search, SumPrices, diff --git a/src/routes/api.js b/src/routes/api.js index 3ac84d6..8fbcf3c 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -7,6 +7,7 @@ const Bricks = require('./bricks-router.js'); const Sets = require('./sets-router.js'); const Query = require('./query-router.js'); const Auth0 = require('./auth0-router.js'); +const Order = require('./order-router.js'); // CRUD is implemented where it makes sense. function Init() { @@ -23,13 +24,12 @@ function Init() { Server.App.post('/api/basket/price/', Helpers.CalculateBasketPrice); Server.App.get('/api/discount/', Helpers.DiscountCode); - Server.App.post('/api/order'); + Server.App.post('/api/order/', Order.ProcessNew); Server.App.get('/api/order:id'); Server.App.get('/api/auth/login/', Auth0.JWTMiddleware, Auth0.Login); - + Server.App.post('/api/auth/order/', Auth0.JWTMiddleware, Order.ProcessNew); Server.App.get('/api/auth/orders/'); - Server.App.get('/api/auth/order/:id'); Logger.Module('API', 'API Routes Initialized'); } diff --git a/src/routes/auth0-router.js b/src/routes/auth0-router.js index b0b2db3..c5eadcd 100644 --- a/src/routes/auth0-router.js +++ b/src/routes/auth0-router.js @@ -91,5 +91,6 @@ async function Login(req, res) { module.exports = { JWTMiddleware, + Auth0GetUser, Login, }; diff --git a/src/routes/helpers.js b/src/routes/helpers.js index 16bc9fc..afeaedb 100644 --- a/src/routes/helpers.js +++ b/src/routes/helpers.js @@ -7,7 +7,6 @@ const Logger = require('../logger.js'); const Delay = (ms) => new Promise((r) => setTimeout(r, ms)); const EndDate = new Date('2022-06-10T00:00:00.000Z'); - function Special(req, res) { res.send({ data: { @@ -58,8 +57,6 @@ async function CalculateBasketPrice(req, res) { } } - console.log(newBrickList); - let setSubtotal = setList.length > 0 ? await SetController.SumPrices(setList, setQuantities) : 0; @@ -81,8 +78,8 @@ async function CalculateBasketPrice(req, res) { async function DiscountCode(req, res) { - // // artificial delay to simulate a lots of maths - // await Delay(1000); + // artificial delay to simulate a lots of maths + await Delay(500); if (!req.query.code) { res.send({ diff --git a/src/routes/order-router.js b/src/routes/order-router.js new file mode 100644 index 0000000..b994652 --- /dev/null +++ b/src/routes/order-router.js @@ -0,0 +1,141 @@ +const ControllerMaster = require('../controllers/controller-master.js'); +const MiscController = require('../controllers/misc-controller.js'); +const BrickController = require('../controllers/brick-controller.js'); +const SetController = require('../controllers/set-controller.js'); +const AuthRouter = require('./auth0-router.js'); + +async function ProcessNew(req, res) { + console.log(req.body); + + // as it's optional auth, 0 is guest + let userID = null; + if (req.auth) { + const user = await AuthRouter.Auth0GetUser(req); + if (user) { + userID = user.sub.split('|')[1]; + } + } + + console.log(userID); + + // validate the request + if (!req.body.basket) { + return res.send({ + error: 'No basket in request', + }); + } + + const basket = req.body.basket; + const discountCode = req.body.discountCode || ''; + + // validate the basket + + // are all of the items in the basket valid? + // bricks, we check if the modifier is valid too + for (const [item, value] of Object.entries(basket)) { + if (value.type === 'brick') { + const brick = await BrickController.GetBrick(item.split('~')[0]); + if (brick.error) { + return res.send({ + error: 'Invalid brick in basket', + }); + } + + const modifier = item.split('~')[1]; + let modifierFound = false; + for (const colour of brick.colours) { + if (colour.id === parseInt(modifier)) { + modifierFound = true; + break; + } + } + + if (!modifierFound) { + return res.send({ + error: 'Invalid modifier in basket', + }); + } + } + + if (value.type === 'set') { + const set = await SetController.GetSet(item); + if (set.error) { + return res.send({ + error: 'Invalid set in basket', + }); + } + } + } + + // awesome, basket is valid + // now we need to calculate the subtotal + + // TODO: consolidate this code with the code in the helpers.js file + // as this is not maintainable + const setList = []; + const setQuantities = []; + const brickList = []; + const brickQuantities = []; + + for (const [item, value] of Object.entries(basket)) { + if (value.type === 'set') { + setList.push(item.split('~')[0]); + setQuantities.push(value.quantity); + } + if (value.type === 'brick') { + brickList.push(item.split('~')[0]); + brickQuantities.push(value.quantity); + } + } + + const newBrickList = []; + const newBrickQuantities = []; + for (let i = 0; i < brickList.length; i++) { + if (!newBrickList.includes(brickList[i])) { + newBrickList.push(brickList[i]); + newBrickQuantities.push(brickQuantities[i]); + } else { + newBrickQuantities[newBrickList.indexOf(brickList[i])] += brickQuantities[i]; + } + } + + let setSubtotal = setList.length > 0 + ? await SetController.SumPrices(setList, setQuantities) + : 0; + let brickSubtotal = brickList.length > 0 + ? await BrickController.SumPrices(newBrickList, newBrickQuantities) + : 0; + + if (setSubtotal.error) setSubtotal = 0; + if (brickSubtotal.error) brickSubtotal = 0; + + const basketSubtotal = setSubtotal + brickSubtotal; + + // now we need to calculate the discount (if applicable) + // again, this could do with some consolidation + + let discount = 0; + if (discountCode !== '') { + const sanatisedCode = ControllerMaster.SanatiseQuery(req.query.code); + + const discount = await MiscController.GetDiscount(sanatisedCode); + + if (discount.error) { + return res.send({ + error: discount.error, + }); + } + + if (discount.end_date < new Date()) { + return res.send({ + error: 'Discount code expired', + }); + } + } + + +} + +module.exports = { + ProcessNew, +};