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.
- eupago-0.5.0/.github/workflows/docs.yml +46 -0
- eupago-0.5.0/.github/workflows/release.yml +50 -0
- eupago-0.5.0/.gitignore +37 -0
- eupago-0.5.0/.pre-commit-config.yaml +20 -0
- eupago-0.5.0/CHANGELOG.md +89 -0
- eupago-0.5.0/CODE_OF_CONDUCT.md +34 -0
- eupago-0.5.0/CONTRIBUTING.md +38 -0
- eupago-0.5.0/LICENSE +21 -0
- eupago-0.5.0/PKG-INFO +299 -0
- eupago-0.5.0/README.md +247 -0
- eupago-0.5.0/SECURITY.md +16 -0
- eupago-0.5.0/docs/api/index.md +138 -0
- eupago-0.5.0/docs/api/index.pt.md +138 -0
- eupago-0.5.0/docs/contributing.md +111 -0
- eupago-0.5.0/docs/contributing.pt.md +111 -0
- eupago-0.5.0/docs/errors/index.md +392 -0
- eupago-0.5.0/docs/errors/index.pt.md +392 -0
- eupago-0.5.0/docs/getting-started/configuration.md +73 -0
- eupago-0.5.0/docs/getting-started/configuration.pt.md +73 -0
- eupago-0.5.0/docs/getting-started/index.md +80 -0
- eupago-0.5.0/docs/getting-started/index.pt.md +80 -0
- eupago-0.5.0/docs/index.md +90 -0
- eupago-0.5.0/docs/index.pt.md +90 -0
- eupago-0.5.0/docs/payments/apple-pay.md +65 -0
- eupago-0.5.0/docs/payments/apple-pay.pt.md +64 -0
- eupago-0.5.0/docs/payments/credit-card.md +180 -0
- eupago-0.5.0/docs/payments/credit-card.pt.md +180 -0
- eupago-0.5.0/docs/payments/google-pay.md +63 -0
- eupago-0.5.0/docs/payments/google-pay.pt.md +63 -0
- eupago-0.5.0/docs/payments/index.md +61 -0
- eupago-0.5.0/docs/payments/index.pt.md +61 -0
- eupago-0.5.0/docs/payments/mbway.md +176 -0
- eupago-0.5.0/docs/payments/mbway.pt.md +169 -0
- eupago-0.5.0/docs/payments/multibanco.md +153 -0
- eupago-0.5.0/docs/payments/multibanco.pt.md +209 -0
- eupago-0.5.0/docs/payments/pay-by-link.md +144 -0
- eupago-0.5.0/docs/payments/pay-by-link.pt.md +144 -0
- eupago-0.5.0/docs/payments/refund.md +184 -0
- eupago-0.5.0/docs/payments/refund.pt.md +182 -0
- eupago-0.5.0/docs/recipes/django.md +215 -0
- eupago-0.5.0/docs/recipes/django.pt.md +215 -0
- eupago-0.5.0/docs/recipes/fastapi.md +174 -0
- eupago-0.5.0/docs/recipes/fastapi.pt.md +174 -0
- eupago-0.5.0/docs/recipes/flask.md +172 -0
- eupago-0.5.0/docs/recipes/flask.pt.md +172 -0
- eupago-0.5.0/docs/recipes/index.md +49 -0
- eupago-0.5.0/docs/recipes/index.pt.md +49 -0
- eupago-0.5.0/docs/webhooks/index.md +151 -0
- eupago-0.5.0/docs/webhooks/index.pt.md +151 -0
- eupago-0.5.0/docs/webhooks/signature.md +214 -0
- eupago-0.5.0/docs/webhooks/signature.pt.md +214 -0
- eupago-0.5.0/examples/01_mbway_payment.py +46 -0
- eupago-0.5.0/examples/02_mbway_auth_capture.py +42 -0
- eupago-0.5.0/examples/03_multibanco_reference.py +83 -0
- eupago-0.5.0/examples/04_webhook_fastapi.py +62 -0
- eupago-0.5.0/examples/05_async_usage.py +48 -0
- eupago-0.5.0/examples/06_error_handling.py +52 -0
- eupago-0.5.0/examples/07_credit_card_payment.py +66 -0
- eupago-0.5.0/examples/08_credit_card_subscription.py +94 -0
- eupago-0.5.0/examples/09_apple_pay.py +44 -0
- eupago-0.5.0/examples/10_google_pay.py +42 -0
- eupago-0.5.0/examples/11_pay_by_link.py +67 -0
- eupago-0.5.0/examples/12_refund.py +101 -0
- eupago-0.5.0/mkdocs.yml +141 -0
- eupago-0.5.0/mkdocs_hooks.py +16 -0
- eupago-0.5.0/pyproject.toml +75 -0
- eupago-0.5.0/scripts/prod_smoke_test.py +149 -0
- eupago-0.5.0/src/eupago/__init__.py +41 -0
- eupago-0.5.0/src/eupago/_auth.py +113 -0
- eupago-0.5.0/src/eupago/_client.py +120 -0
- eupago-0.5.0/src/eupago/_config.py +14 -0
- eupago-0.5.0/src/eupago/_http.py +239 -0
- eupago-0.5.0/src/eupago/_logging.py +30 -0
- eupago-0.5.0/src/eupago/exceptions.py +76 -0
- eupago-0.5.0/src/eupago/models/__init__.py +10 -0
- eupago-0.5.0/src/eupago/models/_base.py +11 -0
- eupago-0.5.0/src/eupago/models/customer.py +10 -0
- eupago-0.5.0/src/eupago/models/payment.py +91 -0
- eupago-0.5.0/src/eupago/models/webhook.py +24 -0
- eupago-0.5.0/src/eupago/py.typed +0 -0
- eupago-0.5.0/src/eupago/services/__init__.py +17 -0
- eupago-0.5.0/src/eupago/services/_base.py +99 -0
- eupago-0.5.0/src/eupago/services/apple_pay.py +157 -0
- eupago-0.5.0/src/eupago/services/credit_card.py +588 -0
- eupago-0.5.0/src/eupago/services/google_pay.py +154 -0
- eupago-0.5.0/src/eupago/services/mbway.py +213 -0
- eupago-0.5.0/src/eupago/services/multibanco.py +216 -0
- eupago-0.5.0/src/eupago/services/pay_by_link.py +157 -0
- eupago-0.5.0/src/eupago/services/refund.py +124 -0
- eupago-0.5.0/src/eupago/utils.py +96 -0
- eupago-0.5.0/src/eupago/webhooks/__init__.py +84 -0
- eupago-0.5.0/src/eupago/webhooks/_parser.py +70 -0
- eupago-0.5.0/src/eupago/webhooks/_signature.py +53 -0
- eupago-0.5.0/tests/__init__.py +0 -0
- eupago-0.5.0/tests/conftest.py +22 -0
- eupago-0.5.0/tests/fixtures/mbway_capture_success.json +5 -0
- eupago-0.5.0/tests/fixtures/mbway_create_error.json +5 -0
- eupago-0.5.0/tests/fixtures/mbway_create_success.json +5 -0
- eupago-0.5.0/tests/fixtures/multibanco_create_success.json +6 -0
- eupago-0.5.0/tests/fixtures/multibanco_info_paid.json +24 -0
- eupago-0.5.0/tests/fixtures/multibanco_info_pending.json +12 -0
- eupago-0.5.0/tests/fixtures/webhook_v2_paid.json +15 -0
- eupago-0.5.0/tests/integration/__init__.py +0 -0
- eupago-0.5.0/tests/integration/infra/.gitignore +14 -0
- eupago-0.5.0/tests/integration/infra/README.md +80 -0
- eupago-0.5.0/tests/integration/infra/build.sh +22 -0
- eupago-0.5.0/tests/integration/infra/handler.py +100 -0
- eupago-0.5.0/tests/integration/infra/main.tf +130 -0
- eupago-0.5.0/tests/integration/infra/outputs.tf +9 -0
- eupago-0.5.0/tests/integration/infra/requirements.txt +1 -0
- eupago-0.5.0/tests/integration/infra/variables.tf +29 -0
- eupago-0.5.0/tests/integration/sandbox_backoffice.py +223 -0
- eupago-0.5.0/tests/integration/test_credit_card_3ds.py +157 -0
- eupago-0.5.0/tests/integration/test_credit_card_authorize_capture_live.py +197 -0
- eupago-0.5.0/tests/integration/test_credit_card_live.py +57 -0
- eupago-0.5.0/tests/integration/test_credit_card_subscription_live.py +192 -0
- eupago-0.5.0/tests/integration/test_mbway_auth_capture_live.py +118 -0
- eupago-0.5.0/tests/integration/test_pay_by_link_e2e_live.py +130 -0
- eupago-0.5.0/tests/integration/test_pay_by_link_live.py +54 -0
- eupago-0.5.0/tests/integration/test_refund_live.py +154 -0
- eupago-0.5.0/tests/integration/test_subscription_management_live.py +164 -0
- eupago-0.5.0/tests/integration/test_webhook_capture.py +168 -0
- eupago-0.5.0/tests/unit/__init__.py +0 -0
- eupago-0.5.0/tests/unit/test_apple_pay.py +114 -0
- eupago-0.5.0/tests/unit/test_auth.py +17 -0
- eupago-0.5.0/tests/unit/test_client.py +88 -0
- eupago-0.5.0/tests/unit/test_credit_card.py +466 -0
- eupago-0.5.0/tests/unit/test_exceptions.py +40 -0
- eupago-0.5.0/tests/unit/test_google_pay.py +103 -0
- eupago-0.5.0/tests/unit/test_http.py +119 -0
- eupago-0.5.0/tests/unit/test_logging.py +20 -0
- eupago-0.5.0/tests/unit/test_mbway.py +225 -0
- eupago-0.5.0/tests/unit/test_models.py +57 -0
- eupago-0.5.0/tests/unit/test_multibanco.py +257 -0
- eupago-0.5.0/tests/unit/test_oauth.py +91 -0
- eupago-0.5.0/tests/unit/test_pay_by_link.py +124 -0
- eupago-0.5.0/tests/unit/test_refund.py +210 -0
- eupago-0.5.0/tests/unit/test_service_base.py +107 -0
- eupago-0.5.0/tests/unit/test_utils.py +44 -0
- 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
|
eupago-0.5.0/.gitignore
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/eupago/)
|
|
56
|
+
[](https://pypi.org/project/eupago/)
|
|
57
|
+
[](https://github.com/bilouro/eupago-python/actions/workflows/test.yml)
|
|
58
|
+
[](LICENSE)
|
|
59
|
+
[](https://mypy.readthedocs.io/)
|
|
60
|
+
[](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.
|