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
2 changes: 1 addition & 1 deletion buckaroo/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = "0.2.0"
VERSION = "0.2.1"
4 changes: 3 additions & 1 deletion buckaroo/builders/base_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,10 @@ def add_parameter(
# Convert value to string for API compatibility
str_value = str(value).lower() if isinstance(value, bool) else str(value)

# Top-level names keep internal caps ("CustomerFirstName"); grouped
# names stay flattened via capitalize().
parameter = Parameter(
name=key.capitalize(),
name=key.capitalize() if group_type else _upper_first(key),
value=str_value,
group_type=_upper_first(group_type) if group_type else None,
group_id=group_id,
Expand Down
6 changes: 3 additions & 3 deletions buckaroo/builders/solutions/credit_management_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ class CreditManagementBuilder(SolutionBuilder):
# type, then caller-friendly field name -> wire field name (matched
# case-insensitively against the already-added ``Parameter``s).
# DebtorInfo maps the debtor's code to wire name "Debtorcode" (not
# "Code", unlike AddOrUpdateDebtor/CreateInvoice); the wire name is run
# through ``.capitalize()`` like every other parameter name, so the
# camelCase written here does not survive as-is.
# "Code", unlike AddOrUpdateDebtor/CreateInvoice); as a grouped parameter
# its wire name is run through ``.capitalize()``, so the camelCase written
# here does not survive as-is.
_WIRE_NAMES: Dict[str, Dict[str, Dict[str, str]]] = {
"debtorinfo": {"Debtor": {"code": "DebtorCode"}},
}
Expand Down
4 changes: 2 additions & 2 deletions tests/feature/payments/test_banking.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ def test_banking_payment_order_returns_success(self, buckaroo, mock_strategy):
service = sent["Services"]["ServiceList"][0]
assert service["Name"] == "Banking"
assert service["Action"] == "PaymentOrder"
assert {"Name": "Accountholdername", "Value": "Arensman"} in [
assert {"Name": "AccountHolderName", "Value": "Arensman"} in [
{"Name": p["Name"], "Value": p["Value"]} for p in service["Parameters"]
]
assert {"Name": "Iban", "Value": "NL44RABO0123456789"} in [
assert {"Name": "IBAN", "Value": "NL44RABO0123456789"} in [
{"Name": p["Name"], "Value": p["Value"]} for p in service["Parameters"]
]
9 changes: 5 additions & 4 deletions tests/feature/payments/test_giftcards.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ def test_giftcards_pay_sends_cardnumber_and_pin_on_the_wire(
):
"""service_parameters dict must reach ServiceList[0].Parameters.

The SDK capitalizes parameter names, so ``"PIN"`` becomes ``"Pin"``
on the wire. Assert both the value pairing and the presence of both
keys so a builder that silently drops service_parameters would fail.
Top-level param names keep their internal casing, so ``"PIN"`` reaches
the wire as ``"PIN"``. Assert both the value pairing and the presence
of both keys so a builder that silently drops service_parameters would
fail.
"""
recording_mock.queue(
BuckarooMockRequest.json(
Expand All @@ -44,7 +45,7 @@ def test_giftcards_pay_sends_cardnumber_and_pin_on_the_wire(
assert recorded_action(recording_mock) == "Pay"
params = {p["Name"]: p["Value"] for p in recorded_service_parameters(recording_mock)}
assert params.get("Cardnumber") == "1234567890123456"
assert params.get("Pin") == "1234"
assert params.get("PIN") == "1234"

def test_giftcards_pay_redirect_mode_omits_card_parameters(
self, recording_buckaroo, recording_mock
Expand Down
8 changes: 4 additions & 4 deletions tests/feature/payments/test_klarna.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def test_klarna_pay_as_capture_attaches_data_request_key(self, buckaroo, mock_st
body = json.loads(mock_strategy.calls[-1]["data"])
services = body["Services"]["ServiceList"]
names = [p["Name"] for p in services[0]["Parameters"]]
assert "Datarequestkey" in names
assert "DataRequestKey" in names

def test_klarna_cancel_reservation_posts_data_request_key(self, buckaroo, mock_strategy):
"""Follow-up actions carry the Buckaroo DataRequestKey (no reservation
Expand All @@ -79,7 +79,7 @@ def test_klarna_cancel_reservation_posts_data_request_key(self, buckaroo, mock_s
assert service["Action"] == "CancelReservation"
assert "OriginalTransactionKey" not in body
names = [p["Name"] for p in service["Parameters"]]
assert "Datarequestkey" in names
assert "DataRequestKey" in names

def test_klarna_update_reservation_dispatches_to_data_request(self, buckaroo, mock_strategy):
response_body = Helpers.pending_redirect_response("klarna", action="UpdateReservation")
Expand Down Expand Up @@ -136,5 +136,5 @@ def test_klarna_pay_carries_shipping_details_on_transaction(self, buckaroo, mock
service = json.loads(mock_strategy.calls[-1]["data"])["Services"]["ServiceList"][0]
assert service["Action"] == "Pay"
names = [p["Name"] for p in service["Parameters"]]
assert "Datarequestkey" in names
assert "Trackingnumber" in names
assert "DataRequestKey" in names
assert "TrackingNumber" in names
28 changes: 14 additions & 14 deletions tests/feature/solutions/test_credit_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ def test_create_invoice_posts_service_parameters_and_returns_invoice_key(
}
assert ("Invoice", "") not in params
assert ("Currency", "") not in params
assert params[("Invoiceamount", "")] == "250.00"
assert params[("Duedate", "")] == "2026-09-01"
assert params[("Schemekey", "")] == "SCHEME-2"
assert params[("InvoiceAmount", "")] == "250.00"
assert params[("DueDate", "")] == "2026-09-01"
assert params[("SchemeKey", "")] == "SCHEME-2"
assert params[("Code", "Debtor")] == "DEBTOR-999"

def test_create_invoice_without_required_params_raises_before_wire(self, buckaroo):
Expand Down Expand Up @@ -201,7 +201,7 @@ def test_debtor_file_actions_post_debtor_file_guid(
assert response.status.code.code == 190
assert recorded_action(mock_strategy) == action
params = {p["Name"]: p["Value"] for p in recorded_service_parameters(mock_strategy)}
assert params["Debtorfileguid"] == "FILE-GUID-999"
assert params["DebtorFileGuid"] == "FILE-GUID-999"

@pytest.mark.parametrize("method_name", ["resume_debtor_file", "pause_debtor_file"])
def test_debtor_file_actions_without_debtor_file_guid_raise_before_wire(
Expand Down Expand Up @@ -271,7 +271,7 @@ def test_create_credit_note_posts_original_invoice_and_debtor(self, buckaroo, mo
for p in recorded_service_parameters(mock_strategy)
}
assert ("Invoice", "") not in params
assert params[("Originalinvoicenumber", "")] == "INV-999"
assert params[("OriginalInvoiceNumber", "")] == "INV-999"
assert params[("Code", "Debtor")] == "DEBTOR-999"

def test_create_credit_note_without_debtor_raises_before_wire(self, buckaroo):
Expand Down Expand Up @@ -314,7 +314,7 @@ def test_add_or_update_product_lines_posts_indexed_articles(self, buckaroo, mock
(p["Name"], p["GroupType"], p["GroupID"]): p["Value"]
for p in recorded_service_parameters(mock_strategy)
}
assert params[("Invoicekey", "", "")] == "INVK-999"
assert params[("InvoiceKey", "", "")] == "INVK-999"
assert params[("Productid", "ProductLine", "1")] == "SKU-1"
assert params[("Productname", "ProductLine", "1")] == "Widget"
assert params[("Quantity", "ProductLine", "1")] == "2"
Expand Down Expand Up @@ -365,13 +365,13 @@ def test_create_payment_plan_posts_installment_and_dossier_fields(
assert request["Description"] == "3-month plan"

params = {p["Name"]: p["Value"] for p in recorded_service_parameters(mock_strategy)}
assert params["Includedinvoicekey"] == "INVK-999"
assert params["Dossiernumber"] == "DOSSIER-999"
assert params["Startdate"] == "2026-09-01"
assert params["IncludedInvoiceKey"] == "INVK-999"
assert params["DossierNumber"] == "DOSSIER-999"
assert params["StartDate"] == "2026-09-01"
assert params["Interval"] == "Month"
assert params["Paymentplancostamount"] == "5.00"
assert params["Recipientemail"] == "debtor@example.com"
assert params["Installmentcount"] == "3"
assert params["PaymentPlanCostAmount"] == "5.00"
assert params["RecipientEmail"] == "debtor@example.com"
assert params["InstallmentCount"] == "3"
assert "Description" not in params

def test_create_payment_plan_without_included_invoice_key_raises_before_wire(self, buckaroo):
Expand Down Expand Up @@ -408,7 +408,7 @@ def test_terminate_payment_plan_posts_included_invoice_key(self, buckaroo, mock_
assert response.status.code.code == 190
assert recorded_action(mock_strategy) == "TerminatePaymentPlan"
params = {p["Name"]: p["Value"] for p in recorded_service_parameters(mock_strategy)}
assert params["Includedinvoicekey"] == "INVK-999"
assert params["IncludedInvoiceKey"] == "INVK-999"

def test_terminate_payment_plan_without_included_invoice_key_raises_before_wire(self, buckaroo):
builder = buckaroo.solutions.create_solution("creditmanagement")
Expand Down Expand Up @@ -473,7 +473,7 @@ def test_create_combined_invoice_combines_into_ideal_pay(self, buckaroo, mock_st
(p["Name"], p["GroupType"]): p["Value"] for p in services[1]["Parameters"]
}
assert ("Invoice", "") not in invoice_params
assert invoice_params[("Invoiceamount", "")] == "95.00"
assert invoice_params[("InvoiceAmount", "")] == "95.00"
assert invoice_params[("Code", "Debtor")] == "DEBTOR-999"

assert response.get_service_parameter("InvoiceKey") == "INVK-COMBINED-1"
Expand Down
16 changes: 8 additions & 8 deletions tests/feature/solutions/test_emandate.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ def test_create_mandate_posts_service_parameters_and_returns_mandate_id(
assert recorded_action(mock_strategy) == "CreateMandate"

params = {p["Name"]: p["Value"] for p in recorded_service_parameters(mock_strategy)}
assert params["Debtorreference"] == "DEBTOR-999"
assert params["Sequencetype"] == "1"
assert params["Purchaseid"] == "PUR-1"
assert params["DebtorReference"] == "DEBTOR-999"
assert params["SequenceType"] == "1"
assert params["PurchaseId"] == "PUR-1"

def test_create_mandate_without_debtor_reference_raises_before_wire(self, buckaroo):
builder = buckaroo.solutions.create_solution("emandate")
Expand Down Expand Up @@ -114,7 +114,7 @@ def test_status_posts_mandate_id_and_returns_status(self, buckaroo, mock_strateg
assert recorded_action(mock_strategy) == "GetStatus"

params = {p["Name"]: p["Value"] for p in recorded_service_parameters(mock_strategy)}
assert params["Mandateid"] == "MND-777"
assert params["MandateId"] == "MND-777"

def test_status_without_mandate_id_raises_before_wire(self, buckaroo):
builder = buckaroo.solutions.create_solution("emandate")
Expand Down Expand Up @@ -157,8 +157,8 @@ def test_modify_mandate_posts_service_parameters_and_returns_mandate_id(
assert recorded_action(mock_strategy) == "ModifyMandate"

params = {p["Name"]: p["Value"] for p in recorded_service_parameters(mock_strategy)}
assert params["Mandateid"] == "MND-555"
assert params["Maxamount"] == "1000.00"
assert params["MandateId"] == "MND-555"
assert params["MaxAmount"] == "1000.00"

def test_modify_mandate_without_mandate_id_raises_before_wire(self, buckaroo):
builder = buckaroo.solutions.create_solution("emandate")
Expand Down Expand Up @@ -200,8 +200,8 @@ def test_cancel_mandate_posts_service_parameters_and_returns_response(
assert recorded_action(mock_strategy) == "CancelMandate"

params = {p["Name"]: p["Value"] for p in recorded_service_parameters(mock_strategy)}
assert params["Mandateid"] == "MND-321"
assert params["Purchaseid"] == "PUR-2"
assert params["MandateId"] == "MND-321"
assert params["PurchaseId"] == "PUR-2"

def test_cancel_mandate_without_mandate_id_raises_before_wire(self, buckaroo):
builder = buckaroo.solutions.create_solution("emandate")
Expand Down
12 changes: 6 additions & 6 deletions tests/feature/solutions/test_marketplaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def test_split_combines_marketplaces_into_ideal_pay(self, buckaroo, mock_strateg
split = {
(p["Name"], p["GroupType"], p["GroupID"]): p["Value"] for p in services[1]["Parameters"]
}
assert split[("Daysuntiltransfer", "", "")] == "2"
assert split[("DaysUntilTransfer", "", "")] == "2"
assert split[("Amount", "Marketplace", "")] == "10.00"
assert split[("Accountid", "Seller", "1")] == "789C60F316D24B088ACD471"
assert split[("Accountid", "Seller", "2")] == "369C60F316D24B088ACD238"
Expand Down Expand Up @@ -145,7 +145,7 @@ def test_transfer_with_splits_posts_grouped_params(self, buckaroo, mock_strategy
assert params[("Amount", "Marketplace", "")] == "10.00"
assert params[("Accountid", "Seller", "1")] == "789C60F316D24B088ACD471"
# DaysUntilTransfer never applies to a Transfer.
assert not any(name == "Daysuntiltransfer" for name, _, _ in params)
assert not any(name == "DaysUntilTransfer" for name, _, _ in params)

def test_transfer_requires_original_key(self, buckaroo):
with pytest.raises(ValueError):
Expand Down Expand Up @@ -245,10 +245,10 @@ def test_manual_transfer_posts_accounts_and_amount(self, buckaroo, mock_strategy
assert "AmountDebit" not in body

params = {p["Name"]: p["Value"] for p in service["Parameters"]}
assert params["Fromaccountid"] == "AAAAAAAAAAAAAAAAAAA"
assert params["Toaccountid"] == "BBBBBBBBBBBBBBBBBBB"
assert params["Fromdescription"] == "Deduction monthly fee"
assert params["Todescription"] == "Monthly fee third party ABC"
assert params["FromAccountId"] == "AAAAAAAAAAAAAAAAAAA"
assert params["ToAccountId"] == "BBBBBBBBBBBBBBBBBBB"
assert params["FromDescription"] == "Deduction monthly fee"
assert params["ToDescription"] == "Monthly fee third party ABC"
assert response.status.code.code == 190

def test_manual_transfer_requires_all_fields(self, buckaroo):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,4 @@ def test_validate_false_skips_parameter_validation(self):

service = recorded_request(mock)["Services"]["ServiceList"][0]
parameter_names = [p["Name"] for p in (service.get("Parameters") or [])]
assert "Someunknownparam" in parameter_names
assert "SomeUnknownParam" in parameter_names
4 changes: 2 additions & 2 deletions tests/unit/builders/payments/test_giftcards_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def test_refund_build_validates_when_intersolve_email_absent(client):

service = request.to_dict()["Services"]["ServiceList"][0]
names = {p["Name"] for p in service["Parameters"]}
assert "Lastname" in names
assert "LastName" in names
assert "Email" not in names


Expand All @@ -205,7 +205,7 @@ def test_refund_build_keeps_intersolve_email_when_supplied(client):

service = request.to_dict()["Services"]["ServiceList"][0]
names = {p["Name"] for p in service["Parameters"]}
assert {"Lastname", "Email"} <= names
assert {"LastName", "Email"} <= names


def test_get_allowed_service_parameters_refund_is_case_insensitive(client):
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/builders/payments/test_klarna_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def test_posts_cancel_reservation_to_data_request_without_original_key(self):
sent = recorded_request(mock)
assert "OriginalTransactionKey" not in sent
names = [p["Name"] for p in sent["Services"]["ServiceList"][0]["Parameters"]]
assert "Datarequestkey" in names
assert "DataRequestKey" in names
assert response.key == "KL-CAN-1"
mock.assert_all_consumed()

Expand Down
24 changes: 12 additions & 12 deletions tests/unit/builders/solutions/test_credit_management_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ def test_debtor_file_actions_post_debtor_file_guid_to_data_request(

service = recorded_request(mock_strategy)["Services"]["ServiceList"][0]
params = {p["Name"]: p["Value"] for p in service["Parameters"]}
assert params["Debtorfileguid"] == "FILE-GUID-001"
assert params["DebtorFileGuid"] == "FILE-GUID-001"


@pytest.mark.parametrize("method_name", ["resume_debtor_file", "pause_debtor_file"])
Expand Down Expand Up @@ -439,9 +439,9 @@ def test_create_credit_note_posts_invoice_top_level_and_debtor_group_to_data_req

service = request["Services"]["ServiceList"][0]
params = {(p["Name"], p["GroupType"]): p["Value"] for p in service["Parameters"]}
assert params[("Originalinvoicenumber", "")] == "INV-001"
assert params[("Invoicedate", "")] == "2026-07-16"
assert params[("Invoiceamount", "")] == "10.0"
assert params[("OriginalInvoiceNumber", "")] == "INV-001"
assert params[("InvoiceDate", "")] == "2026-07-16"
assert params[("InvoiceAmount", "")] == "10.0"
assert params[("Code", "Debtor")] == "DEBTOR-001"


Expand Down Expand Up @@ -500,7 +500,7 @@ def test_add_or_update_product_lines_posts_indexed_grouped_articles(client, mock

service = recorded_request(mock_strategy)["Services"]["ServiceList"][0]
params = {(p["Name"], p["GroupType"], p["GroupID"]): p["Value"] for p in service["Parameters"]}
assert params[("Invoicekey", "", "")] == "INVK-001"
assert params[("InvoiceKey", "", "")] == "INVK-001"
assert params[("Productid", "ProductLine", "1")] == "SKU-1"
assert params[("Productname", "ProductLine", "1")] == "Widget"
assert params[("Quantity", "ProductLine", "1")] == "2"
Expand Down Expand Up @@ -669,13 +669,13 @@ def test_create_payment_plan_posts_to_data_request(client, mock_strategy):

service = request["Services"]["ServiceList"][0]
params = {p["Name"]: p["Value"] for p in service["Parameters"]}
assert params["Includedinvoicekey"] == "INVK-001"
assert params["Dossiernumber"] == "DOSSIER-1"
assert params["Startdate"] == "2026-09-01"
assert params["IncludedInvoiceKey"] == "INVK-001"
assert params["DossierNumber"] == "DOSSIER-1"
assert params["StartDate"] == "2026-09-01"
assert params["Interval"] == "Month"
assert params["Paymentplancostamount"] == "5.00"
assert params["Recipientemail"] == "debtor@example.com"
assert params["Installmentcount"] == "3"
assert params["PaymentPlanCostAmount"] == "5.00"
assert params["RecipientEmail"] == "debtor@example.com"
assert params["InstallmentCount"] == "3"
assert "Description" not in params


Expand Down Expand Up @@ -720,7 +720,7 @@ def test_terminate_payment_plan_posts_to_data_request(client, mock_strategy):

service = recorded_request(mock_strategy)["Services"]["ServiceList"][0]
params = {p["Name"]: p["Value"] for p in service["Parameters"]}
assert params["Includedinvoicekey"] == "INVK-001"
assert params["IncludedInvoiceKey"] == "INVK-001"


def test_terminate_payment_plan_raises_when_included_invoice_key_missing(client):
Expand Down
17 changes: 17 additions & 0 deletions tests/unit/builders/test_base_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,23 @@ def test_add_parameter_flat_capitalizes_name_and_stringifies_value():
]


def test_add_parameter_flat_keeps_internal_capitals():
# Top-level names keep internal caps: "customerFirstName" -> "CustomerFirstName".
builder = populate_required_fields(_make_builder(), amount=10.50)
builder.add_parameter("customerFirstName", "Jan")

request = builder.build(validate=False).to_dict()
service = request["Services"]["ServiceList"][0]
assert service["Parameters"] == [
{
"Name": "CustomerFirstName",
"GroupType": "",
"GroupID": "",
"Value": "Jan",
}
]


def test_add_parameter_grouped_sets_group_type_and_group_id():
builder = populate_required_fields(_make_builder(), amount=10.50)
builder.add_parameter("firstName", "Jane", group_type="customer", group_id="7")
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def test_version_attribute_matches_version_module():
from buckaroo import _version

assert buckaroo.__version__ == _version.VERSION
assert buckaroo.__version__ == "0.2.0"
assert buckaroo.__version__ == "0.2.1"


def test_public_api_reexports():
Expand Down Expand Up @@ -49,4 +49,4 @@ def test_setup_py_can_read_version_module():
version_file = Path(buckaroo.__file__).parent / "_version.py"
namespace = runpy.run_path(str(version_file))

assert namespace["VERSION"] == "0.2.0"
assert namespace["VERSION"] == "0.2.1"
Loading