over time, done
This commit is contained in:
parent
8cf70c4b21
commit
e61789bb1b
|
|
@ -0,0 +1,2 @@
|
||||||
|
# left empty so the user has to import all submodules explicitly,
|
||||||
|
# this ensures clear namespace separation
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
from flask_restful import Resource
|
||||||
|
from flask import session, Response, request
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from src.services import BasketService
|
||||||
|
|
||||||
|
|
||||||
|
class Basket(Resource):
|
||||||
|
basket_service = BasketService()
|
||||||
|
|
||||||
|
def get_session_id():
|
||||||
|
# first interaction with server sets session_id
|
||||||
|
# would need to research if this is ideal
|
||||||
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
if "session_id" not in session:
|
||||||
|
session["session_id"] = self.get_session_id()
|
||||||
|
return Response(
|
||||||
|
{"total": self.basket_service.total()}, mimetype="application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
# expects something similar: {"items": [{"item_id": abc002}]}
|
||||||
|
|
||||||
|
if "session_id" not in session:
|
||||||
|
session["session_id"] = self.get_session_id()
|
||||||
|
jsonData = request.get_json(force=True)
|
||||||
|
for entry in jsonData["items"]:
|
||||||
|
self.basket_service.scan(entry["item_id"])
|
||||||
|
return Response(HTTPStatus=201)
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from flask import Flask, request, g, render_template, redirect
|
||||||
|
from flask_restful import Resource, reqparse
|
||||||
|
from flask_restful_swagger_3 import Api
|
||||||
|
from json import dumps
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import endpoints.BasketEndpoint as BasketEndpoint
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = "SUPER_DUPER_BAD_SECRET_KEY"
|
||||||
|
api = Api(app)
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
api.add_resource(BasketEndpoint.Basket, "/api/v1/basket/")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
"""in lieu of a UI redirect to the API doc"""
|
||||||
|
app.logger.info(f"request from {request.remote_addr}")
|
||||||
|
return redirect("/api/doc/swagger.json", code=302)
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Item:
|
||||||
|
id: str
|
||||||
|
price: str
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
from main import app
|
||||||
|
|
||||||
|
# disabled threading for easier session handleing in this demo project
|
||||||
|
# if threaded was True, as session store would be needed
|
||||||
|
app.run(host="0.0.0.0", port="80", debug=False, threaded=False)
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
from collections import defaultdict
|
||||||
|
import math
|
||||||
|
from services.DealsService import DealsService
|
||||||
|
from services.ProductService import ProductService
|
||||||
|
|
||||||
|
|
||||||
|
class BasketService:
|
||||||
|
baskets = None
|
||||||
|
dealsService = None
|
||||||
|
productService = None
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.baskets = defaultdict()
|
||||||
|
self.dealsService = DealsService()
|
||||||
|
self.productService = ProductService()
|
||||||
|
|
||||||
|
def total(self, sessionID: str) -> int:
|
||||||
|
# this function should really be in a separate file but I am already over time
|
||||||
|
# this round up function needs to be checked with stakeholders
|
||||||
|
# as it would generate millions in profit in a big online shop,
|
||||||
|
# but could be considered unfriendly towards consumers
|
||||||
|
def _round_up(val: float):
|
||||||
|
return math.ceil(val * 100) / 100
|
||||||
|
items = self.baskets[sessionID]
|
||||||
|
prices = self.dealsService.get_items_with_final_prices(items)
|
||||||
|
return _round_up(sum([item.price for item in prices]))
|
||||||
|
|
||||||
|
def scan(self, session_id: str, itemId) -> None:
|
||||||
|
item = self.productService.get_item_from_id(itemId)
|
||||||
|
if session_id not in self.baskets:
|
||||||
|
self.baskets[session_id] = []
|
||||||
|
self.baskets[session_id].append(item)
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
from collections import defaultdict
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
class DealsService:
|
||||||
|
# deals and promotions are global and therefor class vars instead of instance vars
|
||||||
|
deals = dict()
|
||||||
|
promotions = defaultdict(list)
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# placeholder data
|
||||||
|
# promiootion can be read dznamically from db
|
||||||
|
self.promotions["A0001"].append("two_for_one")
|
||||||
|
self.promotions["A0002"].append("ten_percent_off")
|
||||||
|
|
||||||
|
# deal behavior needs to be programmed an can only be
|
||||||
|
# changed with the rollout of a new version,
|
||||||
|
self.deals["two_for_one"] = DealsService.two_for_one_deal
|
||||||
|
self.deals["ten_percent_off"] = DealsService.ten_percent_off
|
||||||
|
|
||||||
|
def get_items_with_final_prices(self, items) -> list:
|
||||||
|
groups = defaultdict(list)
|
||||||
|
|
||||||
|
for obj in items:
|
||||||
|
groups[obj.id].append(obj)
|
||||||
|
|
||||||
|
for group in groups.values():
|
||||||
|
# the deals do not stack, the first deal in the list of deals is taken,
|
||||||
|
# ideally for the customer all deals would be calculated and the maximum value selected
|
||||||
|
# not implemented for time reasons
|
||||||
|
|
||||||
|
for promotion in self.promotions.get(group[0].id):
|
||||||
|
# iterate over all promotions for this item
|
||||||
|
# lookup the higher order function that is associated with this deal name
|
||||||
|
# apply the function to the item group
|
||||||
|
# if the deal was applied to the group of items break and move on to the next item group
|
||||||
|
# this avoids discout stacking
|
||||||
|
if promotion not in self.deals:
|
||||||
|
continue
|
||||||
|
if self.deals[promotion](group):
|
||||||
|
break
|
||||||
|
|
||||||
|
return [item for group in groups.values() for item in group]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def two_for_one_deal(group):
|
||||||
|
"""' applies deal onto item group in place"""
|
||||||
|
|
||||||
|
# ideally would be an object implementing a "Deal"-interface,
|
||||||
|
# which has 2 functions, deal_is_applicable() and apply_deal()
|
||||||
|
def _round_up(val: float):
|
||||||
|
return math.ceil(val * 100) / 100
|
||||||
|
|
||||||
|
# could be compressed
|
||||||
|
# not done since this would be the 2 different functions in the above mentioned interface
|
||||||
|
deal_is_applicable = False
|
||||||
|
if len(group) >= 2:
|
||||||
|
deal_is_applicable = True
|
||||||
|
|
||||||
|
for i in range(len(group) - len(group) % 2):
|
||||||
|
group[i].price = _round_up(group[i].price / 2)
|
||||||
|
return deal_is_applicable
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ten_percent_off(group):
|
||||||
|
# the percentage discount could be added to function signature
|
||||||
|
"""' applies deal onto item group in place"""
|
||||||
|
|
||||||
|
# ideally would be an object implementing a "Deal"-interface,
|
||||||
|
# which has 2 functions, deal_is_applicable() and apply_deal()
|
||||||
|
def _round_up(val: float):
|
||||||
|
return math.ceil(val * 100) / 100
|
||||||
|
|
||||||
|
deal_is_applicable = True
|
||||||
|
|
||||||
|
for i in range(len(group)):
|
||||||
|
group[i].price = _round_up(group[i].price * 0.9)
|
||||||
|
return deal_is_applicable
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
from models.Item import Item
|
||||||
|
|
||||||
|
|
||||||
|
class ProductService:
|
||||||
|
# prices are global, sheduled function to refresh prices periodically
|
||||||
|
# could save ressources under high load
|
||||||
|
prices = dict()
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
# placeholder data
|
||||||
|
self.prices["A0001"] = 12.99
|
||||||
|
self.prices["A0002"] = 3.99
|
||||||
|
|
||||||
|
def get_item_from_id(self, item_id: str) -> Item:
|
||||||
|
return Item(id=item_id, price=self.prices.get(item_id))
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
|
||||||
|
from services import BasketService
|
||||||
|
|
||||||
|
|
||||||
|
def test__BasketService():
|
||||||
|
bs = BasketService.BasketService()
|
||||||
|
bs.scan("1", "A0001")
|
||||||
|
bs.scan("1", "A0001")
|
||||||
|
bs.scan("1", "A0001")
|
||||||
|
bs.scan("1", "A0002")
|
||||||
|
bs.scan("1", "A0002")
|
||||||
|
assert bs.total("1") == 33.2
|
||||||
|
print("total: ", bs.total("1"))
|
||||||
|
|
||||||
|
test__BasketService()
|
||||||
Loading…
Reference in New Issue