-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
1,344 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
#!/usr/bin/env python | ||
import os | ||
import sys | ||
|
||
if __name__ == "__main__": | ||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_payments.settings") | ||
try: | ||
from django.core.management import execute_from_command_line | ||
except ImportError as exc: | ||
raise ImportError( | ||
"Couldn't import Django. Are you sure it's installed and " | ||
"available on your PYTHONPATH environment variable? Did you " | ||
"forget to activate a virtual environment?" | ||
) from exc | ||
execute_from_command_line(sys.argv) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
class InvalidPaymentSolutionException(Exception): | ||
""" | ||
An exceptions raised when you attempt operations not allowed | ||
on the payment solution in question. | ||
e.g calling Paybox direct plus only operations on Paybox direct methods. | ||
""" | ||
def __init__(self, message, payload=None): | ||
self.message = message | ||
self.payload = payload | ||
|
||
def __str__(self): | ||
return str(self.message) | ||
|
||
|
||
class InvalidParametersException(Exception): | ||
""" | ||
An exceptions raised when Paybox has issues with parameters | ||
passed for the call. | ||
""" | ||
def __init__(self, message, payload=None): | ||
self.message = message | ||
self.payload = payload | ||
|
||
def __str__(self): | ||
return str(self.message) | ||
|
||
|
||
class PayboxEndpointException(Exception): | ||
""" | ||
An exception raised when Paybox endpoints can't seem to be reached. | ||
""" | ||
def __init__(self, message, payload=None): | ||
self.message = message | ||
self.payload = payload | ||
|
||
def __str__(self): | ||
return str(self.message) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,282 @@ | ||
import requests | ||
from urllib import parse | ||
|
||
from .exceptions import InvalidPaymentSolutionException, InvalidParametersException, PayboxEndpointException | ||
import urllib.parse | ||
from time import sleep | ||
|
||
|
||
from django.conf import settings | ||
|
||
|
||
class PayboxDirectTransaction: | ||
"""A Paybox Direct transaction, from your server to Paybox server | ||
Attributes: | ||
REQUIRED The values nedded to call for a payment | ||
OPTIONAL The values you may add to modify Paybox behavior | ||
RESPONSE_CODES Every response code Paybox may return after a payment attempt | ||
OPERATION_TYPES Codes for every type of operation possible | ||
ACTIVITE This parameter allows to inform the acquirer (bank) how the transaction was initiated and how the card entry was realized. | ||
""" | ||
|
||
def __init__(self, production=False, dateq=None, operation_type=None, numquestion=None, montant=None, | ||
reference=None, refabonne=None, cle=None, devise=None, porteur=None, dateval=None, cvv=None, | ||
activite=None, archivage=None, differe=None, numappel=None, numtrans=None, autorisation=None, | ||
pays=None, priv_codetraitement=None, datenaiss=None, acquereur=None, typecarte=None, url_encode=None, | ||
sha_1=None, errorcodetest=None, id_session=None, secure_3d=False, | ||
url_http_direct=None): | ||
|
||
self.production = production | ||
self.RETRY_CODES = ["00001", "00097", "00098"] | ||
self.card_verification_api_redirect_url = url_http_direct | ||
self.session_id = id_session | ||
self.secure_3d = secure_3d | ||
self.id3d = None | ||
|
||
self.ACTIVITE = { | ||
"020": "Not specified", | ||
"021": "Telephone order", | ||
"022": "Mail Order", | ||
"023": "Minitel(France)", | ||
"024": "Internet payment", | ||
"027": "Recurring payment" | ||
} | ||
|
||
# VERSION=00104&TYPE=00001&SITE=1999888&RANG=32&CLE=1999888I&NUM | ||
# QUESTION=194102418&MONTANT=1000&DEVISE=978&REFERENCE=TestPaybox&PORTEUR=1111222233334444&DATEVAL=0520&CVV=222&ACTIVITE=024&D | ||
# ATEQ=30012013&PAYS= | ||
|
||
self.REQUIRED = { | ||
"VERSION": "00104", # Version of the protocol PPPS used | ||
"TYPE": operation_type, # Transaction operation type | ||
"SITE": settings.PAYBOX_SITE, # SITE NUMBER (given by Paybox) 7 digits | ||
"RANG": settings.PAYBOX_RANG, # RANG NUMBER (given by Paybox) 2 digits | ||
"CLE": cle, # password for the merchant backoffice that is provided by the technical support upon the | ||
# creation of the merchant account on the Paybox platform. | ||
"NUMQUESTION": numquestion, # Unique identifier for the request that allows to avoid confusion | ||
# in case of multiple simultaneous requests. | ||
"MONTANT": montant, # Amount of the transaction in cents | ||
"DEVISE": devise if devise else "978", # Currency code of the transaction | ||
"REFERENCE": reference, # Merchant's reference number | ||
"PORTEUR": porteur, # PAN (card number) of the customer, without any spaces and left aligned | ||
"DATEVAL": dateval, # Expiry date of the card. (MMYY) | ||
} | ||
|
||
self.OPTIONAL = { | ||
"CVV": cvv, # Visual cryptogram on the back of the card. 3 0r 4 characters | ||
"REFABONNE": refabonne, # Merchant reference number allowing him to clearly identify the | ||
# subscriber (profile) that corresponds to the transaction. | ||
"NUMAPPEL": numappel, # This number is returned by Verifone when a transaction is successfully processed | ||
"NUMTRANS": numtrans, # This number is returned by Verifone when a transaction is successfully processed | ||
"ACTIVITE": activite, # This parameter allows to inform the acquirer (bank) how the transaction | ||
# was initiated and how the card entry was realized. | ||
"DATEQ": dateq, # Date and time of the request using the format DDMMYYYYHHMMSS | ||
"ARCHIVAGE": archivage, # This reference is transmitted to the acquirer (bank) of the merchant during | ||
# the settlement of the transaction | ||
"DIFFERE": differe, # Number of days to postpone the settlement | ||
"AUTORISATION": autorisation, # Authorization number provided by the merchant that was obtained by | ||
# telephone call to the acquirer (bank) | ||
"PAYS": pays, # If the parameter is present (even empty), Paybox Direct returns the country | ||
# code of issuance of the card in the response | ||
"PRIV_CODETRAITEMENT": priv_codetraitement, # Parameter filled in by the merchant to indicate the | ||
# payment option that is proposed to the cardholder of a SOFINCO card | ||
# (or partner card of SOFINCO) or COFINOGA.3 digits. payment language. | ||
# GBR for English | ||
"DATENAISS": datenaiss, # Birthday of the cardholder for the cards of COFINOGA. Date(DDMMYYYY) | ||
"ACQUEREUR": acquereur, # Defines the payment method used. The possible values are | ||
# [PAYPAL, EMS, ATOSBE, BCMC, EQUENS, PSC, FINAREF, 34ONEY] | ||
"TYPECARTE": typecarte, # If the parameter is present (even empty), Paybox Direct will return the type of | ||
# card in the response (for a payment using with a card). | ||
"URL_ENCODE": url_encode, # If the parameter contains O, Paybox Direct will URL-decode the value provided | ||
# in each field before evaluating them. | ||
"SHA-1": sha_1, # If the parameter is present (even empty), Paybox Direct will return the hash of the | ||
# card in the response (for a payment with a card). | ||
"ERRORCODETEST": errorcodetest, # The error code to return (forced) while doing integration testing in | ||
# the pre-production environment. In production the parameter is not | ||
# taken into account. | ||
"ID3D": self.id3d, # Context identifier Verifone that holds the authentication result of the MPI | ||
} | ||
|
||
self.RESPONSE_CODES = { | ||
"00000": "Operation successful", | ||
"00001": "Connection failed.", | ||
"001xx": "Payment rejected", | ||
"00002": "Error due to incoherence", | ||
"00003": "Internal paybox error.", | ||
"00004": "Card number invalid", | ||
"00005": "Request number invalid", | ||
"00006": "Site or rang invalid. Connection rejected", | ||
"00007": "Date invalid", | ||
"00008": "Card expiration date invalid", | ||
"00009": "Requested operation invalid", | ||
"00010": "Unrecognized currency", | ||
"00011": "Incorrect amount", | ||
"00012": "Order reference invalid", | ||
"00013": "This version is no longer supported", | ||
"00014": "Received request incoherent", | ||
"00015": "Error accessing data previously referenced", | ||
"00016": "Subscriber already exists", | ||
"00017": "Subscriber does not exist", | ||
"00018": "Transaction was not found", | ||
"00019": "Reserved", | ||
"00020": "Visual cryptogram missing(CVV)", | ||
"00021": "Card not authorized", | ||
"00022": "Threshold reached", | ||
"00023": "Cardholder already seen", | ||
"00024": "Country code filtered", | ||
"00026": "Activity code incorrect", | ||
"00040": "Card holder enrolled but not authenticated", | ||
"00097": "Connection timeout", | ||
"00098": "Internal connection timeout", | ||
"00099": "Incoherence between query and reply", | ||
} | ||
|
||
self.OPERATION_TYPES = { | ||
"00001": "Authorization Only", | ||
"00002": "Debit(Capture)", | ||
"00003": "Authorization + Capture", | ||
"00004": "Credit", | ||
"00005": "Cancel", | ||
"00011": "Check if a transaction exists", | ||
"00012": "Transaction without authorization request", | ||
"00014": "Refund", | ||
"00017": "Inquiry" | ||
} | ||
|
||
self.DIRECT_PLUS_ONLY_OPERATIONS = [ | ||
"00051", "00052", "00053", "00054", "00055", "00056", "00057", "00058", "00061" | ||
] | ||
|
||
self.DELAY_OPERATIONS = [ | ||
"00051", "00053" | ||
] | ||
|
||
def action_url(self): | ||
if self.production: | ||
main_url = "https://ppps.paybox.com/PPPS.php" | ||
backup_url = "https://ppps1.paybox.com/PPPS.php" | ||
request = requests.get(main_url) | ||
if request.status_code == 200: | ||
return main_url | ||
else: | ||
request = requests.get(backup_url) | ||
if request.status_code == 200: | ||
return backup_url | ||
else: | ||
main_url = "https://preprod-ppps.paybox.com/PPPS.php" | ||
return main_url | ||
raise PayboxEndpointException(message="Paybox Direct URL and its backup not responsive.") | ||
|
||
def remote_mpi_url(self): | ||
if self.production: | ||
mpi_url1 = "https://tpeweb.paybox.com/cgi/RemoteMPI.cgi" | ||
mpi_url2 = "https://tpeweb1.paybox.com/cgi/RemoteMPI.cgi" | ||
mpi_url3 = "https://tpeweb1.paybox.com/cgi/RemoteMPI.cgi" | ||
mpi_url4 = "https://tpeweb0.paybox.com/cgi/RemoteMPI.cgi" | ||
request = requests.get(mpi_url1) | ||
if request.status_code == 200: | ||
return mpi_url1 | ||
else: | ||
request = requests.get(mpi_url2) | ||
if request.status_code == 200: | ||
return mpi_url2 | ||
else: | ||
request = requests.get(mpi_url3) | ||
if request.status_code == 200: | ||
return mpi_url3 | ||
else: | ||
request = requests.get(mpi_url4) | ||
if request.status_code == 200: | ||
return mpi_url4 | ||
else: | ||
remote_mpi_url = "https://preprod-tpeweb.paybox.com/cgi/RemoteMPI.cgi" | ||
return remote_mpi_url | ||
raise PayboxEndpointException(message="Paybox MPI URL is not responsive.") | ||
|
||
def remote_mpi_authenticate(self, session_id): | ||
""" | ||
To carry out a 3D-Secure transaction, merchants will need to authenticate the cardholder before | ||
calling Paybox Direct Applications | ||
:return: {"ID3D": "", "StatusPBX": "", "Check": "", "IdSession": "", "3DCAVV": "", | ||
"3DCAVVALGO": "", "3DECI": "", "3DENROLLED": "", "3DERROR": "", "3DSIGNVAL": "", | ||
"3DSTATUS": "", "3DXID": "", "Check": ""} | ||
""" | ||
card_verification_string = "IdMerchant={0},IdSession={1},Amount={2},Currency={3},CCNumber={4},CCExpDate={5},CVVCode={6},URLHttpDirect={7}".format(settings.PAYBOX_IDENTIFIANT, session_id, self.REQUIRED['MONTANT'], self.REQUIRED['DEVISE'], self.REQUIRED['PORTEUR'], self.REQUIRED['DATEVAL'], self.OPTIONAL['CVV'], self.card_verification_api_redirect_url) | ||
|
||
remote_mpi_call = requests.post(self.remote_mpi_url(), data=card_verification_string) | ||
response = dict(parse.parse_qsl(remote_mpi_call.text)) | ||
try: | ||
if urllib.parse.unquote(response['StatusPBX']) == "Autorisation à faire": | ||
self.id3d = response['ID3D'] | ||
elif urllib.parse.unquote(response['StatusPBX']) == "Autorisation à ne pas faire": | ||
raise InvalidParametersException("Cardholder authentication failed.") | ||
except KeyError: | ||
raise InvalidParametersException(message="3D secure authentication failed.") | ||
return response | ||
|
||
def post_to_paybox(self, numquestion, operation_type=None): | ||
""" | ||
To carry out a 3D-Secure transaction, merchants will need to authenticate the cardholder before | ||
calling Paybox Direct Applications | ||
:return: {"CODEREPONSE”": "", "COMMENTAIRE": "", "AUTORISATION": "", "NUMAPPEL": "" | ||
"NUMQUESTION": "", "NUMTRANS": "", "PAYS": "", "PORTEUR": "", "RANG": "", | ||
"REFABONNE": "", "REMISE": "", "SHA-1": "", "SITE": "", "STATUS": "", | ||
"TYPECARTE": ""} | ||
""" | ||
self.REQUIRED['TYPE'] = operation_type | ||
if self.secure_3d: | ||
if self.id3d is None: | ||
raise InvalidParametersException(message="3D Secure payments require mpi authentication.") | ||
if operation_type in self.DIRECT_PLUS_ONLY_OPERATIONS: | ||
raise InvalidPaymentSolutionException( | ||
message="You have called a Paybox Direct Plus operation on a Paybox Direct method.") | ||
if operation_type in self.DELAY_OPERATIONS: | ||
sleep(1) | ||
self.REQUIRED['NUMQUESTION'] = numquestion | ||
payload = {**self.REQUIRED, **self.OPTIONAL} | ||
session = requests.Session() | ||
paybox_call = session.post(self.action_url(), data=payload) | ||
response = dict(parse.parse_qsl(paybox_call.text)) | ||
if response['CODEREPONSE'] in self.RETRY_CODES: | ||
self.post_to_paybox(numquestion, operation_type) | ||
if self.REQUIRED['TYPE'] == "00001": | ||
self.OPTIONAL['NUMAPPEL'] = response['NUMAPPEL'] | ||
self.OPTIONAL['NUMTRANS'] = response['NUMTRANS'] | ||
return response | ||
|
||
def construct_html_form(self): | ||
""" | ||
Returns an html form ready to be POSTed to Paybox Direct (string) | ||
:return: str <form> | ||
""" | ||
|
||
optional_fields = "\n".join( | ||
[ | ||
"<input type='hidden' name='{0}' value='{1}'>".format( | ||
field, self.OPTIONAL[field] | ||
) | ||
for field in self.OPTIONAL | ||
if self.OPTIONAL[field] | ||
] | ||
) | ||
|
||
html = """<form method=POST action="{action_url}"> | ||
<input name = "DATEQ" value = "{required[DATEQ]}" type="text"> | ||
<input name = "TYPE" value = "{required[TYPE]}" type="text"> | ||
<input name = "NUMQUESTION" value = "{required[NUMQUESTION]}" type="text"> | ||
<input name = "MONTANT" value = "{required[MONTANT]}" type="text"> | ||
<input name = "SITE" value = "{required[SITE]}" type="text"> | ||
<input name = "RANG" value = "{required[RANG]}" type="text"> | ||
<input name="REFERENCE" value="{required[REFERENCE]}" type="text"> | ||
<input name="REFABONNE" value="{required[REFABONNE]}" type="text"> | ||
<input name="PORTEUR" value="{required[PORTEUR]}" type="text"> | ||
<input name="DATEVAL" value="{required[DATEVAL]}" type="text"> | ||
<input name="NUMAPPEL" value="{required[NUMAPPEL]}" type="text"> | ||
<input name="NUMTRANS" value="{required[NUMTRANS]}" type="text"> | ||
{optional} | ||
<input type="submit" value="Pay"> | ||
</form>""" | ||
|
||
return html.format( | ||
action=self.action_url(), required=self.REQUIRED, optional=optional_fields | ||
) |
Oops, something went wrong.