calculations
Former-commit-id: 0281ecdc282da7e83a0bff152f50bc86148cf446
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 !
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
14
docs/API.md
14
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
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
14
src/controllers/order-controller.js
Normal file
14
src/controllers/order-controller.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const Database = require('../database/database.js');
|
||||
const Logger = require('../logger.js');
|
||||
|
||||
// C
|
||||
|
||||
// R
|
||||
|
||||
// U
|
||||
|
||||
// D
|
||||
|
||||
module.exports = {
|
||||
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -91,5 +91,6 @@ async function Login(req, res) {
|
||||
|
||||
module.exports = {
|
||||
JWTMiddleware,
|
||||
Auth0GetUser,
|
||||
Login,
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
141
src/routes/order-router.js
Normal file
141
src/routes/order-router.js
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user