Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/actions/setup-bench/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ runs:
env:
EXTRA_APP_URL: ${{ inputs.extra-app-url }}
run: |
# buzz lists frappe/payments in required_apps, so it must precede install-app.
bench get-app --skip-assets https://github.com/frappe/payments
# buzz lists bwhtech/bwh_payments in required_apps, so it must precede install-app.
bench get-app --skip-assets --branch v1-stable https://github.com/bwhtech/bwh_payments

# Test-only: buzz/tests/factories imports frappe_factory_bot. No install-app —
# the app ships no DocTypes, and get-app already pip-installs it into the env.
Expand Down
4 changes: 2 additions & 2 deletions buzz/api/events/test_ticket_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

from buzz.api.events import get_event_ticket_types
from buzz.api.events.exceptions import CannotManageEvent
from buzz.tests.base_test_cases import make_payment_gateway
from buzz.tests.factories import (
BuzzEventFactory,
BuzzTeamFactory,
BuzzTeamMembershipFactory,
EventBookingFactory,
EventTicketTypeFactory,
PaymentGatewayFactory,
UserFactory,
)
from buzz.ticketing.doctype.event_ticket_type.event_ticket_type import PAID_EVENTS_FLAG
Expand Down Expand Up @@ -98,7 +98,7 @@ def test_lists_sales_per_currency(self):
self.assertEqual([price["tickets_sold"] for price in row["prices"]], [0, 1])

def test_lists_payment_providers_and_flags_the_default(self):
default = PaymentGatewayFactory.create().name
default = make_payment_gateway()

with self.change_settings("Buzz Settings", default_payment_gateway=default):
providers = self.payload_as(self.owner)["payment_providers"]
Expand Down
13 changes: 12 additions & 1 deletion buzz/api/payments/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import frappe
from bwh_payments.api import get_gateway_options

from buzz.api.payments.currencies import enabled_currencies
from buzz.api.payments.schemas import CurrencyItem
from buzz.api.payments.schemas import CurrencyItem, PaymentGatewayLogo
from buzz.payments import get_payment_gateways_for_event


Expand All @@ -14,3 +15,13 @@ def get_event_payment_gateways(event: str) -> list[str]:
@frappe.whitelist(allow_guest=True)
def get_enabled_currencies() -> list[CurrencyItem]:
return [CurrencyItem(**currency) for currency in enabled_currencies()]


# nosemgrep: guest-whitelisted-method
@frappe.whitelist(allow_guest=True)
def get_payment_gateway_logos() -> list[PaymentGatewayLogo]:
return [
PaymentGatewayLogo(name=option.name, logo=option.logo, logo_dark=option.logo_dark)
for option in get_gateway_options()
if option.logo
]
6 changes: 6 additions & 0 deletions buzz/api/payments/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ class CurrencyItem(APIResponse):
name: str
symbol: str | None
number_format: str | None


class PaymentGatewayLogo(APIResponse):
name: str
logo: str
logo_dark: str
31 changes: 27 additions & 4 deletions buzz/api/payments/test_payments.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import frappe
from frappe.tests import IntegrationTestCase

from buzz.api.payments import get_event_payment_gateways
from buzz.api.payments import get_event_payment_gateways, get_payment_gateway_logos
from buzz.payments import resolve_payment_gateway
from buzz.tests.factories import BuzzEventFactory, PaymentGatewayFactory
from buzz.tests.base_test_cases import make_payment_gateway
from buzz.tests.factories import BuzzEventFactory


class TestGetEventPaymentGateways(IntegrationTestCase):
def setUp(self):
self.enterContext(self.change_settings("Buzz Settings", default_payment_gateway=None))
self.gateway = PaymentGatewayFactory.create().name
self.other_gateway = PaymentGatewayFactory.create().name
self.gateway = make_payment_gateway()
self.other_gateway = make_payment_gateway()

def test_configured_gateways_override_default(self):
event = self.create_event(self.gateway)
Expand Down Expand Up @@ -39,6 +40,12 @@ def test_new_event_keeps_gateways_it_was_created_with(self):

self.assertEqual(self.gateways_on(event), [self.other_gateway])

def test_a_disabled_gateway_is_not_offered(self):
event = self.create_event(self.gateway, self.other_gateway)
frappe.get_doc("Payment Gateway Profile", self.other_gateway).update({"enabled": 0}).save()

self.assertEqual(get_event_payment_gateways(event), [self.gateway])

def test_resolve_picks_the_first_gateway_when_none_is_given(self):
self.assertEqual(resolve_payment_gateway(self.create_event(self.gateway), None), self.gateway)

Expand All @@ -63,3 +70,19 @@ def create_event(self, *gateways: str) -> str:

def gateways_on(self, event: str) -> list[str]:
return [row.payment_gateway for row in frappe.get_doc("Buzz Event", event).payment_gateways]


class TestGetPaymentGatewayLogos(IntegrationTestCase):
def test_every_enabled_gateway_comes_with_its_logo_for_both_themes(self):
gateway = make_payment_gateway()

logos = {row.name: row for row in get_payment_gateway_logos()}

self.assertTrue(logos[gateway].logo.startswith("/assets/bwh_payments/"))
self.assertTrue(logos[gateway].logo_dark.startswith("/assets/bwh_payments/"))

def test_a_disabled_gateway_has_no_logo_listed(self):
gateway = make_payment_gateway()
frappe.get_doc("Payment Gateway Profile", gateway).update({"enabled": 0}).save()

self.assertNotIn(gateway, [row.name for row in get_payment_gateway_logos()])
33 changes: 26 additions & 7 deletions buzz/api/sponsorships/test_sponsorships.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from unittest.mock import MagicMock, patch

import frappe
from bwh_payments import testing as payment_testing

from buzz.api.sponsorships import (
create_sponsorship_payment_link,
Expand All @@ -15,8 +14,7 @@
PaymentNotPermitted,
WithdrawalNotPermitted,
)
from buzz.tests.base_test_cases import SponsorshipTestCase
from buzz.tests.factories import PaymentGatewayFactory
from buzz.tests.base_test_cases import SponsorshipTestCase, make_payment_gateway


class TestGetSponsorshipDetails(SponsorshipTestCase):
Expand Down Expand Up @@ -120,8 +118,7 @@ def test_link_charges_the_chosen_currency(self):
self.tier.save()
gateway = self.add_gateway_to_event()

with patch("buzz.payments.get_controller", return_value=MagicMock()):
create_sponsorship_payment_link(self.enquiry.name, self.tier.name, gateway, currency="USD")
create_sponsorship_payment_link(self.enquiry.name, self.tier.name, gateway, currency="USD")

payment = frappe.get_last_doc("Event Payment", {"reference_docname": self.enquiry.name})
self.assertEqual((payment.currency, payment.amount), ("USD", 60))
Expand All @@ -132,8 +129,30 @@ def test_link_refuses_a_currency_the_tier_has_no_price_in(self):
with self.assertRaises(frappe.ValidationError):
create_sponsorship_payment_link(self.enquiry.name, self.tier.name, gateway, currency="EUR")

def test_paying_the_link_makes_the_enquirer_a_sponsor(self):
gateway = self.add_gateway_to_event()
create_sponsorship_payment_link(self.enquiry.name, self.tier.name, gateway)
payment = frappe.get_last_doc("Event Payment", {"reference_docname": self.enquiry.name})

payment_testing.pay(payment.payment_request)

self.assertEqual(frappe.db.get_value("Sponsorship Enquiry", self.enquiry.name, "status"), "Paid")
self.assertTrue(frappe.db.exists("Event Sponsor", {"enquiry": self.enquiry.name}))
self.assertEqual(frappe.db.get_value("Event Payment", payment.name, "payment_received"), 1)

def test_the_sponsor_comes_back_to_their_enquiry_after_paying(self):
gateway = self.add_gateway_to_event()

create_sponsorship_payment_link(self.enquiry.name, self.tier.name, gateway)

payment = frappe.get_last_doc("Event Payment", {"reference_docname": self.enquiry.name})
self.assertEqual(
frappe.db.get_value("Gateway Payment Request", payment.payment_request, "redirect_to"),
f"/b/account/sponsorships/{self.enquiry.name}?success=true",
)

def add_gateway_to_event(self) -> str:
gateway = PaymentGatewayFactory.create().name
gateway = make_payment_gateway()
event = frappe.get_doc("Buzz Event", self.event)
event.set("payment_gateways", [{"payment_gateway": gateway}])
event.save(ignore_permissions=True)
Expand Down
2 changes: 1 addition & 1 deletion buzz/events/doctype/buzz_settings/buzz_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"fieldname": "default_payment_gateway",
"fieldtype": "Link",
"label": "Default Payment Gateway",
"options": "Payment Gateway"
"options": "Payment Gateway Profile"
},
{
"fieldname": "column_break_ehsr",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Payment Gateway",
"options": "Payment Gateway",
"options": "Payment Gateway Profile",
"reqd": 1
}
],
Expand Down
8 changes: 1 addition & 7 deletions buzz/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# Apps
# ------------------

required_apps = ["frappe/payments"]
required_apps = ["bwhtech/bwh_payments"]

export_python_type_annotations = True

Expand Down Expand Up @@ -64,12 +64,6 @@

before_tests = "buzz.install.before_tests"

# Payments
# --------

# Fired by the payments app when a gateway settles a refund.
handle_refund_notification = "buzz.payments.handle_refund_notification"


doc_events = {
"User": {
Expand Down
1 change: 1 addition & 0 deletions buzz/patches.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ buzz.patches.create_administrator_team
buzz.patches.geocode_event_venues
buzz.patches.rename_is_a_community_field
buzz.patches.migrate_desk_tags_to_buzz_tags
buzz.patches.move_payments_to_bwh_payments
79 changes: 79 additions & 0 deletions buzz/patches/move_payments_to_bwh_payments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import frappe
from bwh_payments.api import record_external_payment, record_external_refund
from bwh_payments.migrate.frappe_payments import import_gateways


def execute():
if "payments" not in frappe.get_installed_apps():
return

PaymentHistoryMigration().run()


class PaymentHistoryMigration:
"""Give every payment and refund taken through frappe/payments a bwh_payments record.

Event Payment and Event Booking Refund rows keep every field they had; they only gain a link.
"""

def run(self):
import_gateways()
for payment in frappe.get_all(
"Event Payment",
filters={"payment_request": ("is", "not set"), "payment_gateway": ("is", "set")},
fields=["*"],
order_by="creation asc",
):
self.import_row(self.import_payment, payment)

for refund in frappe.get_all(
"Event Booking Refund",
filters={"gateway_refund": ("is", "not set"), "refund_id": ("is", "set")},
fields=["name", "payment", "refund_id", "status", "amount"],
order_by="creation asc",
):
self.import_row(self.import_refund, refund)

def import_row(self, importer, row):
# One unreadable row, such as a payment for a deleted booking, must not stop the site migrating.
frappe.db.savepoint("move_payment_row")
try:
importer(row)
except Exception:
frappe.db.rollback(save_point="move_payment_row")
frappe.log_error(title=f"Could not move payment record {row.name} to bwh_payments")

def import_payment(self, payment):
self.keep_gateway(payment.payment_gateway)
payment_request = record_external_payment(
payment.reference_doctype,
payment.reference_docname,
payment.amount,
payment.currency,
payment.payment_gateway,
"Paid" if payment.payment_received else "Expired",
payment.order_id or payment.payment_id or f"legacy-{payment.name}",
transaction_ref=payment.payment_id,
)
frappe.db.set_value(
"Event Payment", payment.name, "payment_request", payment_request.name, update_modified=False
)

def import_refund(self, refund):
payment_request = frappe.db.get_value("Event Payment", refund.payment, "payment_request")
if not payment_request:
return

gateway_refund = record_external_refund(
payment_request, refund.refund_id, refund.status, refund.amount, reference=refund.name
)
frappe.db.set_value(
"Event Booking Refund", refund.name, "gateway_refund", gateway_refund.name, update_modified=False
)

def keep_gateway(self, gateway: str):
"""A gateway frappe/payments no longer lists is kept, disabled, so its payments still name it."""
if not frappe.db.exists("Payment Gateway Profile", gateway):
frappe.get_doc({"doctype": "Payment Gateway Profile", "name": gateway, "enabled": 0}).insert(
ignore_permissions=True
)
Loading
Loading