Source code for verifone.verifone

# -*- coding: utf-8 -*-

"""Main module."""
import logging
from datetime import datetime
from random import randrange
from Crypto.PublicKey import RSA
from Crypto.Hash import SHA512, SHA, SHA256
from Crypto.Signature import PKCS1_v1_5
import binascii
import requests
import pycountry

try:
    import urllib.parse
except ImportError:
    import urllib

logs = logging.getLogger(__name__)


[docs]class Verifone(object): """ Class for Verifone payment API. Following packages need to be installed: - pycryptodome - pycountry - requests :param agreement_code: merchant agreement code, string with length of 1-36 characters :param RSA_private_key: RSA private key, string :param RSA_verifone_public_key: Verifone's RSA public key, string :param currency: currency code with three letters, default is EUR, string :param software_name: name of the web shop software, string with length of 1-30 characters :param version: version of the web shop software, string with length of 1-10 characters :param interface_version: version of the payment interface, string with length of 1-11 numeric characters :param test_mode: 1 if use test server, default is 0, boolean :param return_error_dict: if 1 then returns data received from Verifone also when error occurs. If value is 0, then raise an error. Default is 0, boolean :rtype: object """ _default_currency = 'EUR' def __init__(self, agreement_code, RSA_private_key, RSA_verifone_public_key, software_name, version, currency=_default_currency, interface_version='5', test_mode=0, return_error_dict=0): """ Initialize Verifone client. """ currency = self.check_currency(currency) self._agreement_code = agreement_code self._RSA_private_key = RSA_private_key self._RSA_verifone_public_key = RSA_verifone_public_key self._software_name = software_name self._version = version self._interface_version = interface_version self._currency = currency self._test_mode = test_mode self._return_error_dict = return_error_dict @property def endpoint(self): """ Return endpoint. :return: endpoint, string """ if self._test_mode: return 'https://epayment.test.point.fi/pw/serverinterface' return 'https://epayment1.point.fi/pw/serverinterface' @property def endpoint2(self): """ Return endpoint (node2). Verifone has 2 production environment URLs so if first one does not answer, this can be used instead. :return: endpoint, string """ if self._test_mode: return 'https://epayment.test.point.fi/pw/serverinterface' return 'https://epayment2.point.fi/pw/serverinterface' @property def posturl1(self): """ Return post url (Verifone node 1). :return: post url, string """ if self._test_mode: return 'https://epayment.test.point.fi/pw/payment' return 'https://epayment1.point.fi/pw/payment' @property def posturl2(self): """ Return post url (Verifone node 2). :return: post url, string """ if self._test_mode: return 'https://epayment.test.point.fi/pw/payment' return 'https://epayment2.point.fi/pw/payment' @property def posturl(self): """ Return post url. If production mode is used, then check that endpoint is available. If not use another endpoint. :return: post url, string """ if self._test_mode: return 'https://epayment.test.point.fi/pw/payment' url = self.posturl1 response = requests.post(url) logs.debug("Ping response from Verifone: %s", response) if response.status_code != 200: url = self.posturl2 response = requests.post(url) logs.debug("Ping response from Verifone: %s", response) return url @property def test_mode(self): """ Return 1 if client is in test mode. :return: 1 if is in test mode, integer """ return self._test_mode @test_mode.setter def test_mode(self, value): """ Method saves a new test_mode value. :param value: test mode, value can be 1 (test mode) or 0 (production), integer :return: None """ if not value: value = 0 elif value != 0 and value != 1: logs.debug("Wrong value for test mode") raise ValueError("Test mode can be 1 or 0") self._test_mode = value @property def currency(self): """ Method returns currency code. :return: currency, numeric ISO 4217 currency code """ try: currency_data = pycountry.currencies.get(alpha_3 = self._currency) except KeyError: logs.debug("Wrong currency saved: " + self._currency) raise ValueError("Incorrect currency") if currency_data is None: raise ValueError("Incorrect currency") return currency_data.numeric @currency.setter def currency(self, value): """ Method saves a new currency code if value is defined and it is in a correct format. :param value: new currency code, with three letters :return: None """ if not value: logs.debug("Mandatory currency can not be empty.") raise ValueError("Mandatory currency can not be empty") elif len(value) != 3: logs.debug("The length of the currency code is not 3.") raise ValueError("The length of the currency code is not 3") elif not value.isalpha(): logs.debug("Only letters are allowed.") raise ValueError("Only letters are allowed") value = value.upper() try: pycountry.currencies.get(alpha_3 = value) except: raise ValueError("Incorrect currency") self._currency = value
[docs] def check_currency(self, currency): """ Method checks that currency is a valid currency code. If not then default currency code is used. :param currency: new currency code, with three letters :return: currency code, with three letters """ if (not currency) or (len(currency) != 3) or (not currency.isalpha()): logs.debug("Default currency is used instead of " + currency) currency = self._default_currency currency = currency.upper() try: pycountry.currencies.get(alpha_3 = currency) except KeyError: logs.debug("Default currency is used instead of " + currency) currency = self._default_currency return currency
[docs] def is_available(self): """ Method can be used to test connectivity, signatures and requesting availability status of server interface. :return: response from Verifone, dictionary - i-f-1-1_availability: 0 = No access to Server Interface has been granted by Verifone 1 = Express level access to Server Interface 2 = Advanced level access to Server Interface """ options = { "s-f-1-30_operation": "is-available", } return self.send_request(options)
[docs] def list_payment_methods(self): """ Method gets available payment methods and their amount limits. :return: available payment methods, dictionary - s-t-1-30_payment-method-code-<N>: the payment method code - l-t-1-20_payment-method-min-<N>: minimum amount for the payment method - l-t-1-20_payment-method-max-<N>: maximum amount for the payment method - s-t-1-30_payment-method-type-<N>: type of the payment method """ options = { "s-f-1-30_operation": "list-payment-methods", "i-f-1-3_currency-code": self.currency, } return self.send_request(options)
[docs] def cancel_payment(self, data): """ Method cancels the payment :param data: data for cancelling, dictionary - s-f-1-30_payment-method-code: the used payment method code, string - l-f-1-20_transaction-number: transaction number identifying the payment transaction, string - s-t-1-1024_dynamic-feedback: list of parameters to be added to response if available (optional) :return: status for cancelling, dictionary """ options = { "s-f-1-30_operation": "cancel-payment", } # merge data to request options options.update(data) return self.send_request(options)
[docs] def list_saved_payment_methods(self, data): """ Method gets saved payment methods for given buyer. :param data: data for search saved payment methods, dictionary - s-f-1-30_buyer-first-name: first name - s-f-1-30_buyer-last-name: last name - s-f-1-100_buyer-email-address: email address - s-t-1-30_buyer-phone-number: phone number (optional) - s-t-1-255_buyer-external-id: buyer's external id (optional) - s-t-1-30_delivery-address-line-one: street, 1-30 characters (optional) - s-t-1-30_delivery-address-line-two: delivery address extension, 1-30 characters (optional) - s-t-1-30_delivery-address-line-three: delivery address extension, 1-30 characters (optional) - s-t-1-30_delivery-address-city: city, 1-30 characters (optional) - s-t-1-30_delivery-address-postal-code: postal code, 1-30 characters (optional) - i-t-1-3_delivery-address-country-code: country code with 1-3 characters, ISO 3166 (optional) - s-t-1-30_recurring-payment-subscription: subscription code for recurring payment, 1-30 characters (optional) :return: available payment methods, dictionary - s-t-1-30_payment-method-code-<N>: the payment method code - l-t-1-20_payment-method-id-<N>: payment method id - s-t-1-30_payment-method-title-<N>: title for the payment method - s-t-1-6_card-expected-validity-<N>: card validity """ options = { "s-f-1-30_operation": "list-saved-payment-methods", } # merge data to request options options.update(data) return self.send_request(options)
[docs] def remove_saved_payment_method(self, payment_id): """ Method removes saved payment method. :param payment_id: payment method id, integer :return: status for removing, dictionary - l-t-1-10_removed-count: 0 = Payment method not removed, 1 = Payment method removed """ options = { "s-f-1-30_operation": "remove-saved-payment-method", "l-t-1-20_saved-payment-method-id": payment_id, } return self.send_request(options)
[docs] def refund_payment(self, data): """ Method refunds payment. Refund is supported for card, electronic and invoice payments. :param data: data for refunding, dictionary - l-f-1-20_refund-amount: amount to refund, integer - s-f-1-30_payment-method-code: the used payment method code, string - l-f-1-20_transaction-number: transaction number identifying the payment transaction, string - s-t-1-36_order-note: note for the payment, 1-36 characters (optional) - s-t-1-1024_dynamic-feedback: list of parameters to be added to response if available (optional) :return: status for refunding, dictionary """ options = { "s-f-1-30_operation": "refund-payment", "i-f-1-3_refund-currency-code": self.currency, } # merge data to request options options.update(data) return self.send_request(options)
[docs] def process_payment(self, data): """ Method sends payment data to Verifone. :param data: data added to payment request, dictionary - locale-f-2-5_payment-locale: locale for the customer, string - t-f-14-19_payment-timestamp: UTC timestamp defining the payment start time, format is yyyy-MM-dd HH:mm:ss, string (optional) - s-f-1-36_order-number: order number, length is 1-36 characters, string - t-f-14-19_order-timestamp: UTC time defining orders time in format yyyy-MM-dd HH:mm:ss, string (optional) - s-t-1-36_order-note: note, 1-36 characters (optional) - l-f-1-20_order-gross-amount: total amount including taxes and discount with two decimal precision (for example 100 means 1 EUR), integer - s-f-1-30_buyer-first-name: the first name of the customer with 1-30 characters, string - s-f-1-30_buyer-last-name: the last name of the customer with 1-30 characters, string - s-t-1-30_buyer-phone-number: the phone number of the customer with 1-30 characters, string (optional) - s-f-1-100_buyer-email-address: the email of the customer with 1-100 characters, string - s-t-1-255_buyer-external-id: identifier for the customer with 1-255 characters, string (optional) - s-t-1-30_delivery-address-line-one: delivery address, string (optional) - s-t-1-30_delivery-address-line-two: delivery address, string (optional) - s-t-1-30_delivery-address-line-three: delivery address, string (optional) - s-t-1-30_delivery-address-city: city for the delivery address, string (optional) - s-t-1-30_delivery-address-postal-code: postal code for the delivery address, string (optional) - i-t-1-3_delivery-address-country-code: country code of the delivery address, string with 2 character or numeric ISO 3166 (optional) - l-t-1-20_saved-payment-method-id: ID of the saved payment method with 1-20 characters, string (optional) - i-t-1-1_recurring-payment: is payment recurring payment, integer (optional) 0: Not recurring payment 1: Recurring payment - i-t-1-1_deferred-payment: is payment deferred payment, integer (optional) 0: Not deferred payment 1: Deferred payment - s-f-1-30_payment-method-code: used payment method, string - 0-50 basket items are supported :return: payment information returned from Verifone, dictionary """ options = { "s-f-1-30_operation": "process-payment", "i-f-1-3_order-currency-code": self.currency, } current_datetime = datetime.utcnow() timestamp = current_datetime.strftime('%Y-%m-%d %H:%M:%S') if not 't-f-14-19_payment-timestamp' in data: data['t-f-14-19_payment-timestamp'] = timestamp if not 't-f-14-19_order-timestamp' in data: data['t-f-14-19_order-timestamp'] = timestamp if 'i-t-1-3_delivery-address-country-code' in data and data['i-t-1-3_delivery-address-country-code'].isalpha(): country_data = pycountry.countries.get(alpha_2 = data['i-t-1-3_delivery-address-country-code']) data['i-t-1-3_delivery-address-country-code'] = country_data.numeric self.check_product_names(data) # merge data to request options options.update(data) return self.send_request(options)
[docs] def process_supplementary(self, data): """ Method trigger process supplementary. :param data: data added request, dictionary - l-f-1-20_original-transaction-number: transaction number for which supplementary will be initiated - s-f-1-30_payment-method-code: payment method code - l-f-1-20_order-gross-amount: amount for which supplementary auth will be triggered - s-t-1-36_order-note: note (optional) :return: information returned from Verifone, dictionary - l-t-1-20_transaction-number: transaction number - s-t-1-30_payment-method-code: payment method code - i-t-1-3_order-currency-code: currency code - l-t-1-20_order-gross-amount: gross amount including tax with two decimal """ options = { "s-f-1-30_operation": "process-supplementary", "i-f-1-3_order-currency-code": self.currency, } # merge data to request options options.update(data) return self.send_request(options)
[docs] def get_payment_status(self, data): """ Method gets payment status. :param data: data for status inquiry, dictionary - s-f-1-30_payment-method-code: the used payment method code, string - l-f-1-20_transaction-number: transaction number identifying the payment transaction, string :return: status for the payment, dictionary """ options = { "s-f-1-30_operation": "get-payment-status", } # merge data to request options options.update(data) return self.send_request(options)
[docs] def list_transaction_numbers(self, order_number): """ Method lists transaction numbers of one order. :param order_number: order number, string :return: transaction information, dictionary - l-f-1-20_transaction-number-<N>: transaction number - s-f-1-30_payment-method-code-<N>: payment method code """ options = { "s-f-1-30_operation": "list-transaction-numbers", "s-f-1-36_order-number": order_number, } return self.send_request(options)
[docs] def generate_payment_data(self, data): """ Method generates payment data which can be used in html form for showing payment button. :param data: data for the new payment, dictionary - order_number: order number, string - locale: locale for the customer. Supported: fi_FI, sv_SE, no_NO, dk_DK, sv_FI and en_GB. Other are redirected to en_GB - amount_gross: total amount including taxes with two decimal - amount_net: total amount without taxes, with two decimal - vat_amount: tax amount with two decimal - first_name: customer's first name, 1-30 characters, string - last_name: customer's last name, 1-30 characters, string - email: customer's email, 1-100 characters, string - phone: customer's phone number, max length is 30 character (optional) - address: line one of the delivery address, max length is 30 character (optional) - address2: line two of the delivery address, max length is 30 character (optional) - address3: line three of the delivery address, max length is 30 character (optional) - city: city of the delivery address, max length is 30 character (optional) - postal_code: postal code of the delivery address, max length is 30 character (optional) - country: country of the delivery address, with 2 letters or numeric ISO 3166 country code (optional) - style: code for the style sheet used in payment page, max length is 30 character (optional) - cancel_url: return URL if payment is cancelled, max length is 256 characters - error_url: return URL after error, max length is 256 characters - expired_url: return URL after expired situation, max length is 256 characters - rejected_url: return URL if payment is rejected, max length is 256 characters - success_url: return URL after success payment, max length is 256 characters - success_url_server: URL of the delayed success, max length is 256 characters - note: custom parameter reserved for shop system to use, max length is 36 characters (optional) - save_method: is customer's payment method saved (optional) 0: normal payment. If customer uses credit card, 'save payment' button is shown on Verifone's page. 1: payment method is saved if payment is succesful. If payment method is not given in request, only cards are shown to customer. 2: payment method is only saved, so any payment is not charged. 3: disable save payment options - customer_id: identifier for the customer (optional) - payment_method: selected payment method (optional) - saved_payment_method_id: ID of the saved payment method (optional) - products: product data, there can be 1-50 basket items - name: name of the basket item, max length is 30 character - pieces: number of units in the item, integer or float - discount: item discount percentage tax with two decimal, float or integer - vat: tax percentage with two decimal, float or integer - amount_gross: item gross amount including tax and discount with two decimal, float or integer - amount_net: item net amount calculated from unit cost times unit count with two decimal, float or integer - unit_cost_gross: unit cost with two decimal, with discount and tax - unit_cost: unit cost with two decimal and without tax and discounts, this must be filled if unit gross cost is not filled, otherwise must not be used (optional) - payment_timestamp: the payment start time, format is yyyy-MM-dd HH:mm:ss (optional) - order_timestamp: the orders time from web shop point of view, format is yyyy-MM-dd HH:mm:ss (optional) - dynamic_feedback: comma separated list of optional parameters to be added to the response if available, string (optional) :return: generated payment data, dictionary """ if data: current_datetime = datetime.utcnow() timestamp = current_datetime.strftime('%Y-%m-%d %H:%M:%S') if not 'payment_timestamp' in data: data['payment_timestamp'] = timestamp if not 'order_timestamp' in data: data['order_timestamp'] = timestamp payment_token = self.generate_token(data['order_number'], data['payment_timestamp']) if 'style' in data: style_code = data['style'] else: style_code = '' if 'skip_confirmation' in data: skip_confirmation = data['skip_confirmation'] else: skip_confirmation = 0 values = { 's-f-32-32_payment-token': payment_token, 'locale-f-2-5_payment-locale': data['locale'], 't-f-14-19_payment-timestamp': data['payment_timestamp'], 't-f-14-19_order-timestamp': data['order_timestamp'], 's-f-1-36_merchant-agreement-code': self._agreement_code, 's-f-1-36_order-number': data['order_number'], 'i-f-1-3_order-currency-code': self.currency, 'i-t-1-4_order-vat-percentage': '', # set empty because there can be multiple VAT percentages in one payment 's-f-1-30_buyer-first-name': self.get_substring(data['first_name'], 30), 's-f-1-30_buyer-last-name': self.get_substring(data['last_name'], 30), 's-f-1-100_buyer-email-address': self.get_substring(data['email'], 100), 's-t-1-30_style-code': style_code, 's-f-5-256_cancel-url':data['cancel_url'], 's-f-5-256_error-url': data['error_url'], 's-f-5-256_expired-url': data['expired_url'], 's-f-5-256_rejected-url': data['rejected_url'], 's-f-5-256_success-url': data['success_url'], 's-t-5-256_change-server-to-server-success-url': data['success_url_server'], 's-f-1-30_software': self._software_name, 's-f-1-10_software-version': self._version, 'i-f-1-11_interface-version': self._interface_version, 'i-t-1-1_skip-confirmation-page': skip_confirmation, } if 'amount_gross' in data: values['l-f-1-20_order-gross-amount'] = self.format_to_integer(data['amount_gross']) if 'amount_net' in data: values['l-f-1-20_order-net-amount'] = self.format_to_integer(data['amount_net']) if 'vat_amount' in data: values['l-f-1-20_order-vat-amount'] = self.format_to_integer(data['vat_amount']) # Check optional fields, max length is 30 characters extra_fields = { 'phone': 's-t-1-30_buyer-phone-number', 'address': 's-t-1-30_delivery-address-line-one', 'address2': 's-t-1-30_delivery-address-line-two', 'address3': 's-t-1-30_delivery-address-line-three', 'city': 's-t-1-30_delivery-address-city', 'postal_code': 's-t-1-30_delivery-address-postal-code', 'save_method': 'i-t-1-1_save-payment-method', 'payment_method': 's-t-1-30_payment-method-code', } for key, field in extra_fields.items(): if key in data: values[field] = self.get_substring(data[key], 30) if 'saved_payment_method_id' in data: values['l-t-1-20_saved-payment-method-id'] = self.get_substring(data['saved_payment_method_id'], 64) if 'country' in data: if data['country'].isalpha(): country_data = pycountry.countries.get(alpha_2 = data['country'].upper()) values['i-t-1-3_delivery-address-country-code'] = country_data.numeric else: values['i-t-1-3_delivery-address-country-code'] = data['country'] if 'customer_id' in data: values['s-t-1-255_buyer-external-id'] = self.get_substring(data['customer_id'], 255) if 'note' in data: values['s-t-1-36_order-note'] = self.get_substring(data['note'], 36) if 'dynamic_feedback' in data: values['s-t-1-1024_dynamic-feedback'] = self.get_substring(data['dynamic_feedback'], 1024) products = self.build_product_data(data['products']) values.update(products) # get signatures signature = self.generate_signature(values, 'SHA1') signature2 = self.generate_signature(values, 'SHA512') values['s-t-256-256_signature-one'] = signature values['s-t-256-256_signature-two'] = signature2 logs.debug("Values for payment: %s", values) return values
[docs] def send_request(self, options): """ Method sends a request to Verifone and returns response. :param options: options for the request, dictionary :return: response from Verifone, dictionary """ current_time = datetime.now() timestamp = current_time.strftime('%Y-%m-%d %H:%M:%S') request_id = str(current_time.strftime('%Y%m%d%H%M%S'))+str(randrange(99999)) data = { "l-f-1-20_request-id": request_id, "t-f-14-19_request-timestamp": timestamp, "s-f-1-36_merchant-agreement-code": self._agreement_code, "s-f-1-30_software": self._software_name, "s-f-1-10_software-version": self._version, "i-f-1-11_interface-version": self._interface_version, } # merge options to request data data.update(options) # get signatures signature = self.generate_signature(data, 'SHA1') signature2 = self.generate_signature(data, 'SHA512') data['s-t-256-256_signature-one'] = signature data['s-t-256-256_signature-two'] = signature2 logs.debug("Data for Verifone request: %s", data) headers = { 'Content-Type': 'application/x-www-form-urlencoded', } url = self.get_endpoint_url() logs.debug('URL: ' + url) response = requests.post(url, headers=headers, data=data) logs.debug("Response from Verifone: %s", response) logs.debug("Content of Verifone response: %s", response.content) if response.status_code != 200: raise ConnectionError(str(response.content)) elif not response.content: raise ValueError("No content returned") parsed_response = self.parse_response(response.content) if 's-f-1-30_error-message' in parsed_response: if self._return_error_dict: return parsed_response else: raise ValueError(parsed_response['s-f-1-30_error-message']) self.verify_response(parsed_response) return parsed_response
[docs] def get_endpoint_url(self): """ Method returns endpoint url for server to server calls. Verifone has 2 production environment so check first which one production server is available. :return: endpoint url, string """ url = self.endpoint response = requests.post(url) logs.debug("Ping response from Verifone: %s", response) if response.status_code != 200: url = self.endpoint2 response = requests.post(url) logs.debug("Ping response from Verifone: %s", response) return url
[docs] def generate_signature(self, data, signature_type): """ Method generates digital signature from the given values. Algorithm is RSA with SHA1 or RSA with SHA512 :param data: data for the request, dictionary :param signature_type: signature type, value can be "sha1" or "sha512, string :return: signature, 128 byte signature converted to upper case hexadecimal string """ private_key = RSA.importKey(self._RSA_private_key) plaintext = self.get_plaintext(data) digest = '' if signature_type == 'SHA512': digest = SHA512.new(plaintext) elif signature_type == 'SHA1': digest = SHA.new(plaintext) else: raise ValueError('WrongSignatureType') signer = PKCS1_v1_5.new(private_key) signature = signer.sign(digest) hex_signature = binascii.hexlify(signature) return hex_signature.upper()
[docs] def get_plaintext(self, data): """ Method creates plaintext from dictionary. Algorithm is RSA with SHA1 or RSA with SHA512 :param data: data for the request, dictionary :return: plain text, string """ name_key_pairs = [] # needed format is "key=value" for key, value in (sorted(data.items())): name_key_pairs.append(key + "=" + str(value)) logs.debug("Names and keys for signature: %s", name_key_pairs) plaintext = ';'.join(name_key_pairs) + ';' plaintext = plaintext.encode('utf-8') logs.debug("Plaintext for signature: " + str(plaintext)) return plaintext
[docs] def parse_response(self, content): """ Method parses response content returned from Verifone. :param content: response from Verifone, string :return: result, result from Verifone in dictionary """ logs.debug("Content before parsing: %s", content) params = content.decode().split('&') result = {} for param in params: key, value = param.split('=') value = urllib.parse.unquote_plus(value) # for example times are in format: 2018-08-03+06%3A59%3A52 result[key] = value logs.debug("Parsed content: %s", result) return result
[docs] def generate_token(self, order_no, payment_timestamp): """ Method generates token for the payment. :param order_no: order number, string :param payment_timestamp: payment time stamp, string :return: generated token, string """ elements = (self._agreement_code, order_no, payment_timestamp) plaintext = ';'.join(elements) logs.debug("Plaintext for generate token: %s", plaintext) plaintext = plaintext.encode('utf-8') token = SHA256.new(plaintext) token = token.hexdigest() logs.debug("Token: %s", token) return self.get_substring(token, 32).upper()
[docs] def get_substring(self, string, length): """ Method returns substring from string if string is too length. :param string: string which length is checked, string :param length: max length, integer :return: substring, string """ if type(string) != str: string = str(string) if len(string) > length: return string[0:length] return string
[docs] def format_to_integer(self, number): """ Method formats number: 1.23 > 123. :param number: float or integer :return: formatted, integer """ return int(round(number * 100))
[docs] def build_product_data(self, data): """ Method build and returns product data. :param data: product items, dictionary - name: name of the basket item, max length is 30 character - pieces: number of units in the item, integer or float - discount: item discount percentage tax with two decimal, float or integer - vat: tax percentage with two decimal, float or integer - amount_gross: item gross amount including tax and discount with two decimal, float or integer - amount_net: item net amount calculated from unit cost times unit count with two decimal, float or integer - unit_cost_gross: unit cost with two decimal, with discount and tax, this must be filled if unit cost is not filled, otherwise must not be used (optional) - unit_cost: unit cost with two decimal and without tax and discounts, this must be filled if unit gross cost is not filled, otherwise must not be used (optional) :return: product data, dictionary """ product_data = {} for i in range(len(data)): if 'discount' in data[i]: discount = data[i]['discount'] else: discount = 0 product_data['s-t-1-30_bi-name-'+ str(i)] = self.get_substring(data[i]['name'], 30) product_data['i-t-1-11_bi-unit-count-'+ str(i)] = data[i]['pieces'] product_data['i-t-1-4_bi-vat-percentage-'+ str(i)] = self.format_to_integer(data[i]['vat']) product_data['i-t-1-4_bi-discount-percentage-'+ str(i)] = self.format_to_integer(discount) if 'amount_net' in data[i]: product_data['l-t-1-20_bi-net-amount-'+ str(i)] = self.format_to_integer(data[i]['amount_net']) if 'amount_gross' in data[i]: product_data['l-t-1-20_bi-gross-amount-'+ str(i)] = self.format_to_integer(data[i]['amount_gross']) if 'unit_cost_gross' in data[i]: product_data['l-t-1-20_bi-unit-gross-cost-'+ str(i)] = self.format_to_integer(data[i]['unit_cost_gross']) if 'unit_cost' in data[i]: product_data['l-t-1-20_bi-unit-cost-'+ str(i)] = self.format_to_integer(data[i]['unit_cost']) return product_data
[docs] def verify_response(self, data): """ Method verifies response from Verifone. :param data: parsed content from Verifone, dictionary :return: true if response is valid, boolean """ values = data.copy() sign1 = values['s-t-256-256_signature-one'] sign2 = values['s-t-256-256_signature-two'] remove_keys = [ 's-t-256-256_signature-one', 's-t-256-256_signature-two', 's-t-1-40_shop-receipt__phase', 's-t-1-40_shop-order__phase', 's-t-1-40_submit' ] for key in remove_keys: if key in values: del values[key] plaintext = self.get_plaintext(values) result = self.verify_signature(sign1, 'SHA1', plaintext) if not result: logs.debug("SHA1 verification failed for plaintext: " + str(plaintext)) raise ValueError('SignatureVerificationFailed') result = self.verify_signature(sign2, 'SHA512', plaintext) if not result: logs.debug("SHA512 verification failed for plaintext: " + str(plaintext)) raise ValueError('SignatureVerificationFailed') return True
[docs] def verify_signature(self, signature, signature_type, plaintext): """ Method verifies Verifone's signature. :param signature: signature, string :param signature_type: signature type, supported are SHA1 and SHA512, string :param plaintext: plaintext, string :return: true if response is valid, boolean """ digest = '' public_verifone = RSA.importKey(self._RSA_verifone_public_key) signer = PKCS1_v1_5.new(public_verifone) logs.debug("Plaintext for verifying: " + str(plaintext)) if (signature_type == 'SHA1'): digest = SHA.new(plaintext) elif (signature_type == 'SHA512'): digest = SHA512.new(plaintext) if digest: return signer.verify(digest, binascii.unhexlify(signature)) return False
[docs] def check_product_names(self, data): """ Method checks that product name can not be more than 30 character. If it is longer, take only first 30 character. :param data: data sent to Verifone, dictionary :return: - """ for key, value in data.items(): if key.startswith("s-t-1-30_bi-name-"): data[key] = self.get_substring(data[key], 30)