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
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [Example](#example)
- [iDIN](#idin)
- [Instant Refunds](#instant-refunds)
- [eMandate](#emandate)
- [Contribute](#contribute)
- [Versioning](#versioning)
- [Additional information](#additional-information)
Expand Down Expand Up @@ -132,6 +133,61 @@ print("key:", response.key)

See [`examples/instant_refund.py`](examples/instant_refund.py) for a runnable demo covering both iDEAL and Payconiq.

### eMandate

eMandate is a DataRequest-based solution for managing SEPA direct debit mandates, reached through
`app.solutions` rather than `app.payments`. It comes in two variants that share the same five
actions — only the service name differs:

- `emandate` — retail (B2C)
- `emandateb2b` — business (B2B)

```python
from buckaroo.app import Buckaroo

app = Buckaroo.from_env()

# List available issuers (GetIssuerList — no parameters)
response = app.solutions.create_solution("emandate").issuer_list()

# Create a mandate (CreateMandate — debtorReference is required)
response = app.solutions.create_solution(
"emandate",
{
"service_parameters": {
"debtorReference": "DEBTOR-001",
"debtorBankId": "ABNANL2A",
"sequenceType": "1",
"purchaseId": "PUR-001",
"language": "nl",
}
},
).create_mandate()
mandate_id = response.get_service_parameter("MandateId")

# Look up a mandate's status (GetStatus — mandateId is required)
response = app.solutions.create_solution(
"emandate", {"service_parameters": {"mandateId": mandate_id}}
).status()

# Modify a mandate (ModifyMandate — mandateId is required)
response = app.solutions.create_solution(
"emandate",
{"service_parameters": {"mandateId": mandate_id, "maxAmount": "1000.00"}},
).modify_mandate()

# Cancel a mandate (CancelMandate — mandateId is required)
response = app.solutions.create_solution(
"emandate",
{"service_parameters": {"mandateId": mandate_id, "purchaseId": "PUR-001"}},
).cancel_mandate()
```

The B2B variant exposes the same five methods — swap `"emandate"` for `"emandateb2b"`.

See [`examples/emandate.py`](examples/emandate.py) for a runnable demo of all five actions
against both the B2C and B2B services.

### Contribute

We really appreciate it when developers contribute to improve the Buckaroo plugins.
Expand Down
176 changes: 176 additions & 0 deletions buckaroo/builders/solutions/emandate_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
from typing import Dict, Any
from .solution_builder import SolutionBuilder


class EmandateBuilder(SolutionBuilder):
"""Builder for eMandate solutions (DataRequest-based mandate management)."""

def get_service_name(self) -> str:
"""Get the service name for eMandate."""
return "emandate"

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

if action.lower() in ["getissuerlist"]:
return {}

if action.lower() in ["createmandate"]:
return {
"debtorBankId": {
"type": str,
"required": False,
"description": "BIC of the debtor's bank",
},
"debtorReference": {
"type": str,
"required": True,
"description": "Debtor's reference for the mandate",
},
"sequenceType": {
"type": str,
"required": False,
"description": "Sequence type of the mandate (0 = one-off, 1 = recurring)",
},
"purchaseId": {
"type": str,
"required": False,
"description": "Unique purchase identifier",
},
"language": {
"type": str,
"required": False,
"description": "Language for the mandate flow",
},
"emandateReason": {
"type": str,
"required": False,
"description": "Reason for the mandate",
},
"maxAmount": {
"type": str,
"required": False,
"description": "Maximum amount allowed under the mandate",
},
}

if action.lower() in ["getstatus"]:
return {
"mandateId": {
"type": str,
"required": True,
"description": "Identifier of the mandate to look up",
},
}

if action.lower() in ["modifymandate"]:
return {
"mandateId": {
"type": str,
"required": True,
"description": "Identifier of the mandate to modify",
},
"maxAmount": {
"type": str,
"required": False,
"description": "Maximum amount allowed under the mandate",
},
"language": {
"type": str,
"required": False,
"description": "Language for the mandate flow",
},
"emandateReason": {
"type": str,
"required": False,
"description": "Reason for the mandate",
},
}

if action.lower() in ["cancelmandate"]:
return {
"mandateId": {
"type": str,
"required": True,
"description": "Identifier of the mandate to cancel",
},
"purchaseId": {
"type": str,
"required": False,
"description": "Unique purchase identifier",
},
}

return {}

def issuer_list(self, validate: bool = True) -> Any:
"""Retrieve the list of available eMandate issuers via GetIssuerList."""
payload = self.build("GetIssuerList", validate=validate)
request_data = payload.to_dict()

return self._post_data_request(request_data)

def create_mandate(self, validate: bool = True) -> Any:
"""Create a mandate via CreateMandate.

Requires ``debtorReference`` to be set (via ``add_parameter`` or the
``service_parameters`` payload key); the other Mandate parameters are
optional. Raises :class:`RequiredParameterMissingError` when
``debtorReference`` is missing and ``validate`` is True.
"""
payload = self.build("CreateMandate", validate=validate)
request_data = payload.to_dict()

return self._post_data_request(request_data)

def status(self, validate: bool = True) -> Any:
"""Retrieve the status of a mandate via GetStatus.

Requires ``mandateId`` to be set (via ``add_parameter`` or the
``service_parameters`` payload key). Raises
:class:`RequiredParameterMissingError` when ``mandateId`` is missing
and ``validate`` is True.
"""
payload = self.build("GetStatus", validate=validate)
request_data = payload.to_dict()

return self._post_data_request(request_data)

def modify_mandate(self, validate: bool = True) -> Any:
"""Modify a mandate via ModifyMandate.

Requires ``mandateId`` to be set (via ``add_parameter`` or the
``service_parameters`` payload key); ``maxAmount``, ``language`` and
``emandateReason`` are optional. Raises
:class:`RequiredParameterMissingError` when ``mandateId`` is missing
and ``validate`` is True.
"""
payload = self.build("ModifyMandate", validate=validate)
request_data = payload.to_dict()

return self._post_data_request(request_data)

def cancel_mandate(self, validate: bool = True) -> Any:
"""Cancel a mandate via CancelMandate.

Requires ``mandateId`` to be set (via ``add_parameter`` or the
``service_parameters`` payload key); ``purchaseId`` is optional.
Raises :class:`RequiredParameterMissingError` when ``mandateId`` is
missing and ``validate`` is True.
"""
payload = self.build("CancelMandate", validate=validate)
request_data = payload.to_dict()

return self._post_data_request(request_data)


class EmandateB2BBuilder(EmandateBuilder):
"""Builder for the Business (B2B) eMandate variant.

Inherits every action and parameter spec from :class:`EmandateBuilder`
unchanged; only the service name differs.
"""

def get_service_name(self) -> str:
"""Get the service name for the B2B eMandate variant."""
return "emandateb2b"
3 changes: 3 additions & 0 deletions buckaroo/factories/solution_method_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from .builder_factory import BuilderFactory
from buckaroo.builders.solutions.subscription_builder import SubscriptionBuilder
from buckaroo.builders.solutions.emandate_builder import EmandateB2BBuilder, EmandateBuilder
from buckaroo.builders.solutions.default_builder import DefaultBuilder
from buckaroo.builders.solutions.solution_builder import SolutionBuilder

Expand All @@ -13,6 +14,8 @@ class SolutionMethodFactory(BuilderFactory):
# Registry of available solution methods
_solution_methods: Dict[str, Type[SolutionBuilder]] = {
"subscription": SubscriptionBuilder,
"emandate": EmandateBuilder,
"emandateb2b": EmandateB2BBuilder,
}

@classmethod
Expand Down
Loading
Loading