eupago 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. eupago-0.5.0/.github/workflows/docs.yml +46 -0
  2. eupago-0.5.0/.github/workflows/release.yml +50 -0
  3. eupago-0.5.0/.gitignore +37 -0
  4. eupago-0.5.0/.pre-commit-config.yaml +20 -0
  5. eupago-0.5.0/CHANGELOG.md +89 -0
  6. eupago-0.5.0/CODE_OF_CONDUCT.md +34 -0
  7. eupago-0.5.0/CONTRIBUTING.md +38 -0
  8. eupago-0.5.0/LICENSE +21 -0
  9. eupago-0.5.0/PKG-INFO +299 -0
  10. eupago-0.5.0/README.md +247 -0
  11. eupago-0.5.0/SECURITY.md +16 -0
  12. eupago-0.5.0/docs/api/index.md +138 -0
  13. eupago-0.5.0/docs/api/index.pt.md +138 -0
  14. eupago-0.5.0/docs/contributing.md +111 -0
  15. eupago-0.5.0/docs/contributing.pt.md +111 -0
  16. eupago-0.5.0/docs/errors/index.md +392 -0
  17. eupago-0.5.0/docs/errors/index.pt.md +392 -0
  18. eupago-0.5.0/docs/getting-started/configuration.md +73 -0
  19. eupago-0.5.0/docs/getting-started/configuration.pt.md +73 -0
  20. eupago-0.5.0/docs/getting-started/index.md +80 -0
  21. eupago-0.5.0/docs/getting-started/index.pt.md +80 -0
  22. eupago-0.5.0/docs/index.md +90 -0
  23. eupago-0.5.0/docs/index.pt.md +90 -0
  24. eupago-0.5.0/docs/payments/apple-pay.md +65 -0
  25. eupago-0.5.0/docs/payments/apple-pay.pt.md +64 -0
  26. eupago-0.5.0/docs/payments/credit-card.md +180 -0
  27. eupago-0.5.0/docs/payments/credit-card.pt.md +180 -0
  28. eupago-0.5.0/docs/payments/google-pay.md +63 -0
  29. eupago-0.5.0/docs/payments/google-pay.pt.md +63 -0
  30. eupago-0.5.0/docs/payments/index.md +61 -0
  31. eupago-0.5.0/docs/payments/index.pt.md +61 -0
  32. eupago-0.5.0/docs/payments/mbway.md +176 -0
  33. eupago-0.5.0/docs/payments/mbway.pt.md +169 -0
  34. eupago-0.5.0/docs/payments/multibanco.md +153 -0
  35. eupago-0.5.0/docs/payments/multibanco.pt.md +209 -0
  36. eupago-0.5.0/docs/payments/pay-by-link.md +144 -0
  37. eupago-0.5.0/docs/payments/pay-by-link.pt.md +144 -0
  38. eupago-0.5.0/docs/payments/refund.md +184 -0
  39. eupago-0.5.0/docs/payments/refund.pt.md +182 -0
  40. eupago-0.5.0/docs/recipes/django.md +215 -0
  41. eupago-0.5.0/docs/recipes/django.pt.md +215 -0
  42. eupago-0.5.0/docs/recipes/fastapi.md +174 -0
  43. eupago-0.5.0/docs/recipes/fastapi.pt.md +174 -0
  44. eupago-0.5.0/docs/recipes/flask.md +172 -0
  45. eupago-0.5.0/docs/recipes/flask.pt.md +172 -0
  46. eupago-0.5.0/docs/recipes/index.md +49 -0
  47. eupago-0.5.0/docs/recipes/index.pt.md +49 -0
  48. eupago-0.5.0/docs/webhooks/index.md +151 -0
  49. eupago-0.5.0/docs/webhooks/index.pt.md +151 -0
  50. eupago-0.5.0/docs/webhooks/signature.md +214 -0
  51. eupago-0.5.0/docs/webhooks/signature.pt.md +214 -0
  52. eupago-0.5.0/examples/01_mbway_payment.py +46 -0
  53. eupago-0.5.0/examples/02_mbway_auth_capture.py +42 -0
  54. eupago-0.5.0/examples/03_multibanco_reference.py +83 -0
  55. eupago-0.5.0/examples/04_webhook_fastapi.py +62 -0
  56. eupago-0.5.0/examples/05_async_usage.py +48 -0
  57. eupago-0.5.0/examples/06_error_handling.py +52 -0
  58. eupago-0.5.0/examples/07_credit_card_payment.py +66 -0
  59. eupago-0.5.0/examples/08_credit_card_subscription.py +94 -0
  60. eupago-0.5.0/examples/09_apple_pay.py +44 -0
  61. eupago-0.5.0/examples/10_google_pay.py +42 -0
  62. eupago-0.5.0/examples/11_pay_by_link.py +67 -0
  63. eupago-0.5.0/examples/12_refund.py +101 -0
  64. eupago-0.5.0/mkdocs.yml +141 -0
  65. eupago-0.5.0/mkdocs_hooks.py +16 -0
  66. eupago-0.5.0/pyproject.toml +75 -0
  67. eupago-0.5.0/scripts/prod_smoke_test.py +149 -0
  68. eupago-0.5.0/src/eupago/__init__.py +41 -0
  69. eupago-0.5.0/src/eupago/_auth.py +113 -0
  70. eupago-0.5.0/src/eupago/_client.py +120 -0
  71. eupago-0.5.0/src/eupago/_config.py +14 -0
  72. eupago-0.5.0/src/eupago/_http.py +239 -0
  73. eupago-0.5.0/src/eupago/_logging.py +30 -0
  74. eupago-0.5.0/src/eupago/exceptions.py +76 -0
  75. eupago-0.5.0/src/eupago/models/__init__.py +10 -0
  76. eupago-0.5.0/src/eupago/models/_base.py +11 -0
  77. eupago-0.5.0/src/eupago/models/customer.py +10 -0
  78. eupago-0.5.0/src/eupago/models/payment.py +91 -0
  79. eupago-0.5.0/src/eupago/models/webhook.py +24 -0
  80. eupago-0.5.0/src/eupago/py.typed +0 -0
  81. eupago-0.5.0/src/eupago/services/__init__.py +17 -0
  82. eupago-0.5.0/src/eupago/services/_base.py +99 -0
  83. eupago-0.5.0/src/eupago/services/apple_pay.py +157 -0
  84. eupago-0.5.0/src/eupago/services/credit_card.py +588 -0
  85. eupago-0.5.0/src/eupago/services/google_pay.py +154 -0
  86. eupago-0.5.0/src/eupago/services/mbway.py +213 -0
  87. eupago-0.5.0/src/eupago/services/multibanco.py +216 -0
  88. eupago-0.5.0/src/eupago/services/pay_by_link.py +157 -0
  89. eupago-0.5.0/src/eupago/services/refund.py +124 -0
  90. eupago-0.5.0/src/eupago/utils.py +96 -0
  91. eupago-0.5.0/src/eupago/webhooks/__init__.py +84 -0
  92. eupago-0.5.0/src/eupago/webhooks/_parser.py +70 -0
  93. eupago-0.5.0/src/eupago/webhooks/_signature.py +53 -0
  94. eupago-0.5.0/tests/__init__.py +0 -0
  95. eupago-0.5.0/tests/conftest.py +22 -0
  96. eupago-0.5.0/tests/fixtures/mbway_capture_success.json +5 -0
  97. eupago-0.5.0/tests/fixtures/mbway_create_error.json +5 -0
  98. eupago-0.5.0/tests/fixtures/mbway_create_success.json +5 -0
  99. eupago-0.5.0/tests/fixtures/multibanco_create_success.json +6 -0
  100. eupago-0.5.0/tests/fixtures/multibanco_info_paid.json +24 -0
  101. eupago-0.5.0/tests/fixtures/multibanco_info_pending.json +12 -0
  102. eupago-0.5.0/tests/fixtures/webhook_v2_paid.json +15 -0
  103. eupago-0.5.0/tests/integration/__init__.py +0 -0
  104. eupago-0.5.0/tests/integration/infra/.gitignore +14 -0
  105. eupago-0.5.0/tests/integration/infra/README.md +80 -0
  106. eupago-0.5.0/tests/integration/infra/build.sh +22 -0
  107. eupago-0.5.0/tests/integration/infra/handler.py +100 -0
  108. eupago-0.5.0/tests/integration/infra/main.tf +130 -0
  109. eupago-0.5.0/tests/integration/infra/outputs.tf +9 -0
  110. eupago-0.5.0/tests/integration/infra/requirements.txt +1 -0
  111. eupago-0.5.0/tests/integration/infra/variables.tf +29 -0
  112. eupago-0.5.0/tests/integration/sandbox_backoffice.py +223 -0
  113. eupago-0.5.0/tests/integration/test_credit_card_3ds.py +157 -0
  114. eupago-0.5.0/tests/integration/test_credit_card_authorize_capture_live.py +197 -0
  115. eupago-0.5.0/tests/integration/test_credit_card_live.py +57 -0
  116. eupago-0.5.0/tests/integration/test_credit_card_subscription_live.py +192 -0
  117. eupago-0.5.0/tests/integration/test_mbway_auth_capture_live.py +118 -0
  118. eupago-0.5.0/tests/integration/test_pay_by_link_e2e_live.py +130 -0
  119. eupago-0.5.0/tests/integration/test_pay_by_link_live.py +54 -0
  120. eupago-0.5.0/tests/integration/test_refund_live.py +154 -0
  121. eupago-0.5.0/tests/integration/test_subscription_management_live.py +164 -0
  122. eupago-0.5.0/tests/integration/test_webhook_capture.py +168 -0
  123. eupago-0.5.0/tests/unit/__init__.py +0 -0
  124. eupago-0.5.0/tests/unit/test_apple_pay.py +114 -0
  125. eupago-0.5.0/tests/unit/test_auth.py +17 -0
  126. eupago-0.5.0/tests/unit/test_client.py +88 -0
  127. eupago-0.5.0/tests/unit/test_credit_card.py +466 -0
  128. eupago-0.5.0/tests/unit/test_exceptions.py +40 -0
  129. eupago-0.5.0/tests/unit/test_google_pay.py +103 -0
  130. eupago-0.5.0/tests/unit/test_http.py +119 -0
  131. eupago-0.5.0/tests/unit/test_logging.py +20 -0
  132. eupago-0.5.0/tests/unit/test_mbway.py +225 -0
  133. eupago-0.5.0/tests/unit/test_models.py +57 -0
  134. eupago-0.5.0/tests/unit/test_multibanco.py +257 -0
  135. eupago-0.5.0/tests/unit/test_oauth.py +91 -0
  136. eupago-0.5.0/tests/unit/test_pay_by_link.py +124 -0
  137. eupago-0.5.0/tests/unit/test_refund.py +210 -0
  138. eupago-0.5.0/tests/unit/test_service_base.py +107 -0
  139. eupago-0.5.0/tests/unit/test_utils.py +44 -0
  140. eupago-0.5.0/tests/unit/test_webhooks.py +264 -0
@@ -0,0 +1,46 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "docs/**"
8
+ - "src/**"
9
+ - "mkdocs.yml"
10
+ - "mkdocs_hooks.py"
11
+ - "pyproject.toml"
12
+ - ".github/workflows/docs.yml"
13
+ workflow_dispatch:
14
+
15
+ permissions:
16
+ contents: read
17
+ pages: write
18
+ id-token: write
19
+
20
+ concurrency:
21
+ group: pages
22
+ cancel-in-progress: false
23
+
24
+ jobs:
25
+ build:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: "3.12"
32
+ - run: pip install -e ".[docs]"
33
+ - run: mkdocs build --strict
34
+ - uses: actions/upload-pages-artifact@v3
35
+ with:
36
+ path: site
37
+
38
+ deploy:
39
+ needs: build
40
+ runs-on: ubuntu-latest
41
+ environment:
42
+ name: github-pages
43
+ url: ${{ steps.deployment.outputs.page_url }}
44
+ steps:
45
+ - id: deployment
46
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,50 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI via Trusted Publishing (OIDC) — no tokens or secrets.
4
+ # Trigger: publish a GitHub Release (which also creates the tag).
5
+
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ build:
15
+ name: Build distribution
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+ - name: Build sdist + wheel
23
+ run: |
24
+ python -m pip install --upgrade build
25
+ python -m build
26
+ - name: Check artifacts
27
+ run: |
28
+ python -m pip install --upgrade twine
29
+ twine check dist/*
30
+ - uses: actions/upload-artifact@v4
31
+ with:
32
+ name: dist
33
+ path: dist/
34
+
35
+ publish:
36
+ name: Publish to PyPI
37
+ needs: build
38
+ runs-on: ubuntu-latest
39
+ environment:
40
+ name: pypi
41
+ url: https://pypi.org/project/eupago/
42
+ permissions:
43
+ id-token: write # required for OIDC trusted publishing
44
+ steps:
45
+ - uses: actions/download-artifact@v4
46
+ with:
47
+ name: dist
48
+ path: dist/
49
+ - name: Publish to PyPI
50
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,37 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ *.egg
9
+ .eggs/
10
+ *.whl
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .pytest_cache/
14
+ .coverage
15
+ htmlcov/
16
+ .tox/
17
+ .nox/
18
+ .env
19
+ .env.*
20
+ !.env.example
21
+ .venv/
22
+ venv/
23
+ *.log
24
+ .DS_Store
25
+ Thumbs.db
26
+ .idea/
27
+ .vscode/
28
+ *.swp
29
+ *.swo
30
+ *~
31
+ site/
32
+ site/
33
+
34
+ # Local-only working notes
35
+ eupago-duvidas.md
36
+ *.local.md
37
+ CLAUDE.*
@@ -0,0 +1,20 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.4.8
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+
9
+ - repo: https://github.com/pre-commit/mirrors-mypy
10
+ rev: v1.10.0
11
+ hooks:
12
+ - id: mypy
13
+ additional_dependencies: [pydantic>=2.0, httpx>=0.27, cryptography>=42.0]
14
+ args: [--strict, src/]
15
+ pass_filenames: false
16
+
17
+ - repo: https://github.com/gitleaks/gitleaks
18
+ rev: v8.18.2
19
+ hooks:
20
+ - id: gitleaks
@@ -0,0 +1,89 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.5.0] - 2026-05-31
11
+
12
+ First production-validated release. MB WAY, Multibanco, Pay By Link and
13
+ Refunds were exercised against a live eupago production channel
14
+ (`Destrezàvolta`) with real money — 5 transactions paid and 3 refunds
15
+ emitted, all webhooks captured and verified through the SDK. Several
16
+ real wire-shape bugs surfaced and were fixed.
17
+
18
+ ### Added
19
+ - **Subscription management** on `client.credit_card` — four new methods covering the full `/api/management/v1.02/subscriptions` + `/creditcard/edit` surface that the eupago backoffice uses (sync + async):
20
+ - `list_subscriptions()` — returns every subscription on the channel.
21
+ - `get_subscription(subscription_id)` — full detail incl. `nextCollectionDate` and current `autoProcess` / `collectionDay`.
22
+ - `edit_subscription(subscription_id, collection_day=..., auto_process=...)` — changes the billing schedule. With `auto_process=True` eupago bills the registered card itself on every `collection_day` of the period — no need to call `charge_subscription`. Sends as `application/x-www-form-urlencoded`, which the SDK transport now supports.
23
+ - `revoke_subscription(subscription_id)` — cancels an active subscription (raises `SUBSCRIPTION_NOT_FOUND` on `Pendente`).
24
+ - All four take the **integer** `subscriptionId` from the backoffice URL — not the hex `eupagoToken` (`charge_subscription` still takes the hex). The list response does not include the integer ID today; this is documented as a known eupago UX gap.
25
+ - **HTTP transport** now accepts a `data=` parameter for `application/x-www-form-urlencoded` bodies (with the Content-Type override). Used by `edit_subscription`.
26
+ - **`EupagoClient(management_bearer=...)`** — inject a pre-obtained Bearer for the `/api/management/*` endpoints (refunds, transactions, …). Bypasses the OAuth `client_id`/`client_secret` flow. Useful for tests/scripts where the caller already has a token, e.g. from the eupago backoffice login. Production callers should still prefer OAuth.
27
+ - New live integration test `tests/integration/test_refund_live.py` that pays an MB WAY transaction, captures the webhook, then calls `client.refunds.refund(...)` end-to-end and asserts `REFUNDED` + a real `refundId` in the response. Uses `management_bearer` from the backoffice helper as the temporary auth path until eupago issues OAuth credentials.
28
+
29
+ ### Added
30
+ - **`client.refunds.get(refund_id)`** (sync + async) — `GET /api/management/v1.02/refund/{refundId}`. Returns the refund's current state (`identifier`, `reference`, `status`). Particularly useful for Multibanco refunds, which settle asynchronously (`status: "pendente"` until the bank transfer clears, then `"Reembolsado"`).
31
+ - **`eupago.utils.bic_for_pt_iban(iban)`** — helper that maps a Portuguese IBAN's bank code (positions 5–8) to the corresponding BIC. Covers the major retail banks in Portugal (~99% of consumer accounts). Returns `None` for niche banks. Built because Multibanco refunds require a `bic` argument and eupago doesn't expose a lookup API for it.
32
+ - **`WebhookEvent.original_transaction_id`** — populated on refund webhooks (`method="refund"`) with the trid of the original payment being refunded. Lets callers correlate the refund back to the original payment without keeping their own mapping. eupago sends this as `originalTrid` in the webhook payload.
33
+
34
+ ### Docs
35
+ - **Pay By Link behaviour learned from prod smoke tests (2026-05-31):**
36
+ - When the customer completes a Pay By Link payment, the webhook fires as the **chosen method's** webhook (e.g. `method="mbway"`), not a generic "paybylink" event. Your handler doesn't need Pay By Link special-casing — treat the webhook like any direct payment of that method.
37
+ - When a Pay By Link expires unpaid (`expires_at` passes with no payment started), **no webhook fires**. This is different from MB WAY's direct push (which does fire an `"Expired"` webhook). Track expiry from your own clock.
38
+ - **Multibanco refund actually requires `bic` even though docs say optional** — `client.refunds.refund(...)` without `bic` returns `BIC_INVALID` on Multibanco transactions in production. Updated `docs/payments/refund.md` and `examples/12_refund.py` to make `bic` mandatory in Multibanco refund examples (with `BCOMPTPL` for Millennium BCP as the canonical example). Also documented that Multibanco refunds settle asynchronously — the sync response is `"Pendente"`, the settlement webhook arrives later (sometimes hours).
39
+
40
+ ### Fixed
41
+ - **Refund webhook support** — the eupago documentation says no webhook fires on refunds. **In practice it does** (confirmed live in production, 2026-05-31): an async webhook arrives with `method="RB:PT"`, `status="REFUNDED"` (uppercase, distinct from the synchronous response's `"Reembolsado"`), and an `originalTrid` field. The SDK now maps `RB:PT` → `"refund"`, `"REFUNDED"` → `PaymentStatus.REFUNDED`, and surfaces the original trid via `WebhookEvent.original_transaction_id`. Without these mappings the refund webhook fell through to `method="rb:pt"` + `status=PENDING`, silently dropping the actual state.
42
+ - **Status normalization** for cancelled MB WAY: eupago webhooks use the US spelling `"Canceled"` (single L) for transactions the customer rejected on the MB WAY app — confirmed live in production on 2026-05-31. The SDK now maps `Canceled` / `Cancelled` / `Pending` / `Pendente` / `Reembolsado` to the right `PaymentStatus`; previously these fell through to `PaymentStatus.PENDING` (silent loss of state). Added new live smoke-test script (`scripts/prod_smoke_test.py`) used to discover this.
43
+ - **Refund** body shape: was `{value, currency, motivo}`, eupago expects `{amount, reason}` (plus optional `iban`/`bic` for non-MB WAY / non-Card transactions). The old shape returned `AMOUNT_MISSING`. Response parser now reads `refundId` correctly (live-verified: `{"transactionStatus":"Success","refundId":"2788","status":"Reembolsado"}`). Parameter renamed from `value` to `amount` to match the wire field.
44
+ - **MB WAY** request body now includes the required `countryCode` (default `"351"`) and `customerPhone` on every endpoint, not only `create`. Without `countryCode` the `authorize` endpoint returns `CUSTOMERPHONE_MISSING` / `BAD_REQUEST` even though the value was present, because the eupago spec ties the two together.
45
+ - **MB WAY capture** body shape: was `{"payment": {"amount": X}}`, eupago expects `{"payment": {"value": X, "currency": "EUR"}}`. The old body returned `AMOUNT_MISSING`. Fixed and asserted by unit test.
46
+ - **Credit Card capture** now sends the full payment body (amount object + URLs) — the previous empty body returned `AMOUNT_MISSING`. New required parameter `amount`; optional `success_url`/`error_url`/`back_url`/`customer`/`order_id`.
47
+ - **Credit Card subscription**:
48
+ - `create_subscription` now wraps the request with the required `subscription` block (`date`, `autoProcess`, `collectionDay`, `periodicity`, `limitDate`). Without it the endpoint returns 500 BAD_REQUEST. New optional parameters: `start_date`, `periodicity` (default `"Mensal"`), `collection_day` (default `1`), `limit_date` (default 1 year out), `auto_process` (default `False`).
49
+ - Response parser now reads `subscriptionID` and `referenceSubs` (the names the subscription endpoint actually uses) in addition to `transactionID`/`reference`. The `subscriptionID` is mapped to `PaymentResult.transaction_id` so it can be passed straight to `charge_subscription`.
50
+ - `charge_subscription` takes `recurrent_id: str` (eupago returns a hex string, not an int) and accepts the required `success_url`/`error_url`/`back_url` + optional `days_to_capture`.
51
+
52
+ ### Added
53
+ - **MB WAY**: `create_payment`, `authorize`, `capture` (sync + async). Live-verified against the eupago sandbox.
54
+ - **Multibanco**: `create_reference`, `get_info` (sync + async). Live-verified, including the paid `info` response.
55
+ - **Credit Card**: `create_payment`, `authorize`, `capture`, `create_subscription`, `charge_subscription` (sync + async). Full 3D-Secure flow is end-to-end validated by a Playwright integration test using the official sandbox test card (`4018810000150015`, OTP `0101`) — Playwright drives the Shift4 form and the Credorax ACS challenge, and the test asserts the `Paid` webhook lands in the test receiver.
56
+ - **Refunds**: `client.refunds.refund` (sync + async). OAuth-authenticated against `/api/management/v1.02/refund/{trid}`; live verification requires `client_id` / `client_secret` on the channel and a paid transaction to refund. Per the eupago docs, **refunds do not fire webhooks** — verify via the response or the management transactions endpoint. OAuth credentials are issued by eupago support on request (customer.support.eupago.com); they are not the API key and are not self-service in the backoffice.
57
+ - **Apple Pay**: `client.apple_pay.create_payment` (sync + async). Forwards the `PKPaymentToken` from Apple Wallet to `payment.applePayToken`; same verified v1.02 body shape as credit card. Live verification needs a real Wallet-enabled device.
58
+ - **Google Pay**: `client.google_pay.create_payment` (sync + async). Same pattern with `googlePayToken`. Live verification needs a real Google Pay-enabled device.
59
+ - **Pay By Link**: `client.pay_by_link.create_payment` (sync + async). Generates an eupago-hosted checkout URL where the customer picks the payment method (MB WAY, Multibanco, Card, Apple/Google Pay, Cofidis…). Supports optional `expires_at`, `shipping`, `products` line items, and `customer` notification. Body shape verified against the v1.02 reference and live against the sandbox (`tests/integration/test_pay_by_link_live.py`).
60
+ - New `e2e` optional extra (`pip install eupago[e2e]`) — Playwright dep for the headless 3DS test.
61
+ - **`EupagoClient(webhook_secret=...)`** and a **`client.webhooks.parse(body, headers)`** namespace (Stripe-style configuration on the client; the module-level `parse_webhook` stays as the escape hatch).
62
+ - **Encrypted webhook support** (AES-256-CBC) — auto-detected from `X-Initialization-Vector` and the `{"data": "..."}` body shape, validated end-to-end against a real encrypted payload from the sandbox. New `crypto` extra (`pip install eupago[crypto]`).
63
+ - `currency` parameter on MB WAY create/authorize (defaults to `EUR`).
64
+ - Comprehensive typed exception hierarchy with `status_code`, `error_code` (now `int | str | None`) and `message`.
65
+ - PII redaction filter (`_logging.py`) — phone, email, NIF.
66
+ - Audit hook (`client.set_audit_hook(...)`).
67
+ - Headless integration test suite (`tests/integration/`) with a Terraform-managed AWS webhook receiver (Lambda + API Gateway + DynamoDB) and a sandbox-backoffice automation helper, so every paid flow is exercised end-to-end without manual clicks.
68
+ - Lifecycle examples covering every payment method (`examples/01`–`12`): MB WAY (payment, auth+capture), Multibanco (reference, get_info), Credit Card (payment, auth+capture, subscription+charge), Apple Pay, Google Pay, Pay By Link, and a standalone Refund flow — each ending with the refund pattern so the full pay → refund cycle is visible end to end.
69
+
70
+ ### Changed
71
+ - Auth header is now **`Authorization: ApiKey <key>`** (not a header named `ApiKey`). Affects every v1.02 endpoint.
72
+ - Error responses with **string** `code` (e.g. `APIKEY_MISSING`) no longer crash the error path; the message in the `text` field is surfaced.
73
+ - MB WAY create request: `amount` is now `{value, currency}` (object), and the phone goes in `payment.customerPhone` (not `alias`).
74
+ - Webhook v2.0 parsing: `transaction` (singular) — the previously-assumed `transactions` (plural) was incorrect.
75
+ - Webhook signature: `X-Signature` is **base64**(HMAC-SHA256), not hex. For encrypted channels the HMAC payload is the **base64 ciphertext string** (the value of `data`); for cleartext channels it is the **raw body**. Both schemes are auto-detected.
76
+ - AES key for encrypted webhooks: the channel's *Chave Criptográfica* is the **32-byte AES-256 key directly** (UTF-8 bytes). It is not a passphrase to derive with SHA-256.
77
+ - Multibanco `get_info`: paid detection now uses the `estado_referencia == "paga"` field and the `pagamentos` array (the old `data_pagamento`-only heuristic returned PENDING for actually-paid references).
78
+ - Multibanco `order_id` is read from `identificador` (not `id`).
79
+ - v0.6.0 roadmap entry refocused to **webhook docs/recipes only** — no framework adapters, matching Stripe/Mollie.
80
+
81
+ ### Removed
82
+ - The premature `Credit Card`, `Apple Pay` and `Google Pay` scaffolding (services, tests, examples, docs) — they were ahead of the v0.3.0 phase. They will return when that milestone starts.
83
+
84
+ ### Fixed
85
+ - `MB WAY` request body shape against the real v1.02 API (confirmed via sandbox).
86
+ - Webhook decryption error message now points at the `crypto` extra.
87
+
88
+ ## [0.0.1] - 2026-05-26
89
+ - Initial scaffolding.
@@ -0,0 +1,34 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to a positive environment:
15
+
16
+ * Using welcoming and inclusive language
17
+ * Being respectful of differing viewpoints and experiences
18
+ * Gracefully accepting constructive criticism
19
+ * Focusing on what is best for the community
20
+
21
+ Examples of unacceptable behavior:
22
+
23
+ * Trolling, insulting/derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information without explicit permission
26
+
27
+ ## Enforcement
28
+
29
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
30
+ reported to the project maintainer at breathpilatestudio@gmail.com.
31
+
32
+ ## Attribution
33
+
34
+ This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
@@ -0,0 +1,38 @@
1
+ # Contributing
2
+
3
+ ## Setup
4
+
5
+ ```bash
6
+ pip install -e ".[dev]"
7
+ pre-commit install
8
+ pytest
9
+ ```
10
+
11
+ ## Running checks
12
+
13
+ ```bash
14
+ ruff check . # lint
15
+ ruff format . # format
16
+ mypy src/ # type check
17
+ pytest # tests (coverage enforced ≥85%)
18
+ ```
19
+
20
+ ## Pull requests
21
+
22
+ - One feature or fix per PR
23
+ - Include tests for new code
24
+ - Update CHANGELOG.md under `[Unreleased]`
25
+ - All CI checks must pass (lint, types, tests across Python 3.9–3.13)
26
+
27
+ ## Test discipline (please read)
28
+
29
+ Two layers, **both required** for new wire-touching code:
30
+
31
+ - **Unit** (`tests/unit/`) — mock httpx with `respx` and **assert the exact request body** sent on the wire, not just the return value. This is what catches wrong field names / shapes / required-but-missing fields. Coverage gate ≥85%.
32
+ - **Live** (`tests/integration/`) — exercise the operation against the real eupago sandbox (skipped automatically when env vars are missing). When the upstream channel doesn't have a feature provisioned, use `pytest.skip("clear reason …")` rather than letting the test pass silently.
33
+
34
+ For the README / CHANGELOG / roadmap, status is **per operation** (not per service). `service.create_payment` being live-validated does not entitle `service.authorize` to a green check. See the matrix style in `README.md` for the convention.
35
+
36
+ ## Reporting security issues
37
+
38
+ See [SECURITY.md](SECURITY.md) — do not open a public issue for vulnerabilities.
eupago-0.5.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Victor Bilouro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
eupago-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,299 @@
1
+ Metadata-Version: 2.4
2
+ Name: eupago
3
+ Version: 0.5.0
4
+ Summary: Unofficial Python SDK for the eupago payment gateway
5
+ Project-URL: Homepage, https://github.com/bilouro/eupago-python
6
+ Project-URL: Documentation, https://bilouro.github.io/eupago-python/
7
+ Project-URL: Issues, https://github.com/bilouro/eupago-python/issues
8
+ Project-URL: Changelog, https://github.com/bilouro/eupago-python/blob/main/CHANGELOG.md
9
+ Author: Victor Bilouro
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: eupago,mbway,multibanco,payments,portugal
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Office/Business :: Financial
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: httpx>=0.27.0
24
+ Requires-Dist: pydantic>=2.0.0
25
+ Provides-Extra: crypto
26
+ Requires-Dist: cryptography>=42.0; extra == 'crypto'
27
+ Provides-Extra: dev
28
+ Requires-Dist: bandit>=1.7; extra == 'dev'
29
+ Requires-Dist: cryptography>=42.0; extra == 'dev'
30
+ Requires-Dist: mypy>=1.0; extra == 'dev'
31
+ Requires-Dist: pre-commit>=3.0; extra == 'dev'
32
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
33
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
34
+ Requires-Dist: pytest>=7.0; extra == 'dev'
35
+ Requires-Dist: respx>=0.21; extra == 'dev'
36
+ Requires-Dist: ruff>=0.4; extra == 'dev'
37
+ Provides-Extra: django
38
+ Requires-Dist: django>=4.0; extra == 'django'
39
+ Provides-Extra: docs
40
+ Requires-Dist: mkdocs-material; extra == 'docs'
41
+ Requires-Dist: mkdocs-static-i18n; extra == 'docs'
42
+ Requires-Dist: mkdocstrings[python]; extra == 'docs'
43
+ Provides-Extra: e2e
44
+ Requires-Dist: playwright>=1.40; extra == 'e2e'
45
+ Provides-Extra: fastapi
46
+ Requires-Dist: fastapi>=0.100; extra == 'fastapi'
47
+ Provides-Extra: flask
48
+ Requires-Dist: flask>=2.0; extra == 'flask'
49
+ Provides-Extra: integration
50
+ Requires-Dist: boto3>=1.34; extra == 'integration'
51
+ Description-Content-Type: text/markdown
52
+
53
+ # eupago
54
+
55
+ [![PyPI version](https://img.shields.io/pypi/v/eupago)](https://pypi.org/project/eupago/)
56
+ [![Python versions](https://img.shields.io/pypi/pyversions/eupago)](https://pypi.org/project/eupago/)
57
+ [![CI](https://github.com/bilouro/eupago-python/actions/workflows/test.yml/badge.svg)](https://github.com/bilouro/eupago-python/actions/workflows/test.yml)
58
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
59
+ [![Typed](https://img.shields.io/badge/typed-mypy--strict-blue)](https://mypy.readthedocs.io/)
60
+ [![Docs](https://img.shields.io/badge/docs-bilouro.github.io-blue)](https://bilouro.github.io/eupago-python/)
61
+
62
+ The first Python SDK for [eupago](https://www.eupago.com), Portugal's payment gateway.
63
+ MB WAY, Multibanco, and more — in 5 lines of Python.
64
+
65
+ **[Documentation (PT/EN)](https://bilouro.github.io/eupago-python/)** | [Examples](examples/) | [API Reference](https://bilouro.github.io/eupago-python/api/)
66
+
67
+ > **Community SDK** — not affiliated with or endorsed by eupago.
68
+ > For official integrations, visit [eupago.com](https://www.eupago.com/integrations/api-payment-gateway).
69
+
70
+ ## Status
71
+
72
+ Per-operation coverage. **Unit** = `respx`-mocked unit test asserting the wire body.
73
+ **Sandbox** = integration test against the eupago sandbox.
74
+ **Prod** = real production transaction exercised against a live eupago channel
75
+ on 2026-05-31 (real money moved + verified back via webhook).
76
+
77
+ | Operation | Unit | Sandbox | Prod |
78
+ |---|:-:|:-:|:-:|
79
+ | `mbway.create_payment` (sync + async) | ✅ | ✅ | ✅ |
80
+ | `mbway.authorize` / `capture` (sync + async) | ✅ | ⚠️ skip — channel needs *Auth & Capture* | — |
81
+ | `multibanco.create_reference` (sync + async) | ✅ | ✅ | ✅ |
82
+ | `multibanco.get_info` (sync + async) | ✅ | ✅ | — |
83
+ | `credit_card.create_payment` (sync + async, 3DS) | ✅ | ✅ Playwright drives Shift4 + Credorax | — |
84
+ | `credit_card.authorize` / `capture` (sync + async) | ✅ | ⚠️ skip — channel needs *Auth & Capture* | — |
85
+ | `credit_card.create_subscription` / `charge_subscription` (sync + async) | ✅ | ⚠️ partial — channel needs *Subscription* feature | — |
86
+ | `credit_card.list_subscriptions` / `get_subscription` / `edit_subscription` / `revoke_subscription` (sync + async) | ✅ | ✅ | — |
87
+ | `apple_pay.create_payment` (sync + async) | ✅ | ❌ needs a real Apple Wallet token | — |
88
+ | `google_pay.create_payment` (sync + async) | ✅ | ❌ needs a real Google Pay token | — |
89
+ | `pay_by_link.create_payment` (sync + async) | ✅ | ✅ URL only | ✅ |
90
+ | `refunds.refund` (sync + async) | ✅ | ✅ | ✅ |
91
+ | `refunds.get` (sync + async) | ✅ | ✅ | ✅ |
92
+ | Webhooks v2.0 (POST + HMAC, cleartext **and** AES-256-CBC encrypted) | ✅ | ✅ | ✅ |
93
+ | Refund webhook (`method="RB:PT"`, links via `original_transaction_id`) | ✅ | — | ✅ |
94
+ | Webhooks v1.0 (legacy GET) | ✅ | — | — |
95
+ | HTTP transport (retry, audit hook, PII redaction, form-urlencoded support) | ✅ | — | — |
96
+
97
+ Discovered in production and now mapped: `"Canceled"` (US 1-L spelling) → `CANCELLED`,
98
+ `"REFUNDED"` (uppercase) → `REFUNDED`, `"RB:PT"` → method `"refund"`. Multibanco
99
+ refunds settle async (`"Pendente"` → `"Reembolsado"` later via webhook). Pay By
100
+ Link expiry is **silent** — no webhook, link becomes a generic 404 page; track
101
+ `expires_at` yourself.
102
+
103
+ Planned: Direct Debit, Payshop, Cofidis, Floa, PIX, Pagaqui, Paysafecard.
104
+
105
+ ## Installation
106
+
107
+ ```bash
108
+ pip install eupago
109
+ ```
110
+
111
+ Requires Python 3.9+. Runtime deps: [httpx](https://www.python-httpx.org/)
112
+ and [Pydantic v2](https://docs.pydantic.dev/). Add the `crypto` extra
113
+ (`pip install eupago[crypto]`) only if you receive encrypted webhooks.
114
+
115
+ ## Quick Start
116
+
117
+ ```python
118
+ from decimal import Decimal
119
+ from eupago import EupagoClient
120
+
121
+ client = EupagoClient(
122
+ api_key="xxxx-xxxx-xxxx-xxxx-xxxx",
123
+ sandbox=True, # False for production
124
+ )
125
+
126
+ # MB WAY — direct mobile payment
127
+ payment = client.mbway.create_payment(
128
+ order_id="ORD-2026-001",
129
+ amount=Decimal("49.90"),
130
+ phone_number="912345678", # 9-digit Portuguese MB WAY number
131
+ )
132
+
133
+ print(payment.transaction_id) # "txn-abc-123"
134
+ print(payment.status) # PaymentStatus.PENDING
135
+ print(payment.amount) # Decimal("49.90")
136
+ ```
137
+
138
+ ```python
139
+ # Multibanco — entity + reference for ATM/homebanking
140
+ ref = client.multibanco.create_reference(
141
+ order_id="ORD-2026-002",
142
+ amount=Decimal("99.00"),
143
+ )
144
+ print(ref.entity, ref.reference) # "12345", "999888777"
145
+ ```
146
+
147
+ ## Async Support
148
+
149
+ Every method has an async variant — same client, `_async` suffix:
150
+
151
+ ```python
152
+ async with EupagoClient(api_key="...", sandbox=True) as client:
153
+ payment = await client.mbway.create_payment_async(
154
+ order_id="ORD-2026-001",
155
+ amount=Decimal("49.90"),
156
+ phone_number="912345678",
157
+ )
158
+ ```
159
+
160
+ ## Auth & Capture (MB WAY)
161
+
162
+ For two-step payments (authorize first, capture later):
163
+
164
+ ```python
165
+ auth = client.mbway.authorize(
166
+ order_id="ORD-002",
167
+ amount=Decimal("120.00"),
168
+ phone_number="912345678",
169
+ )
170
+
171
+ captured = client.mbway.capture(
172
+ transaction_id=auth.transaction_id,
173
+ amount=Decimal("120.00"),
174
+ )
175
+ ```
176
+
177
+ ## Webhooks
178
+
179
+ Configure the secret once on the client; `client.webhooks.parse` handles both
180
+ cleartext **and** AES-256-CBC encrypted payloads — the SDK auto-detects from
181
+ the headers:
182
+
183
+ ```python
184
+ client = EupagoClient(
185
+ api_key="…",
186
+ webhook_secret="…", # the channel's "Chave Criptográfica"
187
+ )
188
+
189
+ # v2.0 — POST with HMAC signature; decrypts automatically if the channel encrypts
190
+ event = client.webhooks.parse(body=request.body, headers=request.headers)
191
+
192
+ # v1.0 — legacy GET query string
193
+ event = client.webhooks.parse(query_params=dict(request.query_params))
194
+
195
+ print(event.order_id) # "ORD-2026-001"
196
+ print(event.status) # PaymentStatus.PAID
197
+ print(event.amount) # Decimal("49.90")
198
+ print(event.method) # "mbway"
199
+ ```
200
+
201
+ The module-level `eupago.webhooks.parse_webhook(...)` is still available as
202
+ an escape hatch for multi-channel cases that need to pick a secret per call.
203
+
204
+ ## Error Handling
205
+
206
+ All errors inherit from `EupagoError` with typed subclasses:
207
+
208
+ ```python
209
+ from eupago import EupagoClient, AuthenticationError, PaymentError, NetworkError
210
+
211
+ try:
212
+ payment = client.mbway.create_payment(...)
213
+ except AuthenticationError:
214
+ # Invalid API key
215
+ ...
216
+ except PaymentError as e:
217
+ print(e.status_code, e.error_code, e.message)
218
+ except NetworkError:
219
+ # Timeout, connection refused
220
+ ...
221
+ ```
222
+
223
+ ## Configuration
224
+
225
+ ```python
226
+ client = EupagoClient(
227
+ api_key="xxxx-xxxx-xxxx-xxxx-xxxx",
228
+ webhook_secret="…", # The channel's Chave Criptográfica (HMAC + AES key)
229
+ sandbox=True, # Use sandbox environment (default: False)
230
+ timeout=10.0, # Request timeout in seconds (default: 10)
231
+ max_retries=3, # Retry failed GET requests (default: 3)
232
+ # OAuth credentials for management endpoints (refunds, transactions)
233
+ client_id="...",
234
+ client_secret="...",
235
+ )
236
+
237
+ # Audit hook — log every API call to your DB / observability stack
238
+ client.set_audit_hook(
239
+ lambda request, response, duration_ms: log_api_call(request, response, duration_ms)
240
+ )
241
+ ```
242
+
243
+ ## Why This SDK
244
+
245
+ - **Fully typed** — `mypy --strict` passes, `py.typed` marker included. Full autocomplete in VS Code and PyCharm.
246
+ - **Sync + Async** — one client, no separate packages. httpx powers both.
247
+ - **Decimal amounts** — no floating-point surprises with money.
248
+ - **Safe retries** — GET requests retry with exponential backoff + jitter. POSTs never retry (no idempotency keys = risk of duplicate payments).
249
+ - **PII redaction** — phone, email and NIF are auto-redacted from logs.
250
+ - **Webhook verification** — HMAC-SHA256 constant-time signature; AES-256-CBC decryption when the channel encrypts. Both schemes verified against real eupago payloads.
251
+ - **Unified vocabulary** — eupago's API has two generations with inconsistent field names (`valor`/`amount`, `chave`/`ApiKey`). The SDK normalizes everything to consistent English.
252
+ - **Exception hierarchy** — catch `PaymentError`, `NetworkError`, or `EupagoError`. Each carries `status_code`, `error_code` and `message`.
253
+
254
+ ## Development
255
+
256
+ ```bash
257
+ git clone https://github.com/bilouro/eupago-python.git
258
+ cd eupago-python
259
+ python -m venv .venv && source .venv/bin/activate
260
+ pip install -e ".[dev]"
261
+ pre-commit install
262
+ ```
263
+
264
+ Run checks:
265
+
266
+ ```bash
267
+ ruff check . # lint
268
+ ruff format . # format
269
+ mypy src/ # type check (strict)
270
+ pytest # tests with coverage (≥85% enforced)
271
+ ```
272
+
273
+ The default `pytest` runs unit tests only. Live integration tests against the
274
+ sandbox live under `tests/integration/` — see `tests/integration/infra/README.md`
275
+ for the Terraform-managed AWS receiver they need.
276
+
277
+ ## Contributing
278
+
279
+ PRs are welcome — especially for new payment methods on the roadmap, framework
280
+ recipes, docs improvements, or anything that makes the SDK easier to adopt.
281
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
282
+
283
+ ## Production use / Consulting
284
+
285
+ This is a community SDK. If you're integrating it into a production system and
286
+ want **prioritised features, custom payment methods, audit support, or hands-on
287
+ help with eupago's quirks**, you can reach me at **consulting@bilouro.com** —
288
+ happy to help on a paid consulting basis.
289
+
290
+ For general questions, file an issue.
291
+
292
+ ## Security
293
+
294
+ Report vulnerabilities privately — see [SECURITY.md](SECURITY.md). Do not open
295
+ public issues for security bugs.
296
+
297
+ ## License
298
+
299
+ [MIT](LICENSE) — use it however you want.