Repository navigation
refactor: move gateways onto base/live/stub service clients - #13
Merged
Merged
Conversation
Gateway results become pydantic models (CheckoutSession, RefundResult, WebhookEvent). Callers read them through model_validate, so a gateway that still returns plain dicts keeps working. Razorpay transport moves to bwh_payments/services/razorpay: base.py holds the client contract and response models, live.py the HTTP calls, logging and error reading, stub.py an in-memory Razorpay that tests drive through classmethod controls. The settings controller keeps every decision (minor units, status map, capture rules, signature check) and picks the stub when frappe.in_test. Tests drop the transport patches and tests/fake_razorpay.py.
A request turning Paid now calls ref_doc.run_method("on_payment_authorized",
"Completed") from on_update, the frappe/payments convention, so apps built
for it settle orders without a doc_events hook.
bwh_payments.api is the supported import surface for other apps:
create_payment, get_payment_gateways, resolve_payment_mode and the
minor-unit helpers.
bwh_payments/services/stripe holds the client contract and models, the live StripeClient wrapper (one client per instance, never the module level api_key) and an in-memory stub. Webhook signature verification stays in the controller: it is local crypto, and a stub must never be able to accept a forged event. tests/fake_stripe.py and the StripeClient patches are gone; tests drive StubStripeClient.pay() instead.
bwh_payments/services/telr holds the order.json and remote.xml transport, the refund XML and auth-response parsing, and an in-memory stub. The controller keeps the status map, the authorised-as-paid switch, the remote-key check and the refund acceptance rule. The Telr tests drive the stub instead of patching get_order, and now also cover create_session and the refund path.
…st tests bwh_payments/services/tabby holds the checkout and payments transport, error reading and logging, typed models for Tabby's nested checkout configuration, and an in-memory stub. The controller keeps the currency allowlist, rejection messages, capture-on-read, refund guard and the webhook token and IP checks. Tabby had no tests (#12). The stub makes them cheap: session keying, currency refusal, rejection wording, capture of the authorised amount, refund guards and the webhook token/allowlist are now covered.
Describes the gateway contract models, the services/<service>/{base,live,stub}
layout and its rules (stub only under frappe.in_test, decisions stay in the
controller, live clients are untested transport), bwh_payments.api and the
on_payment_authorized callback, and how tests drive stubs instead of
patching transports.
- RefundResult is read after the money has moved, so nothing on it is required and a None result no longer strands the refund ledger. - create_payment only accepts company and customer_* fields: its insert skips permissions, so a forwarded status or order_ref must not land. - Gateway models share GatewayModel, which coerces bare numbers into str fields (a numeric Telr ref no longer fails validation) and relies on pydantic's default of ignoring unknown fields. - Stripe calls go through StripeClient.v1; the floor moves to 12.5, the first SDK that ships it. - get_client() is a single expression; webhook.handle reads the event directly.
Project skills that capture the base/live/stub procedure: pinning behaviour before a migration, sorting transport from decisions, stub design, consumer parity, and the facts to gather before adding a new provider.
get_client now returns the stub under frappe.in_test first and the live client after, in every gateway. create_payment is annotated as returning GatewayPaymentRequest, which it already did at runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refund result is read after the money has moved. If it fails to parse, the ledger is never updated and the
same amount stays refundable, so a retry refunds twice.
- StripeRefund and RazorpayRefund no longer require id or amount. Without an echoed amount, the requested
amount is used.
- CheckoutSession, RefundResult and WebhookEvent share a GatewayResult base that turns numeric ids into
strings, so a gateway from another app that returns {"refund_id": 12345} still books the refund.
- Telr's live client turns a non-JSON body into frappe.ValidationError, as Razorpay and Tabby already do.
- throw_request_error is annotated NoReturn in the Razorpay and Tabby live clients.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ON guards Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Each gateway now talks to its provider through a service client in
bwh_payments/services/<gateway>/:base.pyholds an abstract client and pydantic models that mirror the provider's JSON.live.pyholds the HTTP or SDK calls, error reading and request logging.stub.pyholds an in-memory provider. Tests drive it with controls such aspay()andauthorise().The settings controller selects the stub only when
frappe.in_testis set. A real site always uses the live client. The controller keeps every payment decision: currency conversion, status maps, capture and refund rules, and webhook signature checks.Razorpay, Stripe, Telr and Tabby are migrated. Tests no longer patch transports, and
tests/fake_stripe.pyandtests/fake_razorpay.pyare deleted.Other changes
CheckoutSession,RefundResultandWebhookEvent | None. Callers read them withmodel_validate, so a gateway in another app that returns dicts still works.on_updatecallsref_doc.run_method("on_payment_authorized", "Completed"). This is the frappe/payments convention.bwh_payments/api.pyis the import surface for other apps:create_payment,get_payment_gateways,resolve_payment_modeand the minor-unit helpers.StripeClient.v1. Thestripefloor moves to 12.5, the first release withv1..claude/skills/describe how to migrate a gateway and how to add one.Behavior changes
order_ref.create_paymentaccepts onlycompanyandcustomer_*fields, because its insert skips permission checks.Tests
create_payment, Telr sessions and refunds, and Tabby, which had no tests (Tabby gateway has no tests #12).Merge order
Merge this before bwhtech/commera#228, because it removes the fakes that commera tests import.
fix/refund-lock-commitalso editsgateway_payment_request.py. Whichever merges second needs a rebase.