diff --git a/api/lib/limits.py b/api/lib/limits.py new file mode 100644 index 0000000..bf11fc7 --- /dev/null +++ b/api/lib/limits.py @@ -0,0 +1,52 @@ +# coding=UTF-8 + +CANVAS = { + 'max_long_side': 500, + 'min_long_side': 20, + 'max_short_side': 150, + 'min_short_side': 20 +} + +CANVAS_FRAME = { + 'max_long_side': 150, + 'min_long_side': 40, + 'max_short_side': 150, + 'min_short_side': 40, +} + + +def calculate_canvas_frame_limits(width, height): + if width > height: + width = min(width, CANVAS_FRAME.get('max_long_side')) + width = max(width, CANVAS_FRAME.get('min_long_side')) + height = min(height, CANVAS_FRAME.get('max_short_side')) + height = max(height, CANVAS_FRAME.get('min_short_side')) + else: + height = min(height, CANVAS_FRAME.get('max_long_side')) + height = max(height, CANVAS_FRAME.get('min_long_side')) + width = min(width, CANVAS_FRAME.get('max_short_side')) + width = max(width, CANVAS_FRAME.get('min_short_side')) + + # round to nearest 10 + width = round(width, -1) + height = round(height, -1) + + return width, height + + +def calculate_canvas_limits(width, height, framed=False): + if framed: + return calculate_canvas_frame_limits(width, height) + + if width > height: + width = min(width, CANVAS.get('max_long_side')) + width = max(width, CANVAS.get('min_long_side')) + height = min(height, CANVAS.get('max_short_side')) + height = max(height, CANVAS.get('min_short_side')) + else: + height = min(height, CANVAS.get('max_long_side')) + height = max(height, CANVAS.get('min_long_side')) + width = min(width, CANVAS.get('max_short_side')) + width = max(width, CANVAS.get('min_short_side')) + + return width, height diff --git a/api/lib/prices.py b/api/lib/prices.py index 4f20a75..9b1f3d4 100644 --- a/api/lib/prices.py +++ b/api/lib/prices.py @@ -1,7 +1,9 @@ # coding=UTF-8 - """ Price formulas """ +from api.lib.utils import round_half_up +from api.lib.limits import calculate_canvas_limits + def wallpaper_price(width, height, material_price, market, designer=None, reseller=None, inc_vat=True): m2_price = material_price @@ -96,3 +98,81 @@ def canvas_price(width, height, market, framed=True, designer=None, reseller=Non price *= market.vat return round(price) + + +def canvas_frame_price(width, height, stock_price, additional_price=None): + width, height = calculate_canvas_limits(width, height, framed=True) + circumference = (2 * width) + (2 * height) + circumference = max(circumference, 170) + + price = stock_price * circumference + + # if width or height goes above this limit additional costs are added + if additional_price: + additional_price_limit = 120 + if width > additional_price_limit or height > additional_price_limit: + price += additional_price + + return price + + +def old_canvas_diy_frame_price(width, height, market, reseller=None, inc_vat=True): + # todo: when product models are in place we should use stockproduct ""Do it yourself frame"" + # instead of hardcoded values + stock_price = 1.61084 + additional_price = 94.750000 + price = canvas_frame_price(width, height, stock_price, additional_price) + + # exchange to local currency + price *= market.exchange_rate + + # multiply price with market price adjustments + price *= market.price_adjustments + + # add reseller price premium + if reseller: + price *= (float(reseller.pricepremium) / 100) + 1 + + # add vat + if inc_vat: + price *= market.vat + + return round_half_up(price) + + +def old_canvas_price(width, height, material_price, market, framed=True, designer=None, reseller=None, inc_vat=True): + width, height = calculate_canvas_limits(width, height, framed) + # calculate square meter + sqm = (width / 100) * (height / 100) + sqm = max(sqm, 0.2) # sqm can never go below 0.2 + + # calculate price per square meter + price = sqm * material_price + + if framed: + # todo: when product models are in place we should use stockproduct + # "Canvasframe" instead of hardcoded values + stock_price = 1.04230824 + additional_price = 94.750000 + price += canvas_frame_price(width, height, + stock_price, additional_price) + + # add designer price premium + if designer: + price *= (float(designer.pricepremium) / 100) + 1 + + # add reseller price premium + if reseller: + price *= (float(reseller.pricepremium) / 100) + 1 + + # exchange to local currency + price *= market.exchange_rate + + # multiply price with market price adjustments + price *= market.price_adjustments + + # add vat + if inc_vat: + price *= market.vat + + return round_half_up(price) diff --git a/api/lib/utils.py b/api/lib/utils.py new file mode 100644 index 0000000..2e31930 --- /dev/null +++ b/api/lib/utils.py @@ -0,0 +1,5 @@ +from decimal import Decimal, ROUND_HALF_UP + + +def round_half_up(val): + return int(Decimal(val).quantize(0, ROUND_HALF_UP)) diff --git a/api/models/contract_customer.py b/api/models/contract_customer.py index 22f3252..ff9a631 100644 --- a/api/models/contract_customer.py +++ b/api/models/contract_customer.py @@ -1,9 +1,24 @@ +# coding=UTF-8 + +from flask_sqlalchemy import BaseQuery from api import db +class ContractCustomerQuery(BaseQuery): + + def reseller_store(self, url, territory): + return self.filter( + ContractCustomer.dealerstore == True, + ContractCustomer.dealerurl == url, + ContractCustomer.country == territory).first() + + class ContractCustomer(db.Model): + __tablename__ = 'contract_customers' + query_class = ContractCustomerQuery + id = db.Column('contractcustomerid', db.Integer, primary_key=True) country = db.Column(db.String(2)) pricepremium = db.Column(db.Numeric, nullable=False, default=0) diff --git a/api/models/market.py b/api/models/market.py index 8cf7696..6f6fc2e 100644 --- a/api/models/market.py +++ b/api/models/market.py @@ -78,7 +78,7 @@ _markets = [ Market('United kingdom', 'en_GB', 'GBP', 1.20, 'GBR'), Market('Denmark', 'da_DK', 'DKK', 1.25, 'DNK'), Market('Finland', 'fi_FI', 'EUR', 1.24, 'FIN'), - Market('Russia', 'ru_RU', 'RUB', 1, 'RUS'), + Market('Russia', 'ru_RU', 'EUR', 1, 'RUS'), Market('Germany', 'de_DE', 'EUR', 1.19, 'DEU'), Market('Netherlands', 'nl_NL', 'EUR', 1.21, 'NLD'), Market('Austria', 'de_AT', 'EUR', 1.20, 'AUT'), diff --git a/api/resources/prices.py b/api/resources/prices.py index 4fee3db..f6d42b9 100644 --- a/api/resources/prices.py +++ b/api/resources/prices.py @@ -2,33 +2,33 @@ from flask import Blueprint, request, jsonify, abort from api.models.product import Material from api.models.contract_customer import ContractCustomer from api.models import market as market_model -from api.lib.prices import wallpaper_price, canvas_price +from api.lib.prices import wallpaper_price, old_canvas_price, old_canvas_diy_frame_price +from api.lib.limits import calculate_canvas_limits from api.validators import validate_territory, validate_number, validate_keys_exists mod = Blueprint('prices', __name__, url_prefix='/prices') +def get_reseller(): + dealerurl = request.args.get('dealerurl') + territory = request.args.get('territory') + if dealerurl and territory: + return ContractCustomer.query.reseller_store(dealerurl, territory) + return None + + @mod.route("/wallpaper", methods=['GET']) def wallpaper(): validate_keys_exists(request.args, 'width', 'height', 'territory') validate_number(request.args.get('width'), request.args.get('height')) validate_territory(request.args.get('territory')) + width = request.args.get('width', type=int) + height = request.args.get('height', type=int) market = market_model.from_territory(request.args.get('territory')) - params = { - 'width': request.args.get('width', type=int), - 'height': request.args.get('height', type=int), - 'market': market, - } - - dealerurl = request.args.get('dealerurl') - if dealerurl: - reseller = ContractCustomer.query.filter_by( - dealerstore=True, dealerurl=dealerurl, country=market.territory).first() - if reseller: - params['reseller'] = reseller + reseller = get_reseller() materials = Material.query.wallpapers().all() data = [] @@ -36,7 +36,7 @@ def wallpaper(): data.append({ 'material': material.name, 'material_id': material.id, - 'price': wallpaper_price(material_price=material.price, **params), + 'price': wallpaper_price(width, height, material.price, market, reseller=reseller), }) return jsonify(data=data) @@ -47,28 +47,45 @@ def canvas(): validate_keys_exists(request.args, 'width', 'height', 'territory') validate_number(request.args.get('width'), request.args.get('height')) validate_territory(request.args.get('territory')) - market = market_model.from_territory(request.args.get('territory')) - - params = { - 'width': request.args.get('width', type=int), - 'height': request.args.get('height', type=int), - 'market': market - } - - dealerurl = request.args.get('dealerurl') - if dealerurl: - reseller = ContractCustomer.query.filter_by( - dealerstore=True, dealerurl=dealerurl, country=market.territory).first() - if reseller: - params['reseller'] = reseller + reseller = get_reseller() + material = Material.query.canvas().first() data = [] for framed in [True, False]: + width, height = calculate_canvas_limits( + request.args.get('width', type=int), + request.args.get('height', type=int), + framed + ) data.append({ 'framed': framed, - 'price': canvas_price(framed=framed, **params) + 'width': width, + 'height': height, + 'price': old_canvas_price(width, height, material.price, market, reseller=reseller, framed=framed) }) return jsonify(data=data) + + +@mod.route('/diy-frame', methods=['GET']) +def diy_frame(): + validate_keys_exists(request.args, 'width', 'height', 'territory') + validate_number(request.args.get('width'), request.args.get('height')) + validate_territory(request.args.get('territory')) + market = market_model.from_territory(request.args.get('territory')) + reseller = get_reseller() + width, height = calculate_canvas_limits( + request.args.get('width', type=int), + request.args.get('height', type=int), + framed=True + ) + + data = [{ + 'width': width, + 'height': height, + 'price': old_canvas_diy_frame_price(width, height, market, reseller=reseller) + }] + + return jsonify(data=data) diff --git a/docs/prices.md b/docs/prices.md index a50f3fb..6a56d6b 100644 --- a/docs/prices.md +++ b/docs/prices.md @@ -43,7 +43,7 @@ Sample response ## Calculate canvas price -Calculate price inc VAT of a canvas product. The response will contain information for both framed/not framed canvases. +Calculate price inc VAT of a canvas product. The response will contain information for both framed/not framed canvases. Note that width and height parameters may be adjusted if they go over or under the limits that are valid for a canvas. The values that are used in the price-calculation are always returned in the response. | Parameter | Description | | --------- | -----| -------- | - @@ -63,11 +63,45 @@ Sample response "data": [ { "framed": true, - "price": 284 + "price": 284, + "width": 100, + "height": 50, }, { "framed": false, - "price": 249 + "price": 249, + "width": 100, + "height": 50, + } + ] +} +``` + + +## Calculate "Do it yourself frame" price + +Calculate the price for the "do it yourself frame" (DIY). Note that width and height parameters may be adjusted if they go over or under the limits that are valid for a canvas-frame. The values that are used in the price-calculation are always returned in the response. + +| Parameter | Description | +| --------- | -----| -------- | - +| `width` | Canvas width in cm e.g `100'. | +| `height` | Canvas height in cm e.g `50` | +| `territory` | Two letter iso code e.g `SE`. Territory affect the price in different ways like VAT, currency exchanges etc. | +| `dealerurl` | Optional parameter e.g `ackes`. Should always be used when calling the api from a dealerstore. If a dealerstore exists with this url additional fees will be applied to the price. | + + +Sample request + +`curl -X GET 'https://api2.photowall.com/prices/diy-frame?width=150&height=150&territory=SE` + +Sample response +``` +{ + "data": [ + { + "height": 150, + "price": 1327, + "width": 150 } ] } diff --git a/tests/lib/test_limits.py b/tests/lib/test_limits.py new file mode 100644 index 0000000..f3aac75 --- /dev/null +++ b/tests/lib/test_limits.py @@ -0,0 +1,22 @@ +# coding=UTF-8 + +import unittest2 +from api.lib.limits import calculate_canvas_limits, calculate_canvas_frame_limits + + +class TestCalculate(unittest2.TestCase): + + def test_calculate_canvas_limits(self): + self.assertEqual((100, 100), calculate_canvas_limits(100, 100)) + self.assertEqual((500, 100), calculate_canvas_limits(800, 100)) + self.assertEqual((100, 500), calculate_canvas_limits(100, 800)) + self.assertEqual((20, 20), calculate_canvas_limits(10, 10)) + self.assertEqual((150, 50), calculate_canvas_limits( + 400, 50, True), "Framed canvas") + + def test_calculate_canvas_frame_limits(self): + self.assertEqual((100, 100), calculate_canvas_frame_limits(100, 100)) + self.assertEqual((40, 40), calculate_canvas_frame_limits(1, 1)) + self.assertEqual((150, 150), calculate_canvas_frame_limits(200, 200)) + self.assertEqual((150, 40), calculate_canvas_frame_limits(200, 10)) + self.assertEqual((110, 120), calculate_canvas_frame_limits(112, 117), "round to nearest 10") diff --git a/tests/test_price.py b/tests/lib/test_prices.py similarity index 60% rename from tests/test_price.py rename to tests/lib/test_prices.py index 4b0e6ad..b86d8fd 100644 --- a/tests/test_price.py +++ b/tests/lib/test_prices.py @@ -1,7 +1,7 @@ # coding=UTF-8 import unittest2 -from api.lib.prices import wallpaper_price, canvas_price, canvas_cm2_price +from api.lib.prices import * from tests.factories import DesignerFactory, ContractCustomerFactory, InquiryFactory from api.models import market @@ -80,3 +80,46 @@ class TestPrice(unittest2.TestCase): self.assertEqual(436, canvas_price(width=50, height=50, market=denmark, framed=True, inc_vat=False)) + + def test_canvas_frame_price(self): + self.assertAlmostEqual(208.46, canvas_frame_price( + 50, 50, stock_price=1.04230824, additional_price=94.750000), places=2) + self.assertAlmostEqual(177.19, canvas_frame_price( + 1, 1, stock_price=1.04230824, additional_price=94.750000), places=2) + self.assertAlmostEqual(625.38, canvas_frame_price( + 500, 500, stock_price=1.04230824, additional_price=None), places=2) + + def test_old_canvas_diy_frame_price(self): + self.assertEqual( + 403, old_canvas_diy_frame_price(50, 50, market=sweden)) + self.assertEqual(801, old_canvas_diy_frame_price( + 100, 110, market=denmark)) + self.assertEqual(463, old_canvas_diy_frame_price( + 50, 50, market=sweden, reseller=reseller)) + self.assertEqual(1327, old_canvas_diy_frame_price( + 500, 500, market=sweden)) + + def test_old_canvas_price(self): + self.assertEqual(250, old_canvas_price( + width=50, height=50, material_price=799.2, market=sweden, framed=False)) + + self.assertEqual(200, old_canvas_price( + width=1, height=1, material_price=799.2, market=sweden, framed=False)) + + self.assertEqual(7493, old_canvas_price( + width=500, height=500, material_price=799.2, market=sweden, framed=False)) + + self.assertEqual(5994, old_canvas_price(width=500, height=500, + material_price=799.2, market=sweden, framed=False, inc_vat=False)) + self.assertEqual(7493, old_canvas_price( + width=500, height=500, material_price=799.2, market=sweden, framed=False)) + self.assertEqual(236, old_canvas_price( + width=50, height=50, material_price=799.2, market=denmark, framed=False)) + self.assertEqual(294, old_canvas_price( + width=50, height=50, material_price=799.2, market=sweden, designer=designer, framed=False)) + self.assertEqual(510, old_canvas_price( + width=50, height=50, material_price=799.2, market=sweden, framed=True)) + self.assertEqual(2972, old_canvas_price( + width=150, height=140, material_price=799.2, market=sweden, framed=True)) + self.assertEqual(287, old_canvas_price( + width=50, height=50, material_price=799.2, market=sweden, framed=False, reseller=reseller)) diff --git a/tests/resources/test_prices.py b/tests/resources/test_prices.py new file mode 100644 index 0000000..75dfaa4 --- /dev/null +++ b/tests/resources/test_prices.py @@ -0,0 +1,49 @@ +import json +import flask_testing +from api import create_app, db +from api.models.product import Material, MaterialPrice + + +class TestEndpoints(flask_testing.TestCase): + + def create_app(self): + return create_app('tests.config') + + def setUp(self): + db.create_all() + + def tearDown(self): + db.session.remove() + db.drop_all() + + def _add_material(self, name, groupid, price): + material = Material(name=name) + material.material_price = MaterialPrice(price=price, groupid=groupid) + db.session.add(material) + db.session.commit() + return material + + def test_canvas(self): + self._add_material('standard-canvas', 2, 799.9) + response = self.client.get( + '/prices/canvas?width=200&height=10&territory=SE') + self.assert200(response) + products = response.json['data'] + for product in products: + if product['framed']: + self.assertEqual(150, product['width']) + self.assertEqual(40, product['height']) + self.assertEqual(1213, product['price']) + else: + self.assertEqual(200, product['width']) + self.assertEqual(20, product['height']) + self.assertEqual(400, product['price']) + + def test_diy_frame(self): + response = self.client.get( + '/prices/diy-frame?width=200&height=50&territory=SE') + self.assert200(response) + product = response.json['data'][0] + self.assertEqual(150, product['width'], "it adjusts measurements") + self.assertEqual(50, product['height']) + self.assertEqual(924, product['price'])