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.
|
// 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({
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 !
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
14
docs/API.md
14
docs/API.md
@@ -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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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');
|
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,
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,5 +91,6 @@ async function Login(req, res) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
JWTMiddleware,
|
JWTMiddleware,
|
||||||
|
Auth0GetUser,
|
||||||
Login,
|
Login,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
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