Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Requirements](#requirements)
- [Pip Installation](#pip-installation)
- [Example](#example)
- [iDIN](#idin)
- [Contribute](#contribute)
- [Versioning](#versioning)
- [Additional information](#additional-information)
Expand Down Expand Up @@ -93,6 +94,25 @@ response = (

Find our full documentation online on [docs.buckaroo.io](https://docs.buckaroo.io).

### iDIN

iDIN lets Dutch banks confirm a consumer's identity on your behalf. It carries no amount or currency — only the return URLs plus the `issuerId` (BIC code of the consumer's bank) service parameter. Three actions are available: `identify()`, `verify()` (age 18+), and `login()`.

```python
response = payments.create_payment("idin", {
"return_url": "https://www.buckaroo.nl",
"return_url_cancel": "https://www.buckaroo.nl/cancel",
"return_url_error": "https://www.buckaroo.nl/error",
"return_url_reject": "https://www.buckaroo.nl/reject",
"service_parameters": {"issuerId": "BANKNL2Y"}, # sandbox issuer
}).identify()

print("key:", response.key)
print("redirect:", response.get_redirect_url())
```

See [`examples/idin.py`](examples/idin.py) for a runnable demo of all three actions.

### Contribute

We really appreciate it when developers contribute to improve the Buckaroo plugins.
Expand Down
78 changes: 78 additions & 0 deletions buckaroo/builders/payments/idin_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from __future__ import annotations
from typing import Dict, Any
from ...models.payment_response import PaymentResponse
from .payment_builder import PaymentBuilder


class IdinBuilder(PaymentBuilder):
"""Builder for iDIN identification, age verification, and login.

iDIN lets Dutch banks confirm a consumer's identity on the merchant's
behalf. Every action is a DataRequest keyed on a single ``issuerId``
(BIC code of the consumer's bank) service parameter:

- ``identify()``: full identification, returns the bank-registered
customer details on the async push.
- ``verify()``: age verification (18+).
- ``login()``: returns a unique consumer ID for login purposes.

The immediate response only carries ``Status`` and the redirect
(``RequiredAction.RedirectURL``); the bank's personal data arrives later
via push and is already parseable by the existing :class:`PaymentResponse`.
"""

def required_fields(self, action: str = "identify") -> Dict[str, Any]:
"""
Get the required fields for this payment method.

iDIN carries no amount or currency; only the ReturnURL family is
required.

Returns:
Dict[str, Any]: Dictionary mapping field names to their current values
"""
return {
"return_url": self._return_url,
"return_url_cancel": self._return_url_cancel,
"return_url_error": self._return_url_error,
"return_url_reject": self._return_url_reject,
}

def get_service_name(self) -> str:
"""Get the service name for iDIN requests."""
return "Idin"

def get_allowed_service_parameters(self, action: str = "identify") -> Dict[str, Any]:
"""Get the allowed service parameters for iDIN requests based on action."""

if action.lower() in ["identify", "verify", "login"]:
return {
"issuerId": {
"type": str,
"required": True,
"description": "BIC code of the issuing bank of the consumer",
},
}

return {}

def identify(self, validate: bool = True, strict_validation: bool = False) -> PaymentResponse:
"""Request full identification from the consumer's bank."""
payment_request = self.build(
"identify", validate=validate, strict_validation=strict_validation
)
return self._post_data_request(payment_request.to_dict())

def verify(self, validate: bool = True, strict_validation: bool = False) -> PaymentResponse:
"""Verify whether the consumer is 18 years or older."""
payment_request = self.build(
"verify", validate=validate, strict_validation=strict_validation
)
return self._post_data_request(payment_request.to_dict())

def login(self, validate: bool = True, strict_validation: bool = False) -> PaymentResponse:
"""Request a unique consumer ID for login purposes."""
payment_request = self.build(
"login", validate=validate, strict_validation=strict_validation
)
return self._post_data_request(payment_request.to_dict())
2 changes: 2 additions & 0 deletions buckaroo/factories/payment_method_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from buckaroo.builders.payments.giftcards_builder import GiftcardsBuilder
from buckaroo.builders.payments.google_pay_builder import GooglePayBuilder
from buckaroo.builders.payments.ideal_qr_builder import IdealQrBuilder
from buckaroo.builders.payments.idin_builder import IdinBuilder
from buckaroo.builders.payments.in3_builder import In3Builder
from buckaroo.builders.payments.kbc_builder import KBCBuilder
from buckaroo.builders.payments.billink_builder import BillinkBuilder
Expand Down Expand Up @@ -68,6 +69,7 @@ class PaymentMethodFactory(BuilderFactory):
"googlepay": GooglePayBuilder,
"ideal": IdealBuilder,
"idealqr": IdealQrBuilder,
"idin": IdinBuilder,
"in3": In3Builder,
"kbc": KBCBuilder,
"knaken": KnakenBuilder,
Expand Down
100 changes: 100 additions & 0 deletions examples/idin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Demo of the iDIN payment method.

iDIN lets Dutch banks confirm a consumer's identity on the merchant's behalf.
Every action is a DataRequest keyed on a single ``issuerId`` (BIC code of the
consumer's bank) service parameter; the sandbox issuer is ``BANKNL2Y``. The
demo drives all three actions: identify, verify (age 18+), and login. Gated
on ``BUCKAROO_STORE_KEY`` / ``BUCKAROO_SECRET_KEY`` env vars.
"""

import os
import sys

# Add parent directory to Python path so the demo can import the SDK in-place.
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from buckaroo.app import Buckaroo

_ISSUER_ID = "BANKNL2Y"


def _have_credentials() -> bool:
if not os.getenv("BUCKAROO_STORE_KEY") or not os.getenv("BUCKAROO_SECRET_KEY"):
print("⚠️ Set BUCKAROO_STORE_KEY and BUCKAROO_SECRET_KEY to run this demo")
return False
return True


def _idin_payload() -> dict:
# iDIN carries no amount/currency; only the ReturnURL family plus the
# issuerId service parameter (BIC code of the consumer's bank).
return {
"return_url": "https://www.buckaroo.nl",
"return_url_cancel": "https://www.buckaroo.nl/cancel",
"return_url_error": "https://www.buckaroo.nl/error",
"return_url_reject": "https://www.buckaroo.nl/reject",
"service_parameters": {"issuerId": _ISSUER_ID},
}


def demo_identify() -> None:
"""Request full identification from the consumer's bank."""
print("\n1. iDIN identify")
print("-" * 40)
if not _have_credentials():
return

try:
app = Buckaroo.from_env()
response = app.payments.create_payment("idin", _idin_payload()).identify()
print(f" status.code={response.status.code.code} key={response.key}")
print(f" redirect={response.get_redirect_url()}")
except Exception as e:
print(f" ❌ {e}")


def demo_verify() -> None:
"""Verify whether the consumer is 18 years or older."""
print("\n2. iDIN verify (age 18+)")
print("-" * 40)
if not _have_credentials():
return

try:
app = Buckaroo.from_env()
response = app.payments.create_payment("idin", _idin_payload()).verify()
print(f" status.code={response.status.code.code} key={response.key}")
print(f" redirect={response.get_redirect_url()}")
except Exception as e:
print(f" ❌ {e}")


def demo_login() -> None:
"""Request a unique consumer ID for login purposes."""
print("\n3. iDIN login")
print("-" * 40)
if not _have_credentials():
return

try:
app = Buckaroo.from_env()
response = app.payments.create_payment("idin", _idin_payload()).login()
print(f" status.code={response.status.code.code} key={response.key}")
print(f" redirect={response.get_redirect_url()}")
except Exception as e:
print(f" ❌ {e}")


def main() -> None:
print("BUCKAROO SDK — IDIN DEMO")
print("=" * 60)
demo_identify()
demo_verify()
demo_login()
print("\n" + "=" * 60)
print("done.")


if __name__ == "__main__":
main()
79 changes: 79 additions & 0 deletions tests/feature/payments/test_idin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Feature test: iDIN identify/verify/login round-trip through the full stack.

Every iDIN action is a DataRequest carrying a single ``issuerId`` service
parameter (BIC code of the consumer's bank); the sandbox issuer is
``BANKNL2Y``. The immediate response only carries the redirect — bank
personal data arrives later via push — so this pins the action name, the
``issuerId`` reaching the wire, and ``response.key`` parsing from the mock.
"""

from tests.support.helpers import Helpers
from tests.support.mock_request import BuckarooMockRequest
from tests.support.recording_mock import recorded_action, recorded_service_parameters

_ISSUER_ID = "BANKNL2Y"


def _idin_payload(**overrides):
payload = {
"return_url": "https://example.com/return",
"return_url_cancel": "https://example.com/cancel",
"return_url_error": "https://example.com/error",
"return_url_reject": "https://example.com/reject",
"service_parameters": {"issuerId": _ISSUER_ID},
}
payload.update(overrides)
return payload


class TestIdinFeature:
def test_identify_sends_action_and_issuer_id(self, buckaroo, mock_strategy):
response_body = Helpers.success_response(
{
"Services": [{"Name": "Idin", "Action": "identify", "Parameters": []}],
"ServiceCode": "IDIN",
}
)
mock_strategy.queue(BuckarooMockRequest.json("POST", "*/json/DataRequest", response_body))

response = buckaroo.payments.create_payment("idin", _idin_payload()).identify()

assert response.key == response_body["Key"]
assert recorded_action(mock_strategy) == "identify"

sent = {p["Name"].lower(): p["Value"] for p in recorded_service_parameters(mock_strategy)}
assert sent["issuerid"] == _ISSUER_ID

def test_verify_sends_action_and_issuer_id(self, buckaroo, mock_strategy):
response_body = Helpers.success_response(
{
"Services": [{"Name": "Idin", "Action": "verify", "Parameters": []}],
"ServiceCode": "IDIN",
}
)
mock_strategy.queue(BuckarooMockRequest.json("POST", "*/json/DataRequest", response_body))

response = buckaroo.payments.create_payment("idin", _idin_payload()).verify()

assert response.key == response_body["Key"]
assert recorded_action(mock_strategy) == "verify"

sent = {p["Name"].lower(): p["Value"] for p in recorded_service_parameters(mock_strategy)}
assert sent["issuerid"] == _ISSUER_ID

def test_login_sends_action_and_issuer_id(self, buckaroo, mock_strategy):
response_body = Helpers.success_response(
{
"Services": [{"Name": "Idin", "Action": "login", "Parameters": []}],
"ServiceCode": "IDIN",
}
)
mock_strategy.queue(BuckarooMockRequest.json("POST", "*/json/DataRequest", response_body))

response = buckaroo.payments.create_payment("idin", _idin_payload()).login()

assert response.key == response_body["Key"]
assert recorded_action(mock_strategy) == "login"

sent = {p["Name"].lower(): p["Value"] for p in recorded_service_parameters(mock_strategy)}
assert sent["issuerid"] == _ISSUER_ID
85 changes: 85 additions & 0 deletions tests/unit/builders/payments/test_idin_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Unit coverage for :class:`IdinBuilder`.

iDIN drives three DataRequest actions — ``identify`` (full identification),
``verify`` (age verification), and ``login`` (unique consumer ID) — each
carrying a single ``issuerId`` (BIC code of the consumer's bank) service
parameter. Bank personal data arrives later via push; the immediate response
only carries the redirect, so no custom response model is needed.
"""

from __future__ import annotations

from buckaroo._buckaroo_client import BuckarooClient
from buckaroo.builders.payments.idin_builder import IdinBuilder
from buckaroo.builders.payments.payment_builder import PaymentBuilder
from buckaroo.factories.payment_method_factory import PaymentMethodFactory


def test_builder_instantiates_as_payment_builder(client: BuckarooClient) -> None:
builder = IdinBuilder(client)
assert isinstance(builder, PaymentBuilder)


def test_get_service_name_returns_idin(client: BuckarooClient) -> None:
assert IdinBuilder(client).get_service_name() == "Idin"


_ISSUER_ID_SPEC = {
"issuerId": {
"type": str,
"required": True,
"description": "BIC code of the issuing bank of the consumer",
},
}


def test_get_allowed_service_parameters_identify_requires_issuer_id(
client: BuckarooClient,
) -> None:
assert IdinBuilder(client).get_allowed_service_parameters("identify") == _ISSUER_ID_SPEC


def test_get_allowed_service_parameters_verify_requires_issuer_id(
client: BuckarooClient,
) -> None:
assert IdinBuilder(client).get_allowed_service_parameters("verify") == _ISSUER_ID_SPEC


def test_get_allowed_service_parameters_login_requires_issuer_id(
client: BuckarooClient,
) -> None:
assert IdinBuilder(client).get_allowed_service_parameters("login") == _ISSUER_ID_SPEC


def test_get_allowed_service_parameters_is_case_insensitive(client: BuckarooClient) -> None:
builder = IdinBuilder(client)
assert builder.get_allowed_service_parameters(
"Identify"
) == builder.get_allowed_service_parameters("identify")


def test_get_allowed_service_parameters_unknown_action_returns_empty(
client: BuckarooClient,
) -> None:
assert IdinBuilder(client).get_allowed_service_parameters("Pay") == {}


def test_required_fields_only_return_url_family(client: BuckarooClient) -> None:
builder = (
IdinBuilder(client)
.return_url("https://example.com/return")
.return_url_cancel("https://example.com/cancel")
.return_url_error("https://example.com/error")
.return_url_reject("https://example.com/reject")
)
assert builder.required_fields("identify") == {
"return_url": "https://example.com/return",
"return_url_cancel": "https://example.com/cancel",
"return_url_error": "https://example.com/error",
"return_url_reject": "https://example.com/reject",
}


def test_create_builder_returns_idin_builder(client: BuckarooClient) -> None:
builder = PaymentMethodFactory.create_builder("idin", client)
assert isinstance(builder, IdinBuilder)
Loading