Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Hosted Payment Pages (Details) #32

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
13 changes: 13 additions & 0 deletions adyen/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class Constants:
VALUE = 'value'

# Payment related constants ---
PAYMENT_BRAND_CODE = 'brandCode'
PAYMENT_ISSUER_ID = 'issuerId'

PAYMENT_AMOUNT = 'paymentAmount'
PAYMENT_METHOD = 'paymentMethod'
Expand Down Expand Up @@ -105,3 +107,14 @@ class Constants:
SHOPPER_BIRTH_MONTH = 'shopper.dateOfBirthMonth'
SHOPPER_BIRTH_YEAR = 'shopper.dateOfBirthYear'
SHOPPER_PHONE = 'shopper.telephoneNumber'

INVOICE_NUMLINES = 'openinvoicedata.numberOfLines'
INVOICE_SIG = 'openinvoicedata.sig'
INVOICE_LINE_CURRENCY = 'openinvoicedata.line%d.currencyCode'
INVOICE_LINE_DESCRIPTION = 'openinvoicedata.line%d.description'
INVOICE_LINE_ITEMAMOUNT = 'openinvoicedata.line%d.itemAmount'
INVOICE_LINE_ITEMVATAMOUNT = 'openinvoicedata.line%d.itemVatAmount'
INVOICE_LINE_ITEMVATPERCENTAGE = 'openinvoicedata.line%d.itemVatPercentage'
INVOICE_LINE_LINEREFERENCE = 'openinvoicedata.line%d.lineReference'
INVOICE_LINE_NUMBEROFITEMS = 'openinvoicedata.line%d.numberOfItems'
INVOICE_LINE_VATCATEGORY = 'openinvoicedata.line%d.vatCategory'
8 changes: 7 additions & 1 deletion adyen/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,16 @@ def check_fields(self):

# Check that all mandatory fields are present.
for field_name in self.REQUIRED_FIELDS:
if not params.get(field_name):
if field_name not in params:
raise MissingFieldException(
"The %s field is missing" % field_name
)

# Check that no unexpected field is present.
expected_fields = self.REQUIRED_FIELDS + self.OPTIONAL_FIELDS
for field_name in params.keys():
if field_name.startswith('openinvoicedata.'):
continue
if field_name not in expected_fields:
raise UnexpectedFieldException(
"The %s field is unexpected" % field_name
Expand Down Expand Up @@ -107,6 +109,9 @@ class PaymentFormRequest(BaseInteraction):
Constants.MERCHANT_RETURN_DATA,
Constants.OFFSET,

Constants.PAYMENT_BRAND_CODE,
Constants.PAYMENT_ISSUER_ID,

Constants.DELIVERY_SIG,
Constants.DELIVERY_ADDRESS_TYPE,
Constants.DELIVERY_STREET,
Expand Down Expand Up @@ -142,6 +147,7 @@ class PaymentFormRequest(BaseInteraction):
Constants.SHOPPER_BIRTH_MONTH,
Constants.SHOPPER_BIRTH_YEAR,
Constants.SHOPPER_PHONE,

)

def __init__(self, client, params=None):
Expand Down
87 changes: 79 additions & 8 deletions adyen/scaffold.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.utils import timezone
from oscar.core.loading import get_class
from decimal import Decimal

from .config import get_config

Expand Down Expand Up @@ -141,12 +142,27 @@ def get_field_specs(self, request, order_data):
field_specs.update(
self.get_fields_billing(request, order_data))

if 'brand_code' in order_data:
field_specs[Constants.PAYMENT_BRAND_CODE] = (
order_data['brand_code']
)
try:
field_specs[Constants.PAYMENT_ISSUER_ID] = (
order_data['issuer_id']
)
except KeyError:
raise MissingFieldException(
"Fields issuer_id missing from the order data.")

if 'order' in order_data:
field_specs.update(
self.get_fields_invoice(request, order_data))

return {
key: sanitize_field(value)
for key, value in field_specs.items()
}


def get_field_allowed_methods(self, request, order_data):
"""Get a string of comma separated allowed payment methods.

Expand Down Expand Up @@ -177,7 +193,7 @@ def get_field_allowed_methods(self, request, order_data):
allowed_methods = self.config.get_allowed_methods(request,
source_type)
except NotImplementedError:
# New in version 0.6.0: this may not work properly with existing
# New in version 0.6.0: this may not work properly with existing
# application using this plugin. We make sure not to break here
# and keep this plugin backward-compatible with version 0.5.
return None
Expand Down Expand Up @@ -236,6 +252,18 @@ def get_fields_shopper(self, request, order_data):

return fields

def get_street_housenr(self, address):
words = [l for l in [address.line1, address.line2, address.line3] if l]
numbers = [i for i, token in enumerate(words) if str.isdigit(token)]
if numbers:
offset = numbers[0]
housenr = ' '.join(words[offset:])
street = ' '.join(words[:offset])
else:
housenr = words[-1]
street = ' '.join(words[:-1])
return (street, housenr)

def get_fields_delivery(self, request, order_data):
"""Extract and return delivery related fields from ``order_data``.

Expand All @@ -245,12 +273,11 @@ def get_fields_delivery(self, request, order_data):
"""
shipping = order_data['shipping_address']

street = ' '.join([
l for l in [shipping.line1, shipping.line2, shipping.line3] if l])
street, housenr = self.get_street_housenr(shipping)

fields = {
Constants.DELIVERY_STREET: street,
Constants.DELIVERY_NUMBER: '.',
Constants.DELIVERY_NUMBER: housenr,
Constants.DELIVERY_CITY: shipping.line4,
Constants.DELIVERY_POSTCODE: shipping.postcode,
Constants.DELIVERY_STATE: shipping.state or '',
Expand All @@ -272,12 +299,11 @@ def get_fields_billing(self, request, order_data):
"""
billing = order_data['billing_address']

street = ' '.join([
l for l in [billing.line1, billing.line2, billing.line3] if l])
street, housenr = self.get_street_housenr(billing)

fields = {
Constants.BILLING_STREET: street,
Constants.BILLING_NUMBER: '.',
Constants.BILLING_NUMBER: housenr,
Constants.BILLING_CITY: billing.line4,
Constants.BILLING_POSTCODE: billing.postcode,
Constants.BILLING_STATE: billing.state or '',
Expand All @@ -290,6 +316,51 @@ def get_fields_billing(self, request, order_data):

return fields

def get_fields_invoice(self, request, order_data):
order = order_data['order']

def minor_units(amount):
return int((Decimal(amount) * 100).quantize(Decimal('1')))

fields = {
Constants.INVOICE_NUMLINES: order.lines.count(),
}

check = 0
for index, line in enumerate(order.lines.all()):
ref = index + 1
perc_tax = minor_units(line.unit_tax_rate)
excl_tax = minor_units(
line.line_price_excl_tax / line.quantity
)
incl_tax = minor_units(
line.line_price_incl_tax / line.quantity
)
tax = incl_tax - excl_tax

if perc_tax > 1000:
vat_category = 'High' # or 'Low' or 'None'
elif perc_tax > 100:
vat_category = 'Low'
else:
vat_category = 'None'

fields.update({
Constants.INVOICE_LINE_LINEREFERENCE % ref: ref,
Constants.INVOICE_LINE_CURRENCY % ref: order.currency,
Constants.INVOICE_LINE_DESCRIPTION % ref: line.product.get_title(),
Constants.INVOICE_LINE_ITEMAMOUNT % ref: str(excl_tax),
Constants.INVOICE_LINE_ITEMVATAMOUNT % ref: str(tax),
Constants.INVOICE_LINE_ITEMVATPERCENTAGE % ref: str(perc_tax),
Constants.INVOICE_LINE_NUMBEROFITEMS % ref: str(line.quantity),
Constants.INVOICE_LINE_VATCATEGORY % ref: vat_category,
})
check += (int(excl_tax) + int(tax)) * line.quantity

check = str(check)
assert check == order_data['amount']
return fields

def handle_payment_feedback(self, request):
"""Handle payment feedback from return URL or POST notification.

Expand Down
5 changes: 4 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ known_first_party = adyen, tests
multi_line_output = 3
not_skip = __init__.py
skip = migrations, settings
line_length = 79
line_length = 100

[flake8]
max-line-length=99
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

setup(
name='django-oscar-adyen',
version='0.7.1',
version='0.7.6',
url='https://github.com/oscaro/django-oscar-adyen',
author='Oscaro',
description='Adyen HPP payment module for django-oscar',
Expand Down
10 changes: 7 additions & 3 deletions tests/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@
{'type': 'hidden', 'name': 'currencyCode', 'value': 'EUR'},
{'type': 'hidden', 'name': 'merchantAccount', 'value': settings.ADYEN_IDENTIFIER},
{'type': 'hidden', 'name': 'merchantReference', 'value': '00000000123'},
{'type': 'hidden', 'name': 'merchantReturnData', 'value': 123},
{'type': 'hidden', 'name': 'merchantReturnData', 'value': '123'},
{'type': 'hidden', 'name': 'merchantSig', 'value': 'kKvzRvx7wiPLrl8t8+owcmMuJZM='},
{'type': 'hidden', 'name': 'paymentAmount', 'value': 123},
{'type': 'hidden', 'name': 'paymentAmount', 'value': '123'},
{'type': 'hidden', 'name': 'resURL', 'value': TEST_RETURN_URL},
{'type': 'hidden', 'name': 'sessionValidity', 'value': '2014-07-31T17:20:00Z'},
{'type': 'hidden', 'name': 'shipBeforeDate', 'value': '2014-08-30'},
{'type': 'hidden', 'name': 'shopperEmail', 'value': '[email protected]'},
{'type': 'hidden', 'name': 'shopperLocale', 'value': 'fr'},
{'type': 'hidden', 'name': 'shopperReference', 'value': 789},
{'type': 'hidden', 'name': 'shopperReference', 'value': '789'},
{'type': 'hidden', 'name': 'skinCode', 'value': 'cqQJKZpg'},
{'type': 'hidden', 'name': 'countryCode', 'value': 'fr'},
{'type': 'hidden', 'name': 'brandCode', 'value': 'ideal'},
{'type': 'hidden', 'name': 'issuerId', 'value': '1211'},
]

ORDER_DATA = {
Expand All @@ -36,6 +38,8 @@
'order_number': '00000000123',
'return_url': TEST_RETURN_URL,
'shopper_locale': 'fr',
'brand_code': 'ideal',
'issuer_id': '1211',
}


Expand Down
12 changes: 6 additions & 6 deletions tests/test_scaffold.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_get_fields_delivery(self):
address = ShippingAddress(
first_name='First Name',
last_name='Last Name',
line1='First Line Address',
line1='First Line Address 1',
line4='Bruxelles',
postcode='1000',
country_id='BE')
Expand All @@ -32,8 +32,8 @@ def test_get_fields_delivery(self):
assert Constants.DELIVERY_STATE in fields
assert Constants.DELIVERY_COUNTRY in fields

assert fields[Constants.DELIVERY_STREET] == address.line1
assert fields[Constants.DELIVERY_NUMBER] == '.', (
assert fields[Constants.DELIVERY_STREET] == 'First Line Address'
assert fields[Constants.DELIVERY_NUMBER] == '1', (
'Since Oscar does not provide a street number we set a fake value')
assert fields[Constants.DELIVERY_CITY] == address.city
assert fields[Constants.DELIVERY_POSTCODE] == address.postcode
Expand All @@ -46,7 +46,7 @@ def test_get_fields_billing(self):
address = BillingAddress(
first_name='First Name',
last_name='Last Name',
line1='First Line Address',
line1='First Line Address 1',
line4='Bruxelles',
postcode='1000',
country_id='BE')
Expand All @@ -63,8 +63,8 @@ def test_get_fields_billing(self):
assert Constants.BILLING_STATE in fields
assert Constants.BILLING_COUNTRY in fields

assert fields[Constants.BILLING_STREET] == address.line1
assert fields[Constants.BILLING_NUMBER] == '.', (
assert fields[Constants.BILLING_STREET] == 'First Line Address'
assert fields[Constants.BILLING_NUMBER] == '1', (
'Since Oscar does not provide a street number we set a fake value')
assert fields[Constants.BILLING_CITY] == address.city
assert fields[Constants.BILLING_POSTCODE] == address.postcode
Expand Down