diff --git a/README.md b/README.md index c5dd3e0..bdfe1a2 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ Provides a REST API to Photowall database. +## Resources + +* [Prices](docs/prices.md) + ## Installation Make sure that your system has Python version 3.4 installed. If you dont have it here is how you install it on Ubuntu 12.04: diff --git a/api/core.py b/api/core.py index 5fd8ff0..ebf253f 100644 --- a/api/core.py +++ b/api/core.py @@ -30,10 +30,11 @@ def create_app(config_name='config'): return response # register resources - from api.resources import server, designers, categories + from api.resources import server, designers, categories, prices app.register_blueprint(server.mod) app.register_blueprint(designers.mod) app.register_blueprint(categories.mod) + app.register_blueprint(prices.mod) # authentication app.before_request(check_api_key) diff --git a/api/lib/prices.py b/api/lib/prices.py new file mode 100644 index 0000000..7b5f9d8 --- /dev/null +++ b/api/lib/prices.py @@ -0,0 +1,91 @@ +# coding=UTF-8 + +""" Price formulas """ + + +def wallpaper_price(width, height, material_price, market, designer=None, reseller=None, inc_vat=True): + m2_price = material_price + + # add designer price premium + if designer: + m2_price *= (float(designer.pricepremium) / 100) + 1 + + # add reseller price premium + if reseller: + m2_price *= (float(reseller.pricepremium) / 100) + 1 + + # exchange to local currency + m2_price *= market.exchange_rate + + # multiply m2_price with market price adjustments + m2_price *= market.price_adjustments + + # calculate price per square meter + sqm = (width / 100) * (height / 100) + sqm = max(sqm, 1) + price = sqm * m2_price + + # add vat + if inc_vat: + price *= market.vat + + return round(price) + + +def canvas_cm2_price(cm2): + price_cm2_at_1cm2 = 0.224 + price_cm2_at_5000cm2 = 0.144 + price_cm2_at_10000cm2 = 0.096 + price_cm2_at_22500cm2 = 0.064 + + if cm2 <= 5000: + diff = price_cm2_at_1cm2 - price_cm2_at_5000cm2 + discount = (diff / 5000) * cm2 + cm2_price = price_cm2_at_1cm2 - discount + + elif cm2 > 5000 and cm2 <= 10000: + diff = price_cm2_at_5000cm2 - price_cm2_at_10000cm2 + discount = (diff / 5000) * (cm2 - 5000) + cm2_price = price_cm2_at_5000cm2 - discount + + elif cm2 > 10000: + diff = price_cm2_at_10000cm2 - price_cm2_at_22500cm2 + discount = (diff / 12500) * (cm2 - 10000) + cm2_price = price_cm2_at_10000cm2 - discount + + cm2_price = max(cm2_price, 0.064) + return cm2_price + + +def canvas_price(width, height, market, framed=True, designer=None, reseller=None, inc_vat=True): + cm2 = width * height + + # get the price per cm2 + cm2_price = canvas_cm2_price(cm2) + + # calculate price for canvas with frame + price = (cm2 * cm2_price) + price = max(price, 396.8) # the price can never go below 396.8 SEK + + if not framed: + price *= 0.7 + + # 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(price) \ No newline at end of file diff --git a/api/resources/prices.py b/api/resources/prices.py new file mode 100644 index 0000000..4fee3db --- /dev/null +++ b/api/resources/prices.py @@ -0,0 +1,74 @@ +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.validators import validate_territory, validate_number, validate_keys_exists + + +mod = Blueprint('prices', __name__, url_prefix='/prices') + + +@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')) + + 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 + + materials = Material.query.wallpapers().all() + data = [] + for material in materials: + data.append({ + 'material': material.name, + 'material_id': material.id, + 'price': wallpaper_price(material_price=material.price, **params), + }) + + return jsonify(data=data) + + +@mod.route('/canvas', methods=['GET']) +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 + + data = [] + + for framed in [True, False]: + data.append({ + 'framed': framed, + 'price': canvas_price(framed=framed, **params) + }) + + return jsonify(data=data) diff --git a/docs/prices.md b/docs/prices.md new file mode 100644 index 0000000..a50f3fb --- /dev/null +++ b/docs/prices.md @@ -0,0 +1,74 @@ +# Prices + +The prices resource provides a simple API to retrive price information about wallpaper and canvas products. + +## Calculate wallpaper price + +Calculate price inc VAT of a wallpaper product. The response will contain prices for all available materials. + +| Parameter | Description | +| --------- | -----| -------- | - +| `width` | Wallpaper width in cm e.g `100' | +| `height` | Wallpaper 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/wallpaper?width=100&height=50&territory=SE&dealerurl=ackes'` + +Sample response +``` +{ + "data": [ + { + "material": "standard-wallpaper", + "material_id": 1, + "price": 295 + }, + { + "material": "self-adhesive-wallpaper", + "material_id": 2, + "price": 445 + }, + { + "material": "premium-wallpaper", + "material_id": 4, + "price": 325 + } + ] +} +``` + +## Calculate canvas price + +Calculate price inc VAT of a canvas product. The response will contain information for both framed/not framed canvases. + +| 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/canvas?width=100&height=50&territory=PL` + +Sample response +``` +{ + "data": [ + { + "framed": true, + "price": 284 + }, + { + "framed": false, + "price": 249 + } + ] +} +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 008be70..537f400 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ Werkzeug==0.11.8 itsdangerous==0.24 psycopg2==2.6.1 Flask-Testing==0.5.0 -unittest2==1.1.0 \ No newline at end of file +unittest2==1.1.0 +factory-boy==2.7.0 \ No newline at end of file diff --git a/tests/factories.py b/tests/factories.py new file mode 100644 index 0000000..504d32d --- /dev/null +++ b/tests/factories.py @@ -0,0 +1,30 @@ +import factory +from api import models + + +class DesignerFactory(factory.Factory): + + class Meta: + model = models.Designer + + name = 'Spiderman' + username = 'spiderman' + territory = 'US' + currency = 'USD' + + +class ContractCustomerFactory(factory.Factory): + + class Meta: + model = models.ContractCustomer + + pricepremium = 0 + + +class InquiryFactory(factory.Factory): + + class Meta: + model = models.Inquiry + + price_retouch = None + pricepremium = 0 diff --git a/tests/test_price.py b/tests/test_price.py new file mode 100644 index 0000000..ba6d0f3 --- /dev/null +++ b/tests/test_price.py @@ -0,0 +1,66 @@ +# coding=UTF-8 + +import unittest2 +from api.lib.prices import wallpaper_price, canvas_price, canvas_cm2_price +from tests.factories import DesignerFactory, ContractCustomerFactory, InquiryFactory +from api.models import market + + +sweden = market.from_territory('SE') +sweden.price_adjustments = 1 +sweden.exchange_rate = 1 + +denmark = market.from_territory('DK') +denmark.price_adjustments = 1.185 +denmark.exchange_rate = 0.799000 + +designer = DesignerFactory() +designer.pricepremium = 17.647 +reseller = ContractCustomerFactory(pricepremium=15) + + +class TestPrice(unittest2.TestCase): + + def test_wallpaper_price(self): + self.assertEqual(295, wallpaper_price( + width=1, height=1, material_price=236, market=sweden)) + self.assertEqual(295, wallpaper_price( + width=100, height=100, material_price=236, market=sweden)) + self.assertEqual(1180, wallpaper_price( + width=200, height=200, material_price=236, market=sweden)) + self.assertEqual(325, wallpaper_price( + width=100, height=100, material_price=260, market=sweden)) + self.assertEqual(279, wallpaper_price( + width=100, height=100, material_price=236, market=denmark)) + self.assertEqual(236, wallpaper_price( + width=100, height=100, material_price=236, market=sweden, inc_vat=False)) + + # designer + self.assertEqual(347, wallpaper_price( + width=100, height=100, material_price=236, market=sweden, designer=designer)) + self.assertEqual(329, wallpaper_price( + width=100, height=100, material_price=236, market=denmark, designer=designer)) + + # reseller stores + self.assertEqual(339, wallpaper_price( + width=100, height=100, material_price=236, market=sweden, reseller=reseller)) + self.assertEqual(399, wallpaper_price(width=100, height=100, + material_price=236, market=sweden, designer=designer, reseller=reseller)) + self.assertEqual(154, wallpaper_price(width=100, height=100, material_price=236, + market=sweden, reseller=ContractCustomerFactory(pricepremium=-47.74))) + + def test_canvas_cm2_price(self): + self.assertEqual(0.22, canvas_cm2_price(250)) + self.assertEqual(0.22384, canvas_cm2_price(10)) + self.assertEqual(0.208, canvas_cm2_price(1000)) + + def test_canvas_price(self): + self.assertEqual(460, canvas_price(width=50, height=50, market=sweden, framed=True, inc_vat=False)) + self.assertEqual(960, canvas_price(width=100, height=100, market=sweden, framed=True, inc_vat=False)) + self.assertEqual(1359, canvas_price(width=150, height=120, market=sweden, framed=True, inc_vat=False)) + self.assertEqual(256000, canvas_price(width=2000, height=2000, market=sweden, framed=True, inc_vat=False)) + self.assertEqual(397, canvas_price(width=10, height=10, market=sweden, framed=True, inc_vat=False)) + self.assertEqual(672, canvas_price(width=100, height=100, market=sweden, framed=False, inc_vat=False)) + self.assertEqual(840, canvas_price(width=100, height=100, market=sweden, framed=False, inc_vat=True)) + + self.assertEqual(436, canvas_price(width=50, height=50, market=denmark, framed=True, inc_vat=False))