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
19 changes: 16 additions & 3 deletions buckaroo/builders/payments/in3_builder.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import Dict, Any
from .payment_builder import PaymentBuilder
from .capabilities.authorize_capture_capable import AuthorizeCaptureCapable


class In3Builder(PaymentBuilder):
class In3Builder(PaymentBuilder, AuthorizeCaptureCapable):
"""Builder for IN3 payments with bank transfer capabilities."""

def get_service_name(self) -> str:
Expand All @@ -12,8 +13,8 @@ def get_service_name(self) -> str:
def get_allowed_service_parameters(self, action: str = "Pay") -> Dict[str, Any]:
"""Get the allowed service parameters for IN3 payments based on action."""

if action.lower() in ["pay"]:
return {
if action.lower() in ["pay", "authorize"]:
params = {
"billingCustomer": {
"type": list,
"required": True,
Expand All @@ -27,4 +28,16 @@ def get_allowed_service_parameters(self, action: str = "Pay") -> Dict[str, Any]:
"article": {"type": list, "required": True, "description": "IN3 articles"},
}

# Authorize is only used for the ABN AMRO "Zakelijk op rekening"
# (business-on-account) flow, which the gateway routes via the
# required Route parameter (e.g. "abn_b2b"). Pay does not use it.
if action.lower() == "authorize":
params["route"] = {
"type": str,
"required": True,
"description": 'Financing route, e.g. "abn_b2b" for ABN AMRO business',
}

return params

return {}
145 changes: 145 additions & 0 deletions examples/in3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Demo of the In3 payment method.

Shows the regular ``.pay()`` flow and the ABN-AMRO "Zakelijk op rekening"
(business-on-account) ``.authorize()`` / ``.capture()`` flow. All demos are 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


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 demo_pay() -> None:
"""Regular In3 flow: pay in three installments."""
print("\n1. In3 pay")
print("-" * 40)
if not _have_credentials():
return

try:
app = Buckaroo.from_env()
response = app.payments.create_payment(
"in3",
{
"currency": "EUR",
"amount": 250.00,
"description": "in3 pay demo",
"invoice": "IN3-PAY-001",
"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": {
"article": [
{"Description": "Widget", "Quantity": "2", "GrossUnitPrice": "125.00"},
],
"billingCustomer": [_customer()],
"shippingCustomer": [_customer()],
},
},
).pay()
print(f" status.code={response.status.code.code} redirect={response.get_redirect_url()}")
except Exception as e:
print(f" ❌ {e}")


def demo_authorize_capture() -> None:
"""ABN-AMRO "Zakelijk op rekening": authorize first, capture later.

This business-on-account flow uses ``.authorize()`` instead of ``.pay()`` and
requires the ``route`` service parameter set to ``"AbnB2b"``. Note the exact
Buckaroo field names: ``GrossUnitPrice`` (not Price), ``StreetNumber`` (not
house number), ``CountryCode`` (not country). Capture needs no service params.
"""
print("\n2. In3 authorize / capture (ABN-AMRO Zakelijk op rekening)")
print("-" * 40)
if not _have_credentials():
return

try:
app = Buckaroo.from_env()

billing = _customer()
billing.update({"Category": "B2B", "CompanyName": "Acme B.V.", "CocNumber": "12345678"})

authorize_response = app.payments.create_payment(
"in3",
{
"currency": "EUR",
"amount": 250.00,
"description": "in3 authorize demo",
"invoice": "IN3-AUTH-001",
"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": {
"route": "AbnB2b",
"article": [
{"Description": "Widget", "Quantity": "2", "GrossUnitPrice": "125.00"},
],
"billingCustomer": [billing],
"shippingCustomer": [_customer()],
},
},
).authorize()
print(
f" authorize: status.code={authorize_response.status.code.code} "
f"key={authorize_response.key}"
)

# Once the order is confirmed, capture the authorized amount using the
# transaction key from the authorize response.
capture_response = app.payments.create_payment(
"in3",
{
"currency": "EUR",
"amount": 250.00,
"description": "in3 capture demo",
"invoice": "IN3-AUTH-001",
},
).capture(original_transaction_key=authorize_response.key, amount=250.00)
print(f" capture: status.code={capture_response.status.code.code}")
except Exception as e:
print(f" ❌ {e}")


def _customer() -> dict:
"""A minimal In3 customer using the exact Buckaroo field names."""
return {
"FirstName": "John",
"LastName": "Doe",
"Phone": "0612345678",
"Email": "john@example.com",
"Street": "Hoofdstraat",
"StreetNumber": "1",
"PostalCode": "1000AA",
"City": "Amsterdam",
"CountryCode": "NL",
}


def main() -> None:
print("BUCKAROO SDK — IN3 DEMOS")
print("=" * 60)
demo_pay()
demo_authorize_capture()
print("\n" + "=" * 60)
print("done.")


if __name__ == "__main__":
main()
109 changes: 108 additions & 1 deletion tests/feature/payments/test_in3.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Feature test: in3 pay() round-trip through full stack with MockBuckaroo."""

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


class TestIn3Feature:
Expand All @@ -13,7 +15,7 @@ def test_in3_pay_returns_pending_with_redirect(self, buckaroo, mock_strategy):
payload_overrides={"amount": 25.00, "description": "Test in3"},
service_params={
"article": [
{"description": "Widget", "quantity": "2", "price": "12.50"},
{"description": "Widget", "quantity": "2", "GrossUnitPrice": "12.50"},
],
"billingCustomer": [
{"firstName": "John", "lastName": "Doe"},
Expand All @@ -23,3 +25,108 @@ def test_in3_pay_returns_pending_with_redirect(self, buckaroo, mock_strategy):
],
},
)

def test_in3_authorize_returns_pending_with_redirect(self, buckaroo, mock_strategy):
def add_service_params(builder):
builder.add_parameter("route", "AbnB2b")
builder.add_parameter(
"article", [{"description": "Widget", "quantity": "2", "GrossUnitPrice": "12.50"}]
)
builder.add_parameter("billingCustomer", [{"firstName": "John", "lastName": "Doe"}])
builder.add_parameter("shippingCustomer", [{"firstName": "John", "lastName": "Doe"}])

Helpers.assert_action_returns_pending_with_redirect(
buckaroo,
mock_strategy,
method="in3",
invoice="INV-IN3-002",
action_name="Authorize",
call_method="authorize",
payload_overrides={"amount": 25.00, "description": "Test in3 authorize"},
extra_builder_setup=add_service_params,
)

def test_in3_authorize_then_capture_round_trip(self, buckaroo, mock_strategy):
auth_response_body = Helpers.pending_redirect_response("in3", "Authorize")
mock_strategy.queue(
BuckarooMockRequest.json("POST", "*/json/transaction", auth_response_body)
)

builder = buckaroo.payments.create_payment(
"in3",
Helpers.standard_payload(
invoice="INV-IN3-003", amount=25.00, description="Test in3 authorize"
),
)
builder.add_parameter("route", "AbnB2b")
builder.add_parameter(
"article", [{"description": "Widget", "quantity": "2", "GrossUnitPrice": "12.50"}]
)
builder.add_parameter("billingCustomer", [{"firstName": "John", "lastName": "Doe"}])
builder.add_parameter("shippingCustomer", [{"firstName": "John", "lastName": "Doe"}])
authorize_response = builder.authorize()
assert authorize_response.is_pending()

capture_response_body = Helpers.success_response(
{
"Services": [{"Name": "in3", "Action": "Capture", "Parameters": []}],
"ServiceCode": "in3",
}
)
mock_strategy.queue(
BuckarooMockRequest.json("POST", "*/json/transaction", capture_response_body)
)
capture_response = builder.capture(
original_transaction_key=authorize_response.key, amount=25.00
)

assert capture_response.status.code.code == 190
assert capture_response.key == capture_response_body["Key"]

def test_in3_authorize_with_business_customer_returns_pending_with_redirect(
self, buckaroo, mock_strategy
):
"""ABN-AMRO "Zakelijk op rekening" (business-on-account) runs through the
In3 Authorize action with route="AbnB2b". The business customer is marked
by Category=B2B/CompanyName/CocNumber inside billingCustomer; those
sub-fields flow through the existing billingCustomer list group.
"""

def add_service_params(builder):
builder.add_parameter("route", "AbnB2b")
builder.add_parameter(
"article", [{"description": "Widget", "quantity": "2", "GrossUnitPrice": "12.50"}]
)
builder.add_parameter(
"billingCustomer",
[
{
"Category": "B2B",
"CompanyName": "Acme B.V.",
"CocNumber": "12345678",
"firstName": "John",
"lastName": "Doe",
}
],
)
builder.add_parameter("shippingCustomer", [{"firstName": "John", "lastName": "Doe"}])

Helpers.assert_action_returns_pending_with_redirect(
buckaroo,
mock_strategy,
method="in3",
invoice="INV-IN3-004",
action_name="Authorize",
call_method="authorize",
payload_overrides={"amount": 25.00, "description": "Test in3 authorize B2B"},
extra_builder_setup=add_service_params,
)

billing_params = {
param["Name"]: param["Value"]
for param in recorded_service_parameters(mock_strategy)
if param["GroupType"] == "Billingcustomer"
}
assert billing_params["Category"] == "B2B"
assert billing_params["Companyname"] == "Acme B.V."
assert billing_params["Cocnumber"] == "12345678"
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ def test_capability_method_present_and_callable(
EXPECTED_CAPABILITIES: Dict[str, set] = {
"creditcard": {EncryptedPayCapable, AuthorizeCaptureCapable},
"riverty": {AuthorizeCaptureCapable},
"in3": {AuthorizeCaptureCapable},
"ideal": {BankTransferCapabilities, InstantRefundCapable, FastCheckoutCapable},
"paybybank": {BankTransferCapabilities, InstantRefundCapable, FastCheckoutCapable},
"payconiq": {BankTransferCapabilities, InstantRefundCapable, FastCheckoutCapable},
Expand Down
19 changes: 18 additions & 1 deletion tests/unit/builders/payments/test_in3_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,24 @@ def test_get_allowed_service_parameters_pay_is_case_insensitive(client):
)


@pytest.mark.parametrize("action", ["Refund", "Capture", "Authorize", "UnknownAction"])
def test_get_allowed_service_parameters_authorize_snapshot(client):
builder = In3Builder(client)

authorize = builder.get_allowed_service_parameters("Authorize")

# Authorize allows every Pay param plus the required Route parameter
# (In3 Authorize only exists for the ABN AMRO "Zakelijk op rekening" flow).
assert authorize == {
**builder.get_allowed_service_parameters("Pay"),
"route": {
"type": str,
"required": True,
"description": 'Financing route, e.g. "abn_b2b" for ABN AMRO business',
},
}


@pytest.mark.parametrize("action", ["Refund", "Capture", "UnknownAction"])
def test_get_allowed_service_parameters_non_pay_returns_empty(client, action):
assert In3Builder(client).get_allowed_service_parameters(action) == {}

Expand Down
Loading