calculations

Former-commit-id: 0281ecdc282da7e83a0bff152f50bc86148cf446
This commit is contained in:
Ben
2022-04-28 23:28:39 +01:00
parent 1822f74b87
commit d08904a184
13 changed files with 259 additions and 21 deletions

BIN
README.md

Binary file not shown.

View File

@@ -1,5 +1,4 @@
// Basket is stored locally only and is not persisted to the server. // 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. // It is also used to store the current user's basket.
// The structure of the basket is in local storage and is as follows: // 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: Does the localstorage have a problem with mutual exclusion?
// TODO: Should the basket be persisted to the server? // TODO: Should the basket be persisted to the server?
export function GetBasketItems() { export function GetBasketItems() {
@@ -25,6 +21,10 @@ export function GetBasketItems() {
return JSON.parse(localStorage.getItem('basket')).items; return JSON.parse(localStorage.getItem('basket')).items;
} }
export function ClearBasket() {
localStorage.removeItem('basket');
}
export function AddProductToBasket(product, type, amount, brickModifier = 'none') { export function AddProductToBasket(product, type, amount, brickModifier = 'none') {
if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) {
localStorage.setItem('basket', JSON.stringify({ localStorage.setItem('basket', JSON.stringify({

View File

@@ -204,6 +204,13 @@ class Basket extends Component {
outline: 2px solid #222; outline: 2px solid #222;
color: #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) { if (basketSubtotal) {
basketSubtotal.innerText = parseFloat(subtotal).toFixed(2); 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() { OnRender() {

View File

@@ -1,5 +1,6 @@
import { RegisterComponent, Component, SideLoad } from './components.mjs'; import { RegisterComponent, Component, SideLoad } from './components.mjs';
import * as Basket from '../basket.mjs'; import * as Basket from '../basket.mjs';
import * as Auth from '../auth.mjs';
class Checkout extends Component { class Checkout extends Component {
static __IDENTIFY() { return 'checkout'; } static __IDENTIFY() { return 'checkout'; }
@@ -315,6 +316,59 @@ class Checkout extends Component {
codeApplied: offerText, 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 !
});
} }
} }

View File

@@ -92,7 +92,7 @@ CREATE TABLE IF NOT EXISTS users (
CREATE TABLE IF NOT EXISTS order_log ( CREATE TABLE IF NOT EXISTS order_log (
id VARCHAR (50) NOT NULL PRIMARY KEY, 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, offer_code SERIAL,
subtotal_paid DECIMAL NOT NULL, subtotal_paid DECIMAL NOT NULL,
discount DECIMAL, discount DECIMAL,

View File

@@ -19,7 +19,7 @@ automatically every request
| GET | /api/cdn/:id | | ❌ | | | GET | /api/cdn/:id | | ❌ | |
| GET | /api/basket/price/ | | ❌ | | | GET | /api/basket/price/ | | ❌ | |
| GET | /api/discount/ | offer code | ❌ | | | 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/order/:id | | ❌ | Security By Obscurity |
| GET | /api/auth/login/ | | ✔️ | | | GET | /api/auth/login/ | | ✔️ | |
| GET | /api/auth/orders/ | | ✔️ | | | GET | /api/auth/orders/ | | ✔️ | |
@@ -33,20 +33,18 @@ a subset for product listing pages
For all endpoints that query, the following parameters are supported: For all endpoints that query, the following parameters are supported:
q: string to search for (fuzzy)
tags: tags to include in search tags: tags to include in search
type: type of entity to return (set / brick)
total: total results (not pageified) total: total results (not pageified)
per_page: results to include per page per_page: results to include per page
page: page requested 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 ## Response Structure
```js ```js
@@ -64,7 +62,7 @@ set: brick to search for (absolute, fuzzy string)
```js ```js
{ {
error: "Error doing x", error: "Error doing x",
long: "y needs to be z", long: "y needs to be z", // not always present
} }
``` ```

View File

@@ -4,6 +4,10 @@ const Logger = require('../logger.js');
const PgFormat = require('pg-format'); const PgFormat = require('pg-format');
// C
// R
async function Search(fuzzyStrings) { async function Search(fuzzyStrings) {
await Database.Query('BEGIN TRANSACTION;'); await Database.Query('BEGIN TRANSACTION;');
const dbres = await Database.Query(PgFormat(` const dbres = await Database.Query(PgFormat(`
@@ -209,6 +213,10 @@ async function GetBrick(brickId) {
return brick; return brick;
} }
// U
// D
module.exports = { module.exports = {
Search, Search,
SumPrices, SumPrices,

View File

@@ -0,0 +1,14 @@
const Database = require('../database/database.js');
const Logger = require('../logger.js');
// C
// R
// U
// D
module.exports = {
};

View File

@@ -4,6 +4,9 @@ const Logger = require('../logger.js');
const PgFormat = require('pg-format'); const PgFormat = require('pg-format');
// C
// R
async function Search(fuzzyStrings) { async function Search(fuzzyStrings) {
await Database.Query('BEGIN TRANSACTION;'); await Database.Query('BEGIN TRANSACTION;');
const dbres = await Database.Query(PgFormat(` const dbres = await Database.Query(PgFormat(`
@@ -224,6 +227,10 @@ async function GetSets(page, resPerPage) {
return { total, sets }; return { total, sets };
} }
// U
// D
module.exports = { module.exports = {
Search, Search,
SumPrices, SumPrices,

View File

@@ -7,6 +7,7 @@ const Bricks = require('./bricks-router.js');
const Sets = require('./sets-router.js'); const Sets = require('./sets-router.js');
const Query = require('./query-router.js'); const Query = require('./query-router.js');
const Auth0 = require('./auth0-router.js'); const Auth0 = require('./auth0-router.js');
const Order = require('./order-router.js');
// CRUD is implemented where it makes sense. // CRUD is implemented where it makes sense.
function Init() { function Init() {
@@ -23,13 +24,12 @@ function Init() {
Server.App.post('/api/basket/price/', Helpers.CalculateBasketPrice); Server.App.post('/api/basket/price/', Helpers.CalculateBasketPrice);
Server.App.get('/api/discount/', Helpers.DiscountCode); 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/order:id');
Server.App.get('/api/auth/login/', Auth0.JWTMiddleware, Auth0.Login); 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/orders/');
Server.App.get('/api/auth/order/:id');
Logger.Module('API', 'API Routes Initialized'); Logger.Module('API', 'API Routes Initialized');
} }

View File

@@ -91,5 +91,6 @@ async function Login(req, res) {
module.exports = { module.exports = {
JWTMiddleware, JWTMiddleware,
Auth0GetUser,
Login, Login,
}; };

View File

@@ -7,7 +7,6 @@ const Logger = require('../logger.js');
const Delay = (ms) => new Promise((r) => setTimeout(r, ms)); const Delay = (ms) => new Promise((r) => setTimeout(r, ms));
const EndDate = new Date('2022-06-10T00:00:00.000Z'); const EndDate = new Date('2022-06-10T00:00:00.000Z');
function Special(req, res) { function Special(req, res) {
res.send({ res.send({
data: { data: {
@@ -58,8 +57,6 @@ async function CalculateBasketPrice(req, res) {
} }
} }
console.log(newBrickList);
let setSubtotal = setList.length > 0 let setSubtotal = setList.length > 0
? await SetController.SumPrices(setList, setQuantities) ? await SetController.SumPrices(setList, setQuantities)
: 0; : 0;
@@ -81,8 +78,8 @@ async function CalculateBasketPrice(req, res) {
async function DiscountCode(req, res) { async function DiscountCode(req, res) {
// // artificial delay to simulate a lots of maths // artificial delay to simulate a lots of maths
// await Delay(1000); await Delay(500);
if (!req.query.code) { if (!req.query.code) {
res.send({ res.send({

141
src/routes/order-router.js Normal file
View File

@@ -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,
};