Skip to content

Commit

Permalink
🚑️(backend) manage refused zero click payments
Browse files Browse the repository at this point in the history
Currently, if a zero click payment is refused, our logic can consider this one
as accepted as we are checking the wrong response attribute...
  • Loading branch information
jbpenrath committed Oct 23, 2024
1 parent 0fe9bc9 commit 7b636fe
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to

## [Unreleased]

### Fixed

- Manage zero click refused payment

## [2.9.0] - 2024-10-22

### Added
Expand Down
8 changes: 5 additions & 3 deletions src/backend/joanie/payment/backends/lyra/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,12 +288,13 @@ def create_zero_click_payment(self, order, installment, credit_card_token):
}

response_json = self._call_api(url, payload)
if response_json.get("status") != "SUCCESS":
answer = response_json.get("answer")

if answer["orderStatus"] != "PAID":
self._do_on_payment_failure(order, installment["id"])
return False

answer = response_json.get("answer")
billing_details = answer["customer"]["billingDetails"]

payment = {
"id": answer["transactions"][0]["uuid"],
"installment_id": installment["id"],
Expand All @@ -312,6 +313,7 @@ def create_zero_click_payment(self, order, installment, credit_card_token):
order=order,
payment=payment,
)

return True

def _check_hash(self, post_data):
Expand Down
113 changes: 112 additions & 1 deletion src/backend/joanie/tests/payment/test_backend_lyra.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@

from joanie.core.enums import (
ORDER_STATE_COMPLETED,
ORDER_STATE_NO_PAYMENT,
ORDER_STATE_PENDING,
ORDER_STATE_PENDING_PAYMENT,
PAYMENT_STATE_PAID,
PAYMENT_STATE_REFUSED,
)
from joanie.core.factories import (
OrderFactory,
Expand Down Expand Up @@ -879,7 +881,7 @@ def test_payment_backend_lyra_create_zero_click_payment(self):
).exists()
)

# If the installment payment is success, the order state chenges to pending payment
# If the installment payment is success, the order state changes to pending payment
order.refresh_from_db()
self.assertEqual(order.state, ORDER_STATE_PENDING_PAYMENT)
# First installment is paid
Expand Down Expand Up @@ -1535,6 +1537,115 @@ def test_backend_lyra_create_zero_click_payment_server_error(self):
]
self.assertLogsEquals(logger.records, expected_logs)

@responses.activate(assert_all_requests_are_fired=True)
def test_backend_lyra_create_zero_click_payment_refused(self):
"""
When a zero click payment is refused, the related order should be updated accordingly.
"""
backend = LyraBackend(self.configuration)
owner = UserFactory(
email="[email protected]",
first_name="John",
last_name="Doe",
language="en-us",
)
product = ProductFactory(price=D("10.00"), title="Product 1")
product.translations.create(language_code="fr-fr", title="Produit 1")
order = OrderGeneratorFactory(
state=ORDER_STATE_PENDING,
owner=owner,
product=product,
)
# Force the installments id to match the stored request
first_installment = order.payment_schedule[0]
first_installment["id"] = "d9356dd7-19a6-4695-b18e-ad93af41424a"
order.save()
first_installment_amount = order.payment_schedule[0]["amount"]
credit_card = order.credit_card

with self.open("lyra/responses/create_zero_click_payment.json") as file:
json_response = json.loads(file.read())

json_response["answer"]["transactions"][0]["uuid"] = "first_transaction_id"
json_response["answer"]["orderStatus"] = "UNPAID"
json_response["answer"]["orderDetails"]["orderTotalAmount"] = int(
first_installment_amount.sub_units
)

responses.add(
responses.POST,
"https://api.lyra.com/api-payment/V4/Charge/CreatePayment",
headers={
"Content-Type": "application/json",
},
match=[
responses.matchers.header_matcher(
{
"content-type": "application/json",
"authorization": "Basic Njk4NzYzNTc6dGVzdHBhc3N3b3JkX0RFTU9QUklWQVRFS0VZMjNHNDQ3NXpYWlEyVUE1eDdN",
}
),
responses.matchers.json_params_matcher(
{
"amount": int(first_installment_amount * 100),
"currency": "EUR",
"customer": {
"email": "[email protected]",
"reference": str(owner.id),
"shippingDetails": {
"shippingMethod": "DIGITAL_GOOD",
},
},
"orderId": str(order.id),
"formAction": "SILENT",
"paymentMethodToken": credit_card.token,
"transactionOptions": {
"cardOptions": {
"initialIssuerTransactionIdentifier": credit_card.initial_issuer_transaction_identifier,
}
},
"ipnTargetUrl": "https://example.com/api/v1.0/payments/notifications",
"metadata": {
"installment_id": order.payment_schedule[0]["id"],
},
}
),
],
status=200,
json=json_response,
)

response = backend.create_zero_click_payment(
order, order.payment_schedule[0], credit_card.token
)

self.assertFalse(response)

# Invoices are not created
self.assertEqual(order.invoices.count(), 1)
self.assertIsNotNone(order.main_invoice)
self.assertEqual(order.main_invoice.children.count(), 0)

# Transaction is created
self.assertFalse(
Transaction.objects.filter(
invoice__parent__order=order,
total=first_installment_amount.as_decimal(),
reference="first_transaction_id",
).exists()
)

# If the installment payment is refused, the order state changes to no payment
order.refresh_from_db()
self.assertEqual(order.state, ORDER_STATE_NO_PAYMENT)
# Installment is refused
self.assertEqual(order.payment_schedule[0]["state"], PAYMENT_STATE_REFUSED)

# Mail is sent
self._check_installment_refused_email_sent(owner.email, order)

mail.outbox.clear()

@patch.object(BasePaymentBackend, "_send_mail_refused_debit")
def test_payment_backend_lyra_handle_notification_payment_failure_sends_email(
self, mock_send_mail_refused_debit
Expand Down

0 comments on commit 7b636fe

Please sign in to comment.