From 3f2037f1fca7d24c30d89de118926ecfff43f57c Mon Sep 17 00:00:00 2001 From: Ben <36240171+benkyd@users.noreply.github.com> Date: Wed, 27 Apr 2022 23:23:52 +0100 Subject: [PATCH] discounts as well as fixing a dumbb fucking bug Former-commit-id: 4c082340cebde8ff3434734797e08e8046a63764 --- client/public/basket.mjs | 109 +++++++++++++++ client/public/components/basket-popout.mjs | 122 +---------------- client/public/components/basket.mjs | 36 ++--- client/public/components/checkout.mjs | 103 ++++++++++++-- client/public/components/css/checkout.css | 126 +++++++++++++++++- .../public/components/css/product-listing.css | 2 +- .../components/immutable-basket-list.mjs | 8 +- client/public/components/product-listing.mjs | 2 +- .../components/super-compact-listing.mjs | 4 +- docs/API.md | 35 ++--- src/controllers/brick-controller.js | 2 + src/controllers/controller-master.js | 1 - src/controllers/misc-controller.js | 40 ++++++ src/routes/api.js | 8 +- src/routes/helpers.js | 65 ++++++++- 15 files changed, 485 insertions(+), 178 deletions(-) create mode 100644 client/public/basket.mjs create mode 100644 src/controllers/misc-controller.js diff --git a/client/public/basket.mjs b/client/public/basket.mjs new file mode 100644 index 0000000..fe80d8f --- /dev/null +++ b/client/public/basket.mjs @@ -0,0 +1,109 @@ +// 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: +// { +// "basket": { +// "items": { +// "item1~modifier": { quantity, type }, +// "item2": { quantity, type }, +// ... +// }, +// "total": total +// }, +// } + +// TODO: Does the localstorage have a problem with mutual exclusion? +// TODO: Should the basket be persisted to the server? +export function GetBasketItems() { + if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { + return; + } + return JSON.parse(localStorage.getItem('basket')).items; +} + +export function AddProductToBasket(product, type, amount, brickModifier = 'none') { + if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { + localStorage.setItem('basket', JSON.stringify({ + items: {}, + total: 0, + })); + } + + const basket = JSON.parse(localStorage.getItem('basket')); + + if (type === 'brick') { + product += '~' + brickModifier; + } + + if (basket.items[product]) { + basket.items[product].quantity += amount; + } else { + basket.items[product] = { + quantity: amount, + type, + }; + } + + basket.total += amount; + + localStorage.setItem('basket', JSON.stringify(basket)); +} + +export function RemoveProductFromBasket(product, type, amount, brickModifier = 'none') { + if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { + return; + } + const basket = JSON.parse(localStorage.getItem('basket')); + + if (type === 'brick') { + product += '~' + brickModifier; + } + + if (basket.items[product]) { + if (basket.items[product].quantity > amount) { + basket.items[product].quantity -= amount; + } else { + delete basket.items[product]; + } + } + + basket.total -= amount; + + localStorage.setItem('basket', JSON.stringify(basket)); +} + +export function GetBasketTotal() { + if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { + return 0; + } + + const basket = JSON.parse(localStorage.getItem('basket')); + + return basket.total; +} + +export async function GetBasketTotalPrice(discount = 0, type = '£', entity_type = undefined) { + if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { + return 0; + } + + const basket = JSON.parse(localStorage.getItem('basket')); + + const res = await fetch('/api/basket/price', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(basket), + }).then(res => res.json()); + + if (res.error) { + return 0; + } + return res.data.subtotal; +} + +export async function GetAbsoluteBasketDiscount(discount = 0, type = '£', entity_type = undefined) { + +} diff --git a/client/public/components/basket-popout.mjs b/client/public/components/basket-popout.mjs index bd7fbcf..c17c69b 100644 --- a/client/public/components/basket-popout.mjs +++ b/client/public/components/basket-popout.mjs @@ -1,120 +1,6 @@ import { RegisterComponent, Component } from './components.mjs'; - -// 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: -// { -// "basket": { -// "items": { -// "item1~modifier": { quantity, type }, -// "item2": { quantity, type }, -// ... -// }, -// "total": total -// }, -// } - -let basketCallback = null; - -// TODO: Does the localstorage have a problem with mutual exclusion? -// TODO: Should the basket be persisted to the server? -export function GetBasketItems() { - if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { - return; - } - return JSON.parse(localStorage.getItem('basket')).items; -} - -export function AddProductToBasket(product, type, amount, brickModifier = 'none') { - if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { - localStorage.setItem('basket', JSON.stringify({ - items: {}, - total: 0, - })); - } - - const basket = JSON.parse(localStorage.getItem('basket')); - - if (type === 'brick') { - product += '~' + brickModifier; - } - - if (basket.items[product]) { - basket.items[product].quantity += amount; - } else { - basket.items[product] = { - quantity: amount, - type, - }; - } - - basket.total += amount; - - localStorage.setItem('basket', JSON.stringify(basket)); - - if (basketCallback) { - basketCallback(); - } -} - -export function RemoveProductFromBasket(product, type, amount, brickModifier = 'none') { - if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { - return; - } - const basket = JSON.parse(localStorage.getItem('basket')); - - if (type === 'brick') { - product += '~' + brickModifier; - } - - if (basket.items[product]) { - if (basket.items[product].quantity > amount) { - basket.items[product].quantity -= amount; - } else { - delete basket.items[product]; - } - } - - basket.total -= amount; - - localStorage.setItem('basket', JSON.stringify(basket)); - - if (basketCallback) { - basketCallback(); - } -} - -export function GetBasketTotal() { - if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { - return 0; - } - - const basket = JSON.parse(localStorage.getItem('basket')); - - return basket.total; -} - -export async function GetBasketTotalPrice() { - if (localStorage.getItem('basket') === null || !localStorage.getItem('basket')) { - return 0; - } - - const basket = JSON.parse(localStorage.getItem('basket')); - - const res = await fetch('/api/basket/price', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(basket), - }).then(res => res.json()); - - if (res.error) { - return 0; - } - return res.data.subtotal; -} +import * as Basket from '../basket.mjs'; +import * as LocalStorageListener from '../localstorage-listener.mjs'; class BasketPopout extends Component { static __IDENTIFY() { return 'basket-popout'; } @@ -129,7 +15,7 @@ class BasketPopout extends Component { if (basket) { try { const basketJSON = JSON.parse(basket); - const subtotal = await GetBasketTotalPrice(); + const subtotal = await Basket.GetBasketTotalPrice(); this.setState({ items: basketJSON.items, total: basketJSON.total, @@ -156,7 +42,7 @@ class BasketPopout extends Component { this.OnLocalBasketUpdate(Object.bind(this)); - basketCallback = this.OnLocalBasketUpdate.bind(this); + LocalStorageListener.ListenOnKey('basket', this.OnLocalBasketUpdate.bind(this)); } Render() { diff --git a/client/public/components/basket.mjs b/client/public/components/basket.mjs index c093272..cec5d10 100644 --- a/client/public/components/basket.mjs +++ b/client/public/components/basket.mjs @@ -1,5 +1,5 @@ -import { GetBasketItems, AddProductToBasket, RemoveProductFromBasket, GetBasketTotal, GetBasketTotalPrice } from './basket-popout.mjs'; import { RegisterComponent, Component } from './components.mjs'; +import * as BasketMaster from '../basket.mjs'; class Basket extends Component { static __IDENTIFY() { return 'basket'; } @@ -93,7 +93,7 @@ class Basket extends Component { } .basket-item-listing { - font-size: 1.3em; + font-size: 1.2em; flex-basis: 65%; flex-grow: 4; } @@ -209,7 +209,7 @@ class Basket extends Component { } async UpdateSubtotal() { - const subtotal = await GetBasketTotalPrice(); + const subtotal = await BasketMaster.GetBasketTotalPrice(); const basketSubtotal = this.root.querySelector('.basket-subtotal'); if (basketSubtotal) { basketSubtotal.innerText = parseFloat(subtotal).toFixed(2); @@ -272,34 +272,34 @@ class Basket extends Component { if (event.target.classList.contains('reduce-quantity')) { if (item.quantity > 0) { item.quantity--; - RemoveProductFromBasket(id, item.type, 1, modifier); + BasketMaster.RemoveProductFromBasket(id, item.type, 1, modifier); } if (item.quantity === 0) { - RemoveProductFromBasket(id, item.type, item.quantity, modifier); + BasketMaster.RemoveProductFromBasket(id, item.type, item.quantity, modifier); this.UpdateSubtotal(); return this.setState({ ...this.state, - total: GetBasketTotal(), + total: BasketMaster.GetBasketTotal(), items: { - ...GetBasketItems(), + ...BasketMaster.GetBasketItems(), }, }); } } else if (event.target.classList.contains('increase-quantity')) { if (item.quantity < item.stock) { item.quantity++; - AddProductToBasket(id, item.type, 1, modifier); + BasketMaster.AddProductToBasket(id, item.type, 1, modifier); } } else if (event.target.classList.contains('remove-quantity')) { - RemoveProductFromBasket(id, item.type, item.quantity, modifier); + BasketMaster.RemoveProductFromBasket(id, item.type, item.quantity, modifier); this.UpdateSubtotal(); return this.setState({ ...this.state, - total: GetBasketTotal(), + total: BasketMaster.GetBasketTotal(), items: { - ...GetBasketItems(), + ...BasketMaster.GetBasketItems(), }, }); } @@ -307,7 +307,7 @@ class Basket extends Component { // update the total this.setState({ ...this.state, - total: GetBasketTotal(), + total: BasketMaster.GetBasketTotal(), items: { ...this.state.items, [compositeId]: item, @@ -349,23 +349,23 @@ class Basket extends Component { } if (parseInt(event.target.value) === 0) { - RemoveProductFromBasket(id, item.type, item.quantity, modifier); + BasketMaster.RemoveProductFromBasket(id, item.type, item.quantity, modifier); return this.setState({ ...this.state, - total: GetBasketTotal(), + total: BasketMaster.GetBasketTotal(), items: { - ...GetBasketItems(), + ...BasketMaster.GetBasketItems(), }, }); } // if has gone up in quantity then add it if (item.quantity < event.target.value) { - AddProductToBasket(id, item.type, event.target.value - item.quantity, modifier); + BasketMaster.AddProductToBasket(id, item.type, event.target.value - item.quantity, modifier); item.quantity = parseInt(event.target.value); } else if (item.quantity > event.target.value) { - RemoveProductFromBasket(id, item.type, item.quantity - event.target.value, modifier); + BasketMaster.RemoveProductFromBasket(id, item.type, item.quantity - event.target.value, modifier); item.quantity = parseInt(event.target.value); } @@ -374,7 +374,7 @@ class Basket extends Component { // update the total this.setState({ ...this.state, - total: GetBasketTotal(), + total: BasketMaster.GetBasketTotal(), items: { ...this.state.items, [compositeId]: item, diff --git a/client/public/components/checkout.mjs b/client/public/components/checkout.mjs index 24433be..da0ad4d 100644 --- a/client/public/components/checkout.mjs +++ b/client/public/components/checkout.mjs @@ -1,4 +1,4 @@ -import { GetBasketTotalPrice } from './basket-popout.mjs'; + import { GetBasketTotalPrice } from '../basket.mjs'; import { RegisterComponent, Component, SideLoad } from './components.mjs'; class Checkout extends Component { @@ -10,7 +10,9 @@ class Checkout extends Component { async OnMount() { this.setState({ + subtotal: parseFloat(await GetBasketTotalPrice()).toFixed(2), total: parseFloat(await GetBasketTotalPrice()).toFixed(2), + discount: 0, }); } @@ -22,10 +24,20 @@ class Checkout extends Component {
+
Shipping Details
-
Shipping Details
- - + + + + + + + + + + + +
Payment Details
@@ -43,7 +55,7 @@ class Checkout extends Component {
- + @@ -53,17 +65,36 @@ class Checkout extends Component {
- +
Your Order edit basket
-
Subtotal ${this.state.total}
-
Shipping (UK Only) ${this.state.total}
-
Total ${this.state.total}
- + +
+
+ Subtotal + £${this.state.subtotal} +
+
+ Delivery + £0.00 +
+
+ Discount + £-${parseFloat(this.state.discount).toFixed(2)} +
+
+ Total + £${parseFloat(this.state.subtotal - this.state.discount).toFixed(2)} +
+
+
+
+ +
@@ -156,6 +187,58 @@ class Checkout extends Component { lastCvv = e.target.value; } }); + + // discount code + this.root.querySelector('.offer-button').addEventListener('click', async () => { + // get discount code + const offerText = this.root.querySelector('input[name="offer-text"]').value; + const offerTextBox = this.root.querySelector('.offer-text'); + + // check if valid + if (offerText.length === 0 || offerTextBox.classList.contains('code-applied')) { + // show error + offerTextBox.classList.add('error'); + setTimeout(() => { + offerTextBox.classList.remove('error'); + }, 1000); + return; + } + + // ask server for discount + const req = await fetch(`/api/discount?code=${encodeURIComponent(offerText)}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }).then((res) => res.json()); + + if (req.error) { + // show error + offerTextBox.classList.add('error'); + setTimeout(() => { + offerTextBox.classList.remove('error'); + }, 1000); + return; + } + + offerTextBox.classList.add('code-applied'); + offerTextBox.disabled = true; + + if (await GetBasketTotalPrice() < req.discount.min_value) { + // show error + offerTextBox.classList.add('error'); + setTimeout(() => { + offerTextBox.classList.remove('error'); + }, 1000); + return; + } + + this.setState({ + subtotal: parseFloat(await GetBasketTotalPrice()).toFixed(2), + total: parseFloat(await GetBasketTotalPrice(req.discount, req.type, req.entity_type)).toFixed(2), + discount: await GetAbsoluteBasketDiscount(req.discount, req.type, req.entity_type), + }); + }); } } diff --git a/client/public/components/css/checkout.css b/client/public/components/css/checkout.css index db196a7..e803b4f 100644 --- a/client/public/components/css/checkout.css +++ b/client/public/components/css/checkout.css @@ -38,14 +38,16 @@ flex-direction: column; } + /* we want the "your order" on top here */ .checkout-body-left { + width: 100%; order: 1; - flex-basis: 100%; } .checkout-body-right { + width: 100%; + margin-top: 50px; order: 0; - flex-basis: 100%; } } @@ -104,10 +106,128 @@ align-items: flex-start; } +.full-width { + width: 100%; +} + +.checkout-place-order { + width: 100%; +} + +.checkout-place-order-button { + width: 100%; + box-shadow: #222 0px 0px 2px; + background: #222; + color: #fff; + border: none; + padding: 10px; + font-size: 1.2em; + cursor: pointer; + transition: all 250ms ease-in-out; +} + .edit-basket { font-size: 0.7em; color: #888; float: right; text-transform: uppercase; } - \ No newline at end of file + +.checkout-summary-prices { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-end; + margin-top: 20px; + margin-bottom: 20px; +} + +.checkout-summary-prices-row { + width: 100%; + margin-top: 10px; + font-size: 1em; + color: #444; + font-weight: lighter; + border-bottom: 1px solid #ccc; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-end; +} + +.discount-row { + color: #E55744; +} + +.checkout-summary-discount-code { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + margin-top: 10px; + margin-bottom: 10px; +} + +.offer-text { + margin-bottom: 10px; + height: 3em; + width: 79%; + background-color: #F5F6F6; + border: 1px solid #ccc; + border-radius: 0.4em; + transition: all 250ms ease-in-out; +} + +.offer-text:focus { + outline: 0; + border: 2px solid transparent; + border-bottom: 2px solid #222; + border-radius: 0; +} + +.offer-button { + width: 14%; + background: #222; + color: #fff; + border: none; + padding: 10px; + font-size: 1em; + cursor: pointer; + transition: all 250ms ease-in-out; +} + +.offer-button:hover { + background-color: #F5F6F6; + outline: 2px solid #222; + color: #222; +} + +.error { + color: #E55744; + font-size: 0.8em; + animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; +} + +@keyframes shake { + 10%, 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, 50%, 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, 60% { + transform: translate3d(4px, 0, 0); + } +} + +.code-applied { + color: #F2CA52; + font-weight: bold; + pointer-events: none; +} diff --git a/client/public/components/css/product-listing.css b/client/public/components/css/product-listing.css index 62a3d5b..ca30972 100644 --- a/client/public/components/css/product-listing.css +++ b/client/public/components/css/product-listing.css @@ -94,7 +94,7 @@ .product-listing-price-new { font-weight: bold; - color: red; + color: #E55744; font-size: 1.5em; } diff --git a/client/public/components/immutable-basket-list.mjs b/client/public/components/immutable-basket-list.mjs index 216e8cb..33d84fb 100644 --- a/client/public/components/immutable-basket-list.mjs +++ b/client/public/components/immutable-basket-list.mjs @@ -1,5 +1,5 @@ import { RegisterComponent, Component } from './components.mjs'; -import { GetBasketTotalPrice } from './basket-popout.mjs'; +import { GetBasketTotalPrice } from '../basket.mjs'; import * as LocalStorageListener from '../localstorage-listener.mjs'; class ImmutableBasketList extends Component { @@ -79,8 +79,10 @@ class ImmutableBasketList extends Component { flex-direction: column; flex-wrap: nowrap; justify-content: left; - height: ${this.state.w || '100%'}; - height: ${this.state.h || 'auto'}; + height: 100%; + width: 100%; + max-width: ${this.state.w || '100%'}; + max-height: ${this.state.h || 'auto'}; overflow-y: scroll; overflow-x: hidden; justify-content: space-between; diff --git a/client/public/components/product-listing.mjs b/client/public/components/product-listing.mjs index 5b8e46d..60e8da6 100644 --- a/client/public/components/product-listing.mjs +++ b/client/public/components/product-listing.mjs @@ -1,5 +1,5 @@ import { RegisterComponent, Component, SideLoad } from './components.mjs'; -import { AddProductToBasket } from './basket-popout.mjs'; +import { AddProductToBasket } from '../basket.mjs'; class ProductListing extends Component { static __IDENTIFY() { return 'product-listing'; } diff --git a/client/public/components/super-compact-listing.mjs b/client/public/components/super-compact-listing.mjs index cf5b5fb..76fe783 100644 --- a/client/public/components/super-compact-listing.mjs +++ b/client/public/components/super-compact-listing.mjs @@ -109,8 +109,8 @@ class SuperCompactProductListing extends Component { } .product-image { - max-height: 150px; - max-width: 150px; + max-height: 110px; + max-width: 110px; object-fit: scale-down; object-position: center; } diff --git a/docs/API.md b/docs/API.md index fde99e6..781d65b 100644 --- a/docs/API.md +++ b/docs/API.md @@ -9,21 +9,22 @@ automatically every request ## Routes | Type | Route | Queries | Auth? | Notes | -| --- | --- | --- | -- | --- | -| GET | /api/special/ | | no | | -| GET | /api/type/:id | | no | | -| GET | /api/search/ | query (q), page | no | Query endpoint | -| GET | /api/bricks/ | query (q), page | no | Query endpoint | -| GET | /api/sets/ | query (q), page | no | Query endpoint | -| GET | /api/sets/featured | page | no | Query endpoint | -| GET | /api/brick/:id | | no | | -| POST | /api/bulk/brick | array | no | POST due to bulk nature | -| GET | /api/set/:id | | no | | -| GET | /api/cdn/:id | | no | | -| GET | /api/basket/price/ | | no | | -| PUT | /api/auth/login/ | | yes | | -| POST | /api/auth/signup/ | | yes | | -| GET | /api/auth/orders/ | | yes | | +| --- | --- | --- | - | --- | +| GET | /api/special/ | | ❌ | | +| GET | /api/search/ | query (q), page | ❌ | Query endpoint | +| GET | /api/bricks/ | query (q), page | ❌ | Query endpoint | +| GET | /api/sets/ | query (q), page | ❌ | Query endpoint | +| GET | /api/sets/featured | page | ❌ | Query endpoint | +| GET | /api/brick/:id | | ❌ | | +| POST | /api/bulk/brick | array | ❌ | POST due to bulk nature | +| GET | /api/set/:id | | ❌ | | +| GET | /api/cdn/:id | | ❌ | | +| GET | /api/basket/price/ | | ❌ | | +| GET | /api/discount/ | offer code | ❌ | | +| GET | /api/auth/login/ | | ✔️ | | +| POST | /api/auth/order/ | | ✔️❌ | | +| GET | /api/auth/order/:id | | ✔️❌ | | +| GET | /api/auth/orders/ | | ✔️ | | Query endpoints do not return the full data on a brick/set, they return a subset for product listing pages @@ -52,11 +53,11 @@ set: brick to search for (absolute, fuzzy string) ```js { - error: false data: { // defined in the response description for each route } - // other important data, or metadata for the data can be added here + // other important data, or metadata for the data + // (such as pagination data) can be added here } ``` diff --git a/src/controllers/brick-controller.js b/src/controllers/brick-controller.js index 4b788fb..316d486 100644 --- a/src/controllers/brick-controller.js +++ b/src/controllers/brick-controller.js @@ -91,6 +91,8 @@ async function SumPrices(bricksArr, quantityArray) { } Database.Query('COMMIT TRANSACTION;'); + console.log(dbres.rows) + // validate database response if (dbres.rows.length === 0) { return { diff --git a/src/controllers/controller-master.js b/src/controllers/controller-master.js index 36515dc..6e8b10d 100644 --- a/src/controllers/controller-master.js +++ b/src/controllers/controller-master.js @@ -57,7 +57,6 @@ function SanatiseQuery(query) { query = query.trim(); query = query.replace(/[^a-zA-Z0-9,&/\s]/g, ''); query = escape(query); - query = query.toLowerCase(); return query; } diff --git a/src/controllers/misc-controller.js b/src/controllers/misc-controller.js new file mode 100644 index 0000000..ba73e31 --- /dev/null +++ b/src/controllers/misc-controller.js @@ -0,0 +1,40 @@ +const Database = require('../database/database.js'); +const Logger = require('../logger.js'); + +async function GetDiscount(code) { + await Database.Query('BEGIN TRANSACTION;'); + const dbres = await Database.Query(` + SELECT discount, discount_type, min_order_value, type + FROM offer_code + WHERE code = $1; + `, [code]).catch(() => { + return { + error: 'Database error', + }; + }); + if (dbres.error) { + Database.Query('ROLLBACK TRANSACTION;'); + Logger.Error(dbres.error); + return dbres.error; + } + + // validate database response + if (dbres.rows.length === 0) { + return { + error: 'Discount code not found', + long: 'The discount code you are looking for does not exist', + }; + } + + return { + discount: dbres.rows[0].discount, + type: dbres.rows[0].discount_type === '0' ? '%' : '£', + min_value: dbres.rows[0].min_order_value, + entity_type: dbres.rows[0].type, + end_date: dbres.rows[0].expiry_date, + }; +} + +module.exports = { + GetDiscount, +}; diff --git a/src/routes/api.js b/src/routes/api.js index 512865e..01089f5 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -13,6 +13,7 @@ function Init() { Server.App.get('/api/special/', Helpers.Special); Server.App.get('/api/search/', Query.Search); + Server.App.get('/api/bricks/', Bricks.Query); Server.App.get('/api/sets/'); Server.App.get('/api/sets/featured/', Sets.Featured); @@ -22,12 +23,13 @@ function Init() { Server.App.get('/api/cdn/:id', CDN.Get); - Server.App.get('/api/auth/login', Auth0.JWTMiddleware, Auth0.Login); + Server.App.post('/api/basket/price/', Helpers.CalculateBasketPrice); + Server.App.get('/api/discount/', Helpers.DiscountCode); + + Server.App.get('/api/auth/login/', Auth0.JWTMiddleware, Auth0.Login); Server.App.get('/api/auth/orders/'); Server.App.get('/api/auth/order/:id'); - Server.App.post('/api/basket/price', Helpers.CalculateBasketPrice); - Logger.Module('API', 'API Routes Initialized'); } diff --git a/src/routes/helpers.js b/src/routes/helpers.js index a2e65fd..d95488d 100644 --- a/src/routes/helpers.js +++ b/src/routes/helpers.js @@ -1,7 +1,11 @@ +const ControllerMaster = require('../controllers/controller-master.js'); const BrickController = require('../controllers/brick-controller.js'); const SetController = require('../controllers/set-controller.js'); +const MiscController = require('../controllers/misc-controller.js'); 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) { @@ -39,11 +43,26 @@ async function CalculateBasketPrice(req, res) { } } + // combine bricks by id and quantity into a single array + // this annoyingly happens as the brick ids are not unique + // when it comes to composite IDs based on modifiers. + // As modifiers do not change the price of a brick this is fine. + const newBrickList = []; + const newBrickQuantities = []; + for (let i = 0; i < brickList.length; i++) { + if (!newBrickList.includes(brickList[i])) { + newBrickList[i] = brickList[i]; + newBrickQuantities[i] = 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(brickList, brickQuantities) + ? await BrickController.SumPrices(newBrickList, newBrickQuantities) : 0; if (setSubtotal.error) setSubtotal = 0; @@ -59,7 +78,51 @@ async function CalculateBasketPrice(req, res) { } +async function DiscountCode(req, res) { + // // artificial delay to simulate a lots of maths + // await Delay(1000); + + if (!req.query.code) { + res.send({ + error: 'No code provided', + }); + return; + } + + const sanatisedCode = ControllerMaster.SanatiseQuery(req.query.code); + + const discount = await MiscController.GetDiscount(sanatisedCode); + + + Logger.Debug(JSON.stringify(discount)); + + if (discount.error) { + res.send({ + error: discount.error, + }); + return; + } + + if (discount.end_date < new Date()) { + res.send({ + error: 'Discount code expired', + }); + return; + } + + res.send({ + data: { + discount: discount.discount, + type: discount.type, + min_value: discount.min_value, + entity_type: discount.entity_type, + end_date: discount.end, + }, + }); +} + module.exports = { Special, CalculateBasketPrice, + DiscountCode, };