From 2e2c6dafe03e3ffe5d26a4b5fc0bc4f7f738400c Mon Sep 17 00:00:00 2001 From: pmaryska Date: Wed, 8 Apr 2026 10:24:33 +0200 Subject: [PATCH] GPOMA-2221: New QR payment feature --- gopay/enums.py | 5 ++ gopay/payments.py | 17 +++++- gopay/utils.py | 2 +- pyproject.toml | 2 +- tests/test_api_qr_payment.py | 115 +++++++++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 tests/test_api_qr_payment.py diff --git a/gopay/enums.py b/gopay/enums.py index 3626feb..0440263 100644 --- a/gopay/enums.py +++ b/gopay/enums.py @@ -276,6 +276,11 @@ class ContentType(StrEnum): FORM = "application/x-www-form-urlencoded" JSON = "application/json" + +class QrCodeFormat(StrEnum): + PNG = "png" + SVG = "svg" + class BnplType(StrEnum): DEFERRED_PAYMENT = "DEFERRED_PAYMENT" PAY_IN_THREE = "PAY_IN_THREE" diff --git a/gopay/payments.py b/gopay/payments.py index 1597d32..65e75fa 100644 --- a/gopay/payments.py +++ b/gopay/payments.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from gopay.api import GoPay, Response -from gopay.enums import ContentType, Currency +from gopay.enums import ContentType, Currency, QrCodeFormat @dataclass @@ -35,6 +35,21 @@ def get_status(self, payment_id: str | int) -> Response: """ return self.gopay.call("GET", f"/payments/payment/{payment_id}") + def get_qr_payment( + self, + payment_id: int | str, + format: QrCodeFormat | None = None, + ) -> Response: + """ + Retrieves QR payment information for a given payment. + GET /api/payments/payment/{id}/qr-payment + Optional query parameter: format (png | svg), defaults to png. + """ + path = f"/payments/payment/{payment_id}/qr-payment" + if format is not None: + path = f"{path}?format={format}" + return self.gopay.call("GET", path) + def refund_payment(self, payment_id: int | str, amount: int) -> Response: """ https://doc.gopay.com#payment-refund diff --git a/gopay/utils.py b/gopay/utils.py index f1b5082..3e48fcd 100644 --- a/gopay/utils.py +++ b/gopay/utils.py @@ -1,2 +1,2 @@ -VERSION = "2.2.6" +VERSION = "2.3.0" DEFAULT_USER_AGENT = "GoPay Python " + VERSION diff --git a/pyproject.toml b/pyproject.toml index 504072a..87ff8ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ name = "gopay" packages = [{include = "gopay"}] readme = "README.md" repository = "https://github.com/gopaycommunity/gopay-python-api" -version = "2.2.6" +version = "2.3.0" [tool.poetry.dependencies] deprecated = "^1.2.14" diff --git a/tests/test_api_qr_payment.py b/tests/test_api_qr_payment.py new file mode 100644 index 0000000..65447d0 --- /dev/null +++ b/tests/test_api_qr_payment.py @@ -0,0 +1,115 @@ +import logging + +from gopay import Payments +from gopay.enums import QrCodeFormat + + +class TestQrPayment: + """ + Integration tests for the QR payment endpoint. + + Prerequisites: + - Set the GOID, CLIENT_ID, CLIENT_SECRET and GATEWAY_URL environment variables. + - The `payments` and `base_payment` fixtures are provided by conftest.py. + """ + + _payment_id: int | str + + def test_create_payment_for_qr(self, payments: Payments, base_payment: dict): + """ + Creates a standard payment that will be used for subsequent QR payment tests. + Stores the payment id on the class so the other tests can re-use it. + """ + response = payments.create_payment(base_payment) + assert response.success, f"Payment creation failed: {response.json}" + + body = response.json + logging.info(f"Created payment: {body}") + + assert "errors" not in body + assert "id" in body + assert body["state"] == "CREATED" + + # Store the payment id for the other tests in this class + TestQrPayment._payment_id = body["id"] + + def test_get_qr_payment_default_format(self, payments: Payments): + """ + Calls GET /api/payments/payment/{id}/qr-payment without specifying format. + Expects a successful response containing amount, currency and qr_code. + """ + payment_id = TestQrPayment._payment_id + response = payments.get_qr_payment(payment_id) + + logging.info(f"QR payment response (default format): {response.json}") + assert response.success, f"QR payment request failed: {response.json}" + + body = response.json + assert "errors" not in body + assert "amount" in body + assert "currency" in body + assert "qr_code" in body + + # qr_code must contain at least one of the known QR types + qr_code = body["qr_code"] + assert any( + key in qr_code for key in ("spayd", "paybysquare", "sepa", "mnb_qr") + ), f"Unexpected qr_code structure: {qr_code}" + + def test_get_qr_payment_png_format(self, payments: Payments): + """ + Calls GET /api/payments/payment/{id}/qr-payment?format=png. + """ + payment_id = TestQrPayment._payment_id + response = payments.get_qr_payment(payment_id, format=QrCodeFormat.PNG) + + logging.info(f"QR payment response (PNG): {response.json}") + assert response.success, f"QR payment PNG request failed: {response.json}" + + body = response.json + assert "errors" not in body + assert "qr_code" in body + + def test_get_qr_payment_svg_format(self, payments: Payments): + """ + Calls GET /api/payments/payment/{id}/qr-payment?format=svg. + """ + payment_id = TestQrPayment._payment_id + response = payments.get_qr_payment(payment_id, format=QrCodeFormat.SVG) + + logging.info(f"QR payment response (SVG): {response.json}") + assert response.success, f"QR payment SVG request failed: {response.json}" + + body = response.json + assert "errors" not in body + assert "qr_code" in body + + def test_get_qr_payment_recipient_structure(self, payments: Payments): + """ + Validates the structure of the recipient block in the QR payment response. + """ + payment_id = TestQrPayment._payment_id + response = payments.get_qr_payment(payment_id) + assert response.success + + body = response.json + assert "recipient" in body + + recipient = body["recipient"] + # name is expected + assert "name" in recipient + + # bank_account block (local + international) is expected + if "bank_account" in recipient: + bank_account = recipient["bank_account"] + if "local" in bank_account: + local = bank_account["local"] + assert "account_number" in local + assert "bank_code" in local + if "international" in bank_account: + international = bank_account["international"] + assert "iban" in international + # bic is not always present (depends on currency/payment type) + assert any( + key in international for key in ("bic", "reference") + ), f"Unexpected international bank_account structure: {international}"