Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions redsys/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,117 @@
RECURRING_DEFERRED_PREAUTHORIZATION,
SUCCESSIVE_RECURRING_TRANSACTION,
]


"""
Tokenization
------------

Tokenization is a security process that replaces sensitive card information
(such as the PAN, expiration date, and CVV) with a randomly generated unique
identifier known as a token . This token has no value outside the specific
system for which it was created and cannot be used to conduct fraudulent
transactions if intercepted.

Credential on File (COF)
------------------------

A COF transaction, also known as Credential on File or Card on File, is a
transaction in which the merchant uses the cardholder's card data (PAN or
tokenized PAN and expiration date), with the cardholder having explicitly
authorized the merchant to store and use this data in that and subsequent
transactions. A COF transaction can be initiated by the cardholder or by
the merchant as a result of an agreement between the cardholder and the
merchant. When performing a COF transaction, you must identify the specific
scenario in which you intend to operate.

Types of COF operations
-----------------------

It's necessary to consider whether the operation is the first COF operation,
meaning the request and storage of credentials, or a subsequent one, meaning
you'll be using previously saved credentials. Additionally, you need to know
what type of operation you're going to perform:

Main operations.

* Installments / Deferred Payment: Always referring to an individual
purchase, the amount of the transactions must be fixed, and with a defined
time interval.
* Recurring / Recurring Payment: The amount of the transactions can be
variable, but the time interval must be defined.

Special operations.

* Reauthorization: This is usually done for partial shipments or when the
customer extends the paid service (hotel stay, vehicle rental, etc.) or when,
having an estimated authorization, the final amount is requested.
* Resubmission: Used when the original has been refused due to "insufficient
funds." It can only be used in certain sectors; you should consult the
trademark regulations for more information.
* Delayed: Charges made after the main transaction for services rendered,
such as use of the minibar in a hotel stay or damage to the rented vehicle.
* Incremental: Used when the contracted service incurs additional expenses
not included in the main operation.
* No Show: Type used when the business charges for services that the account
holder contracted but ultimately did not show up or did not use, such as a
hotel reservation that was not cancelled.

More info: https://pagosonline.redsys.es/desarrolladores-inicio/documentacion-funcionalidades-avanzadas/tokenizacion/
"""
COF_TYPE_INSTALLMENTS = "I"
COF_TYPE_RECURRING = "R"
COF_TYPE_REAUTHORISATION = "H"
COF_TYPE_RESUBMISSION = "E"
COF_TYPE_DELAYED = "D"
COF_TYPE_INCREMENTAL = "M"
COF_TYPE_NO_SHOW = "N"
COF_TYPE_OTRAS = "C"

TYPES_OF_COF = [
COF_TYPE_INSTALLMENTS,
COF_TYPE_RECURRING,
COF_TYPE_REAUTHORISATION,
COF_TYPE_RESUBMISSION,
COF_TYPE_DELAYED,
COF_TYPE_INCREMENTAL,
COF_TYPE_NO_SHOW,
COF_TYPE_OTRAS,
]


"""
COF Transition Indicator
S: It is first COF transaction (store credentials)
N: It is not the first COF transaction
"""
COF_FIRST_TRANSACTION = "S"
COF_NOT_FIRST_TRANSACTION = "N"
COF_TRANSACTIONS = [
COF_FIRST_TRANSACTION,
COF_NOT_FIRST_TRANSACTION,
]


"""
Strong Customer Authentication (SCA):

PSD2 requires payment service providers (also called PSPs) to implement
additional security measures to verify the customer's identity when making
electronic transactions, such as using one-time codes via SMS or accepting
the transaction within their bank's mobile application.
"""

SCA_EXCEP_MIT = "MIT" # Merchant-Initiated Transactions
SCA_EXCEP_LWV = "LWV" # Low-value transactions
SCA_EXCEP_TRA = "TRA" # Low-risk transactions
SCA_EXCEP_COR = "COR" # Corporate Payment
SCA_EXCEP_ATD = "ATD" # Trusted Beneficiaries

SCA_EXEMPTIONS = [
SCA_EXCEP_MIT,
SCA_EXCEP_LWV,
SCA_EXCEP_TRA,
SCA_EXCEP_COR,
SCA_EXCEP_ATD,
]
40 changes: 39 additions & 1 deletion redsys/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
from typing import Any
from typing import Dict

from redsys.constants import COF_TRANSACTIONS
from redsys.constants import CURRENCIES
from redsys.constants import LANGUAGES
from redsys.constants import SCA_EXEMPTIONS
from redsys.constants import TRANSACTIONS
from redsys.constants import TYPES_OF_COF

# General parameters
MERCHANT_CODE = "Ds_Merchant_MerchantCode"
Expand All @@ -15,6 +18,15 @@
CURRENCY = "Ds_Merchant_Currency"
AMOUNT = "Ds_Merchant_Amount"

# Tokenization - Credential on File (COF)
MERCHANT_IDENTIFIER = "Ds_Merchant_Identifier"
MERCHANT_COF_INI = "Ds_Merchant_COF_INI"
MERCHANT_COF_TYPE = "Ds_Merchant_COF_TYPE"
MERCHANT_COF_TXNID = "Ds_Merchant_Cof_Txnid"
# Merchant-Initiated Transactions (MITs)
MERCHANT_EXCEP_SCA = "Ds_Merchant_Excep_Sca"
MERCHANT_DIRECTPAYMENT = "Ds_Merchant_DirectPayment"

# Recurring transaction parameters
SUM_TOTAL = "Ds_Merchant_SumTotal"
DATE_FREQUENCY = "Ds_Merchant_DateFrecuency"
Expand Down Expand Up @@ -57,6 +69,12 @@
"authorization_code": AUTHORIZATION_CODE,
"merchant_data": MERCHANT_DATA,
"merchant_name": MERCHANT_NAME,
"merchant_identifier": MERCHANT_IDENTIFIER,
"merchant_cof_txnid": MERCHANT_COF_TXNID,
"merchant_cof_ini": MERCHANT_COF_INI,
"merchant_cof_type": MERCHANT_COF_TYPE,
"merchant_excep_sca": MERCHANT_EXCEP_SCA,
"merchant_directpayment": MERCHANT_DIRECTPAYMENT,
"product_description": PRODUCT_DESCRIPTION,
"titular": TITULAR,
"merchant_url": MERCHANT_URL,
Expand Down Expand Up @@ -135,9 +153,29 @@ def check_amount(value):

@staticmethod
def check_sum_total(value):
if type(value) is not Decimal:
if not isinstance(value, Decimal):
raise TypeError("sum_total must be defined as decimal.Decimal.")

@staticmethod
def check_authorization_code(value):
if not re.match(r"^[a-zA-Z0-9]{1,6}$", value):
raise ValueError("authorization_code format is not valid.")

@staticmethod
def check_merchant_cof_ini(value):
if value not in COF_TRANSACTIONS:
raise ValueError("merchant_cof_ini is not valid.")

@staticmethod
def check_merchant_cof_type(value):
if value not in TYPES_OF_COF:
raise ValueError("merchant_cof_type is not valid.")

@staticmethod
def check_merchant_excep_sca(value):
if value not in SCA_EXEMPTIONS:
raise ValueError("merchant_excep_sca is not valid.")

@staticmethod
def check_merchant_data(value):
if len(value) > 1024:
Expand Down
18 changes: 18 additions & 0 deletions redsys/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,30 @@

import pytest

from redsys.client import Client
from redsys.client import RedirectClient
from redsys.constants import EUR
from redsys.constants import STANDARD_PAYMENT
from redsys.request import Request


class TestClient:
def test_abstract_create_response_raises_not_implemented(self):
class DummyClient(Client):
def create_response(self, signature, parameters):
return super().create_response(signature, parameters)

def prepare_request(self, request):
return super().prepare_request(request)

client = DummyClient("secret")
with pytest.raises(NotImplementedError):
client.create_response("sig", "params")

with pytest.raises(NotImplementedError):
client.prepare_request({})


class TestRedirectClient:
@pytest.fixture(autouse=True)
def set_up(self):
Expand Down
68 changes: 68 additions & 0 deletions redsys/tests/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,71 @@ def test_super_long_merchant_url_throws_error(self):
merchant_url = "".join(choice("abcdefghijklmnopqrtsuvwxyz-0123456789") for _ in range(251))
with pytest.raises(ValueError):
assert Request.check_merchant_url(merchant_url)

def test_setattr(self):
request = Request({
"merchant_code": "100000001",
"terminal": "1",
"transaction_type": STANDARD_PAYMENT,
"currency": EUR,
"order": "000000000001",
"amount": D("10.00"),
})
request.merchant_data = "new data"
assert request.merchant_data == "new data"

def test_check_order_invalid_format(self):
with pytest.raises(ValueError, match="order format is not valid"):
Request.check_order("invalid")

def test_check_transaction_type_invalid(self):
with pytest.raises(ValueError, match="transaction_type is not valid"):
Request.check_transaction_type("X")

def test_check_currency_invalid(self):
with pytest.raises(ValueError, match="currency is not valid"):
Request.check_currency(999)

def test_check_amount_wrong_type(self):
with pytest.raises(TypeError, match="amount must be defined as decimal.Decimal"):
Request.check_amount("10.00")

def test_check_sum_total_wrong_type(self):
with pytest.raises(TypeError, match="sum_total must be defined as decimal.Decimal"):
Request.check_sum_total("10.00")

def test_check_merchant_cof_ini_invalid(self):
with pytest.raises(ValueError, match="merchant_cof_ini is not valid"):
Request.check_merchant_cof_ini("X")

def test_check_merchant_cof_type_invalid(self):
with pytest.raises(ValueError, match="merchant_cof_type is not valid"):
Request.check_merchant_cof_type("X")

def test_check_merchant_excep_sca_invalid(self):
with pytest.raises(ValueError, match="merchant_excep_sca is not valid"):
Request.check_merchant_excep_sca("X")

def test_check_url_ok_too_long(self):
url_ok = "".join(choice("a") for _ in range(251))
with pytest.raises(ValueError, match="url_ok cannot be longer than 250 characters"):
Request.check_url_ok(url_ok)

def test_check_url_ko_too_long(self):
url_ko = "".join(choice("a") for _ in range(251))
with pytest.raises(ValueError, match="url_ko cannot be longer than 250 characters"):
Request.check_url_ko(url_ko)

def test_check_consumer_language_invalid(self):
with pytest.raises(ValueError, match="consumer_language is not valid"):
Request.check_consumer_language("999")

def test_prepare_sum_total(self):
result = Request.prepare_sum_total(D("100.50"))
assert result == 10050

def test_check_authorization_code_invalid_format(self):
with pytest.raises(ValueError, match="authorization_code format is not valid"):
Request.check_authorization_code("invalid!")
with pytest.raises(ValueError, match="authorization_code format is not valid"):
Request.check_authorization_code("12345678")
29 changes: 29 additions & 0 deletions redsys/tests/test_response.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from decimal import Decimal as D

from redsys.response import Response


Expand All @@ -21,3 +23,30 @@ def test_create_response(self):
assert response.is_paid is True
assert response.is_canceled is False
assert response.is_refunded is False

def test_setattr(self):
response = Response({
"Ds_Response": "90",
"Ds_MerchantCode": "1056",
"Ds_Terminal": "1",
"Ds_TransactionType": "1",
"Ds_Order": "000000000001",
"Ds_Amount": "10054",
"Ds_Currency": 978,
})
response.merchant_data = "new data"
assert "Ds_MerchantData" in response._parameters
assert response._parameters["Ds_MerchantData"] == "new data"

def test_parameters_property(self):
parameters = {
"Ds_Response": "90",
"Ds_MerchantCode": "1056",
"Ds_Terminal": "1",
}
response = Response(parameters)
assert response.parameters == response._parameters

def test_clean_amount(self):
result = Response.clean_amount("10054")
assert result == D("100.54")