From d92c32ecaf87cb524035b1ae817d98bee0deefef Mon Sep 17 00:00:00 2001 From: Jonathan Reveille Date: Fri, 6 Sep 2024 10:05:45 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(backend)=20payment=20backend=20refund?= =?UTF-8?q?=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For now, we have added the refund action with Lyra backend payment provider. It allows to refund transaction created with this payment provider. --- src/backend/joanie/payment/backends/base.py | 8 + src/backend/joanie/payment/backends/dummy.py | 6 + .../joanie/payment/backends/lyra/__init__.py | 46 ++- .../payment/backends/payplug/__init__.py | 2 +- src/backend/joanie/settings.py | 3 + .../requests/cancel_or_refund_request.json | 6 + .../responses/cancel_transaction_payment.json | 298 ++++++++++++++++++ .../responses/refund_transaction_payment.json | 255 +++++++++++++++ .../joanie/tests/payment/test_backend_base.py | 3 + .../joanie/tests/payment/test_backend_lyra.py | 72 ++++- 10 files changed, 696 insertions(+), 3 deletions(-) create mode 100644 src/backend/joanie/tests/payment/lyra/requests/cancel_or_refund_request.json create mode 100644 src/backend/joanie/tests/payment/lyra/responses/cancel_transaction_payment.json create mode 100644 src/backend/joanie/tests/payment/lyra/responses/refund_transaction_payment.json diff --git a/src/backend/joanie/payment/backends/base.py b/src/backend/joanie/payment/backends/base.py index 56b20c38b7..b9c42caafe 100644 --- a/src/backend/joanie/payment/backends/base.py +++ b/src/backend/joanie/payment/backends/base.py @@ -265,3 +265,11 @@ def tokenize_card(self, order=None, billing_address=None, user=None): raise NotImplementedError( "subclasses of BasePaymentBackend must provide a tokenize_card() method." ) + + def cancel_or_refund(self, amount, transaction, user_email): + """ + Method called to cancel or refund installments from an order payment schedule. + """ + raise NotImplementedError( + "subclasses of BasePaymentBackend must provide a cancel_or_refund() method." + ) diff --git a/src/backend/joanie/payment/backends/dummy.py b/src/backend/joanie/payment/backends/dummy.py index 1309cd745c..c9f4ce38a1 100644 --- a/src/backend/joanie/payment/backends/dummy.py +++ b/src/backend/joanie/payment/backends/dummy.py @@ -316,3 +316,9 @@ def tokenize_card(self, order=None, billing_address=None, user=None): # pylint: "customer": str(user.id), "card_token": f"card_{user.id}", } + + def cancel_or_refund(self, amount, transaction, user_email): + """ + Dummy method to mark installments has refunded. + """ + pass diff --git a/src/backend/joanie/payment/backends/lyra/__init__.py b/src/backend/joanie/payment/backends/lyra/__init__.py index 37c43f859c..8dae8d5ecf 100644 --- a/src/backend/joanie/payment/backends/lyra/__init__.py +++ b/src/backend/joanie/payment/backends/lyra/__init__.py @@ -118,7 +118,12 @@ def _call_api(self, url, payload): logger.info("Calling Lyra API %s", url, extra={"context": context}) try: - response = requests.post(url, json=payload, headers=self.headers, timeout=5) + response = requests.post( + url, + json=payload, + headers=self.headers, + timeout=settings.JOANIE_PAYMENT_TIMEOUT, + ) response.raise_for_status() except requests.exceptions.RequestException as e: context = context.copy() @@ -480,3 +485,42 @@ def abort_payment(self, payment_id): """ Abort a payment, nothing to do for Lyra """ + + def cancel_or_refund(self, amount, transaction, user_email): + """ + Method to cancel or refund a transaction made on an order for a user. + This endpoint from Lyra will determine if the transaction can be canceled or refunded + depending on the expected capture date set on the transaction. If the money is not yet + captured on the user's bank account, the transaction it will be canceled, otherwise, + if the capture date has been reached it will trigger the refund of the transaction. + In Joanie's case, the capturedDate is set to 0, which means, the capture of the amount + will be effective the same day. + + In a few words, this method cancels the transaction when its still eligible or + refunds the transaction if it has been captured already. + + In the answer from the payment provider API : `REFUNDED` or `CANCELLED` + """ + url = f"{self.api_url}Transaction/CancelOrRefund" + payload = { + "amount": int(amount.sub_units), + "currency": settings.DEFAULT_CURRENCY, + "customer": {"email": f"{user_email}"}, + "uuid": str(transaction.reference), + "resolutionMode": "AUTO", + } + + response_json = self._call_api(url, payload) + + if response_json.get("status") != "SUCCESS": + return False + + return { + "status": "REFUNDED" + if response_json.get("answer").get("detailedStatus") == "AUTHORISED" + else "CANCELLED", + "reference": response_json.get("answer").get("uuid"), + "installment_id": response_json.get("answer") + .get("metadata") + .get("installment_id"), + } diff --git a/src/backend/joanie/payment/backends/payplug/__init__.py b/src/backend/joanie/payment/backends/payplug/__init__.py index c5f884375c..186d7ef907 100644 --- a/src/backend/joanie/payment/backends/payplug/__init__.py +++ b/src/backend/joanie/payment/backends/payplug/__init__.py @@ -225,7 +225,7 @@ def delete_credit_card(self, credit_card): "Content-Type": "appliation/json", "Payplug-Version": self.configuration.get("api_version"), }, - timeout=10, + timeout=settings.JOANIE_PAYMENT_TIMEOUT, ) if not response.ok: diff --git a/src/backend/joanie/settings.py b/src/backend/joanie/settings.py index 24106f6ca3..12be51a3c0 100755 --- a/src/backend/joanie/settings.py +++ b/src/backend/joanie/settings.py @@ -452,6 +452,9 @@ class Base(Configuration): environ_name="JOANIE_INSTALLMENT_REMINDER_DAYS_BEFORE", environ_prefix=None, ) + JOANIE_PAYMENT_TIMEOUT = values.PositiveIntegerValue( + 10, environ_name="JOANIE_PAYMENT_TIMEOUT", environ_prefix=None + ) # CORS CORS_ALLOW_CREDENTIALS = True diff --git a/src/backend/joanie/tests/payment/lyra/requests/cancel_or_refund_request.json b/src/backend/joanie/tests/payment/lyra/requests/cancel_or_refund_request.json new file mode 100644 index 0000000000..8859daa156 --- /dev/null +++ b/src/backend/joanie/tests/payment/lyra/requests/cancel_or_refund_request.json @@ -0,0 +1,6 @@ +{ + "amount": 10000, + "currency": "EUR", + "resolutionMode": "AUTO", + "uuid": "dbf4b89ae157499e83bea366c91daaa8" +} diff --git a/src/backend/joanie/tests/payment/lyra/responses/cancel_transaction_payment.json b/src/backend/joanie/tests/payment/lyra/responses/cancel_transaction_payment.json new file mode 100644 index 0000000000..87b3e45728 --- /dev/null +++ b/src/backend/joanie/tests/payment/lyra/responses/cancel_transaction_payment.json @@ -0,0 +1,298 @@ +{ + "webService": "Transaction/CancelOrRefund", + "version": "V4", + "applicationVersion": "6.21.0", + "status": "SUCCESS", + "answer": { + "shopId": "69876357", + "uuid": "d1053bae1aad463f8975ec248fa46eb3", + "paymentMethodType": "CARD", + "paymentMethodToken": null, + "detailedStatus": "CANCELLED", + "status": "UNPAID", + "amount": 990, + "currency": "EUR", + "creationDate": "2024-09-30T16:43:51+00:00", + "errorCode": null, + "errorMessage": null, + "detailedErrorCode": null, + "detailedErrorMessage": null, + "effectiveStrongAuthentication": "DISABLED", + "customer": { + "billingDetails": { + "address": null, + "category": null, + "cellPhoneNumber": null, + "city": null, + "country": null, + "district": null, + "firstName": null, + "identityCode": null, + "identityType": null, + "language": "EN", + "lastName": null, + "phoneNumber": null, + "state": null, + "streetNumber": null, + "title": null, + "zipCode": null, + "legalName": null, + "_type": "V4/Customer/BillingDetails" + }, + "email": "sample@example.com", + "reference": null, + "shippingDetails": { + "address": null, + "address2": null, + "category": null, + "city": null, + "country": null, + "deliveryCompanyName": null, + "district": null, + "firstName": null, + "identityCode": null, + "lastName": null, + "legalName": null, + "phoneNumber": null, + "shippingMethod": null, + "shippingSpeed": null, + "state": null, + "streetNumber": null, + "zipCode": null, + "_type": "V4/Customer/ShippingDetails" + }, + "extraDetails": { + "browserAccept": null, + "fingerPrintId": null, + "ipAddress": "90.92.39.117", + "browserUserAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + "_type": "V4/Customer/ExtraDetails" + }, + "shoppingCart": { + "insuranceAmount": null, + "shippingAmount": null, + "taxAmount": null, + "cartItemInfo": null, + "_type": "V4/Customer/ShoppingCart" + }, + "_type": "V4/Customer/Customer" + }, + "transactionDetails": { + "liabilityShift": "NO", + "effectiveAmount": 990, + "effectiveCurrency": "EUR", + "creationContext": "CHARGE", + "cardDetails": { + "paymentSource": "EC", + "manualValidation": "NO", + "expectedCaptureDate": "2024-10-06T16:43:51+00:00", + "effectiveBrand": "VISA", + "pan": "497010XXXXXX0055", + "expiryMonth": 12, + "expiryYear": 2025, + "country": "FR", + "issuerCode": null, + "issuerName": "Banque de demo et de l'innovation", + "effectiveProductCode": "F", + "legacyTransId": "905496", + "legacyTransDate": "2024-09-30T16:43:51+00:00", + "paymentMethodSource": "NEW", + "authorizationResponse": { + "amount": 990, + "currency": "EUR", + "authorizationDate": "2024-09-30T16:43:51+00:00", + "authorizationNumber": "3fe584", + "authorizationResult": "0", + "authorizationMode": "FULL", + "_type": "V4/PaymentMethod/Details/Cards/CardAuthorizationResponse" + }, + "captureResponse": { + "refundAmount": null, + "refundCurrency": null, + "captureDate": null, + "captureFileNumber": null, + "effectiveRefundAmount": null, + "effectiveRefundCurrency": null, + "_type": "V4/PaymentMethod/Details/Cards/CardCaptureResponse" + }, + "threeDSResponse": { + "authenticationResultData": { + "transactionCondition": null, + "enrolled": null, + "status": null, + "eci": null, + "xid": null, + "cavvAlgorithm": null, + "cavv": null, + "signValid": null, + "brand": null, + "_type": "V4/PaymentMethod/Details/Cards/CardAuthenticationResponse" + }, + "_type": "V4/PaymentMethod/Details/Cards/ThreeDSResponse" + }, + "authenticationResponse": { + "id": "44911066-1a2f-436f-9b1b-44d9592e7339", + "protocol": { + "name": "THREEDS", + "version": "2.2.0", + "network": "VISA", + "challengePreference": "NO_PREFERENCE", + "simulation": true, + "_type": "V4/Charge/Authenticate/Protocol" + }, + "value": { + "status": "NOT_ENROLLED", + "extension": { + "authenticationType": "THREEDS_V2", + "requestedExemption": "TECHNICAL_ERROR", + "requestorName": "Demo shop", + "_type": "V4/Charge/Authenticate/AuthenticationResultExtensionThreedsV2" + }, + "_type": "V4/Charge/Authenticate/AuthenticationResult" + }, + "_type": "V4/AuthenticationResponseData" + }, + "installmentNumber": null, + "installmentCode": null, + "markAuthorizationResponse": { + "amount": null, + "currency": null, + "authorizationDate": null, + "authorizationNumber": null, + "authorizationResult": null, + "_type": "V4/PaymentMethod/Details/Cards/MarkAuthorizationResponse" + }, + "cardHolderName": null, + "cardHolderPan": "497010XXXXXX0055", + "cardHolderExpiryMonth": 12, + "cardHolderExpiryYear": 2025, + "identityDocumentNumber": null, + "identityDocumentType": null, + "initialIssuerTransactionIdentifier": null, + "productCategory": "DEBIT", + "nature": "CONSUMER_CARD", + "_type": "V4/PaymentMethod/Details/CardDetails" + }, + "paymentMethodDetails": { + "id": "497010XXXXXX0055", + "paymentSource": "EC", + "manualValidation": "NO", + "expectedCaptureDate": "2024-10-06T16:43:51+00:00", + "effectiveBrand": "VISA", + "expiryMonth": 12, + "expiryYear": 2025, + "country": "FR", + "issuerCode": null, + "issuerName": "Banque de demo et de l'innovation", + "effectiveProductCode": "F", + "legacyTransId": "905496", + "legacyTransDate": "2024-09-30T16:43:51+00:00", + "paymentMethodSource": "NEW", + "authorizationResponse": { + "amount": 990, + "currency": "EUR", + "authorizationDate": "2024-09-30T16:43:51+00:00", + "authorizationNumber": "3fe584", + "authorizationResult": "0", + "authorizationMode": "FULL", + "_type": "V4/PaymentMethod/Details/Cards/CardAuthorizationResponse" + }, + "captureResponse": { + "refundAmount": null, + "refundCurrency": null, + "captureDate": null, + "captureFileNumber": null, + "effectiveRefundAmount": null, + "effectiveRefundCurrency": null, + "_type": "V4/PaymentMethod/Details/Cards/CardCaptureResponse" + }, + "authenticationResponse": { + "id": "44911066-1a2f-436f-9b1b-44d9592e7339", + "protocol": { + "name": "THREEDS", + "version": "2.2.0", + "network": "VISA", + "challengePreference": "NO_PREFERENCE", + "simulation": true, + "_type": "V4/Charge/Authenticate/Protocol" + }, + "value": { + "status": "NOT_ENROLLED", + "extension": { + "authenticationType": "THREEDS_V2", + "requestedExemption": "TECHNICAL_ERROR", + "requestorName": "Demo shop", + "_type": "V4/Charge/Authenticate/AuthenticationResultExtensionThreedsV2" + }, + "_type": "V4/Charge/Authenticate/AuthenticationResult" + }, + "_type": "V4/AuthenticationResponseData" + }, + "installmentNumber": null, + "installmentCode": null, + "markAuthorizationResponse": { + "amount": null, + "currency": null, + "authorizationDate": null, + "authorizationNumber": null, + "authorizationResult": null, + "_type": "V4/PaymentMethod/Details/Cards/MarkAuthorizationResponse" + }, + "cardHolderName": null, + "cardHolderPan": "497010XXXXXX0055", + "cardHolderExpiryMonth": 12, + "cardHolderExpiryYear": 2025, + "identityDocumentNumber": null, + "identityDocumentType": null, + "initialIssuerTransactionIdentifier": null, + "_type": "V4/PaymentMethod/Details/PaymentMethodDetails" + }, + "acquirerDetails": null, + "fraudManagement": { + "riskControl": [], + "riskAnalysis": [], + "_type": "V4/PaymentMethod/Details/FraudManagement" + }, + "parentTransactionUuid": null, + "mid": "9876357", + "sequenceNumber": 1, + "taxAmount": null, + "preTaxAmount": null, + "taxRate": null, + "externalTransactionId": null, + "nsu": null, + "tid": "001", + "acquirerNetwork": "CB", + "taxRefundAmount": null, + "userInfo": null, + "paymentMethodTokenPreviouslyRegistered": null, + "occurrenceType": "UNITAIRE", + "archivalReferenceId": "L27490549601", + "useCase": null, + "wallet": null, + "_type": "V4/TransactionDetails" + }, + "orderDetails": { + "orderTotalAmount": 990, + "orderEffectiveAmount": null, + "orderCurrency": "EUR", + "mode": "TEST", + "orderId": "myOrderId-321", + "metadata": null, + "_type": "V4/OrderDetails" + }, + "operationType": "DEBIT", + "metadata": { + "installment_id": "8998", + "orderInfo": "" + }, + "_type": "V4/Transaction" + }, + "ticket": "753889b9cc784f84b5339da9be5ede6f", + "serverDate": "2024-09-30T16:46:04+00:00", + "applicationProvider": "LYRA", + "metadata": null, + "mode": "TEST", + "serverUrl": "https://api.lyra.com", + "_type": "V4/WebService/Response" +} diff --git a/src/backend/joanie/tests/payment/lyra/responses/refund_transaction_payment.json b/src/backend/joanie/tests/payment/lyra/responses/refund_transaction_payment.json new file mode 100644 index 0000000000..9a383a0112 --- /dev/null +++ b/src/backend/joanie/tests/payment/lyra/responses/refund_transaction_payment.json @@ -0,0 +1,255 @@ +{ + "webService": "Transaction/CancelOrRefund", + "version": "V4", + "applicationVersion": "6.21.0", + "status": "SUCCESS", + "answer": { + "shopId": "69876357", + "uuid": "3c28bc9cfea343a99edfcbde833f09b9", + "paymentMethodType": "CARD", + "paymentMethodToken": "d662d15e7ebf4682a22b8858b4f2dac0", + "detailedStatus": "AUTHORISED", + "status": "PAID", + "amount": 10000, + "currency": "EUR", + "creationDate": "2024-09-30T16:09:00+00:00", + "errorCode": null, + "errorMessage": null, + "detailedErrorCode": null, + "detailedErrorMessage": null, + "effectiveStrongAuthentication": "DISABLED", + "customer": { + "billingDetails": { + "address": null, + "category": null, + "cellPhoneNumber": null, + "city": null, + "country": null, + "district": null, + "firstName": null, + "identityCode": null, + "identityType": null, + "language": "EN", + "lastName": null, + "phoneNumber": null, + "state": null, + "streetNumber": null, + "title": null, + "zipCode": null, + "legalName": null, + "_type": "V4/Customer/BillingDetails" + }, + "email": "sample@example.com", + "reference": null, + "shippingDetails": { + "address": null, + "address2": null, + "category": null, + "city": null, + "country": null, + "deliveryCompanyName": null, + "district": null, + "firstName": null, + "identityCode": null, + "lastName": null, + "legalName": null, + "phoneNumber": null, + "shippingMethod": null, + "shippingSpeed": null, + "state": null, + "streetNumber": null, + "zipCode": null, + "_type": "V4/Customer/ShippingDetails" + }, + "extraDetails": { + "browserAccept": null, + "fingerPrintId": null, + "ipAddress": "103.105.136.19", + "browserUserAgent": null, + "_type": "V4/Customer/ExtraDetails" + }, + "shoppingCart": { + "insuranceAmount": null, + "shippingAmount": null, + "taxAmount": null, + "cartItemInfo": null, + "_type": "V4/Customer/ShoppingCart" + }, + "_type": "V4/Customer/Customer" + }, + "transactionDetails": { + "liabilityShift": null, + "effectiveAmount": 10000, + "effectiveCurrency": "EUR", + "creationContext": "REFUND", + "cardDetails": { + "paymentSource": "OTHER", + "manualValidation": "NO", + "expectedCaptureDate": "2024-09-30T16:09:00+00:00", + "effectiveBrand": "VISA", + "pan": "497010XXXXXX0055", + "expiryMonth": 12, + "expiryYear": 2025, + "country": "FR", + "issuerCode": null, + "issuerName": "Banque de demo et de l'innovation", + "effectiveProductCode": "F", + "legacyTransId": "930244", + "legacyTransDate": "2024-09-30T16:09:00+00:00", + "paymentMethodSource": "NEW", + "authorizationResponse": { + "amount": null, + "currency": null, + "authorizationDate": null, + "authorizationNumber": null, + "authorizationResult": null, + "authorizationMode": "FULL", + "_type": "V4/PaymentMethod/Details/Cards/CardAuthorizationResponse" + }, + "captureResponse": { + "refundAmount": null, + "refundCurrency": null, + "captureDate": null, + "captureFileNumber": null, + "effectiveRefundAmount": null, + "effectiveRefundCurrency": null, + "_type": "V4/PaymentMethod/Details/Cards/CardCaptureResponse" + }, + "threeDSResponse": { + "authenticationResultData": { + "transactionCondition": null, + "enrolled": null, + "status": null, + "eci": null, + "xid": null, + "cavvAlgorithm": null, + "cavv": null, + "signValid": null, + "brand": null, + "_type": "V4/PaymentMethod/Details/Cards/CardAuthenticationResponse" + }, + "_type": "V4/PaymentMethod/Details/Cards/ThreeDSResponse" + }, + "authenticationResponse": null, + "installmentNumber": null, + "installmentCode": null, + "markAuthorizationResponse": { + "amount": null, + "currency": null, + "authorizationDate": null, + "authorizationNumber": null, + "authorizationResult": null, + "_type": "V4/PaymentMethod/Details/Cards/MarkAuthorizationResponse" + }, + "cardHolderName": null, + "cardHolderPan": "497010XXXXXX0055", + "cardHolderExpiryMonth": 12, + "cardHolderExpiryYear": 2025, + "identityDocumentNumber": null, + "identityDocumentType": null, + "initialIssuerTransactionIdentifier": null, + "productCategory": "DEBIT", + "nature": "CONSUMER_CARD", + "_type": "V4/PaymentMethod/Details/CardDetails" + }, + "paymentMethodDetails": { + "id": "497010XXXXXX0055", + "paymentSource": "OTHER", + "manualValidation": "NO", + "expectedCaptureDate": "2024-09-30T16:09:00+00:00", + "effectiveBrand": "VISA", + "expiryMonth": 12, + "expiryYear": 2025, + "country": "FR", + "issuerCode": null, + "issuerName": "Banque de demo et de l'innovation", + "effectiveProductCode": "F", + "legacyTransId": "930244", + "legacyTransDate": "2024-09-30T16:09:00+00:00", + "paymentMethodSource": "NEW", + "authorizationResponse": { + "amount": null, + "currency": null, + "authorizationDate": null, + "authorizationNumber": null, + "authorizationResult": null, + "authorizationMode": "FULL", + "_type": "V4/PaymentMethod/Details/Cards/CardAuthorizationResponse" + }, + "captureResponse": { + "refundAmount": null, + "refundCurrency": null, + "captureDate": null, + "captureFileNumber": null, + "effectiveRefundAmount": null, + "effectiveRefundCurrency": null, + "_type": "V4/PaymentMethod/Details/Cards/CardCaptureResponse" + }, + "authenticationResponse": null, + "installmentNumber": null, + "installmentCode": null, + "markAuthorizationResponse": { + "amount": null, + "currency": null, + "authorizationDate": null, + "authorizationNumber": null, + "authorizationResult": null, + "_type": "V4/PaymentMethod/Details/Cards/MarkAuthorizationResponse" + }, + "cardHolderName": null, + "cardHolderPan": "497010XXXXXX0055", + "cardHolderExpiryMonth": 12, + "cardHolderExpiryYear": 2025, + "identityDocumentNumber": null, + "identityDocumentType": null, + "initialIssuerTransactionIdentifier": null, + "_type": "V4/PaymentMethod/Details/PaymentMethodDetails" + }, + "acquirerDetails": null, + "fraudManagement": { + "riskControl": [], + "riskAnalysis": [], + "_type": "V4/PaymentMethod/Details/FraudManagement" + }, + "parentTransactionUuid": "dbf4b89ae157499e83bea366c91daaa8", + "mid": "9876357", + "sequenceNumber": 1, + "taxAmount": null, + "preTaxAmount": null, + "taxRate": null, + "externalTransactionId": null, + "nsu": null, + "tid": "001", + "acquirerNetwork": "CB", + "taxRefundAmount": null, + "userInfo": null, + "paymentMethodTokenPreviouslyRegistered": null, + "occurrenceType": "UNITAIRE", + "archivalReferenceId": "L27493024401", + "useCase": null, + "wallet": null, + "_type": "V4/TransactionDetails" + }, + "orderDetails": { + "orderTotalAmount": 10000, + "orderEffectiveAmount": null, + "orderCurrency": "EUR", + "mode": "TEST", + "orderId": "myOrderId-948135", + "metadata": null, + "_type": "V4/OrderDetails" + }, + "operationType": "CREDIT", + "metadata": { + "installment_id": "d9356dd7-19a6-4695-b18e-ad93af41424a" + }, + "_type": "V4/Transaction" + }, + "ticket": "ffd839c87d5947c9a84262ddda3e2055", + "serverDate": "2024-09-30T16:09:01+00:00", + "applicationProvider": "LYRA", + "metadata": null, + "mode": "TEST", + "serverUrl": "https://api.lyra.com", + "_type": "V4/WebService/Response" +} diff --git a/src/backend/joanie/tests/payment/test_backend_base.py b/src/backend/joanie/tests/payment/test_backend_base.py index 7e0ff38c33..f501f5eabe 100644 --- a/src/backend/joanie/tests/payment/test_backend_base.py +++ b/src/backend/joanie/tests/payment/test_backend_base.py @@ -66,6 +66,9 @@ def handle_notification(self, request): def tokenize_card(self, order=None, billing_address=None, user=None): pass + def cancel_or_refund(self, amount, transaction, user_email): + pass + # pylint: disable=too-many-public-methods, too-many-lines @override_settings(JOANIE_CATALOG_NAME="Test Catalog") diff --git a/src/backend/joanie/tests/payment/test_backend_lyra.py b/src/backend/joanie/tests/payment/test_backend_lyra.py index 67ed4d4e76..be0ca22e45 100644 --- a/src/backend/joanie/tests/payment/test_backend_lyra.py +++ b/src/backend/joanie/tests/payment/test_backend_lyra.py @@ -35,7 +35,11 @@ RegisterPaymentFailed, TokenizationCardFailed, ) -from joanie.payment.factories import CreditCardFactory +from joanie.payment.factories import ( + CreditCardFactory, + InvoiceFactory, + TransactionFactory, +) from joanie.payment.models import CreditCard, Transaction from joanie.tests.base import BaseLogMixinTestCase from joanie.tests.payment.base_payment import BasePaymentTestCase @@ -1715,3 +1719,69 @@ def test_payment_backend_lyra_payment_failure_send_mail_use_fallback_language_tr mail.outbox[0].subject, ) self.assertIn("Product 1", email_content) + + @override_settings(JOANIE_PAYMENT_SCHEDULE_LIMITS={100: (30, 35, 35)}) + @responses.activate(assert_all_requests_are_fired=True) + def test_backend_lyra_refund_transaction(self): + """ + When backend refunds a transaction, it should return the resolution mode mode used, + the transaction reference and finally the installment id concerned by the refund. + """ + backend = LyraBackend(self.configuration) + owner = UserFactory(email="john.doe@acme.org") + order = OrderGeneratorFactory( + owner=owner, product__price=100, state=ORDER_STATE_PENDING_PAYMENT + ) + # Set manually the id of the installment + order.payment_schedule[0]["id"] = "d9356dd7-19a6-4695-b18e-ad93af41424a" + child_invoice = InvoiceFactory( + order=order, + total=0, + parent=order.main_invoice, + recipient_address=order.main_invoice.recipient_address, + ) + transaction = TransactionFactory( + total=D(str(order.payment_schedule[0]["amount"])), + invoice=child_invoice, + reference="dbf4b89ae157499e83bea366c91daaa8", # Transaction uuid from payment provider + ) + + with self.open("lyra/responses/refund_transaction_payment.json") as file: + json_response = json.loads(file.read()) + + responses.add( + responses.POST, + "https://api.lyra.com/api-payment/V4/Transaction/CancelOrRefund", + headers={ + "Content-Type": "application/json", + }, + match=[ + responses.matchers.header_matcher( + { + "content-type": "application/json", + "authorization": "Basic Njk4NzYzNTc6dGVzdHBhc3N3b3JkX0RFTU9QUklWQVRFS0VZMjNHNDQ3NXpYWlEyVUE1eDdN", + } + ), + responses.matchers.json_params_matcher( + { + "amount": 3000, + "currency": "EUR", + "customer": {"email": "john.doe@acme.org"}, + "uuid": "dbf4b89ae157499e83bea366c91daaa8", + "resolutionMode": "AUTO", + }, + ), + ], + status=200, + json=json_response, + ) + + response = backend.cancel_or_refund( + order.payment_schedule[0]["amount"], transaction, owner.email + ) + + self.assertEqual(response.get("status"), "REFUNDED") + self.assertEqual( + response.get("installment_id"), "d9356dd7-19a6-4695-b18e-ad93af41424a" + ) + self.assertEqual(response.get("reference"), "3c28bc9cfea343a99edfcbde833f09b9")