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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ jobs:
run: |
bench get-app buzz $GITHUB_WORKSPACE
bench get-app payments
bench get-app zoom_integration https://github.com/bwhtech/zoom_integration
bench setup requirements --dev
bench new-site --db-root-password root --admin-password admin test_site
bench --site test_site install-app zoom_integration
bench --site test_site install-app buzz
bench build
env:
Expand Down
3 changes: 2 additions & 1 deletion buzz/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1040,12 +1040,13 @@ def get_ticket_details(ticket_id: str) -> dict:
zoom_registration = frappe.db.get_value(
"Zoom Webinar Registration",
ticket_doc.zoom_webinar_registration,
["join_url", "webinar"],
["join_url", "webinar", "meeting"],
as_dict=True,
)
if zoom_registration:
details.zoom_join_url = zoom_registration.join_url
details.zoom_webinar = zoom_registration.webinar
details.zoom_meeting = zoom_registration.meeting

return details

Expand Down
44 changes: 44 additions & 0 deletions buzz/events/doctype/buzz_event/buzz_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,30 @@ def create_webinar_on_zoom(self):

return zoom_webinar

@frappe.whitelist()
@only_if_app_installed("zoom_integration", raise_exception=True)
def create_meeting_on_zoom(self):
if not self.end_time:
frappe.throw(_("End time is needed for Zoom Meeting creation"))

zoom_meeting = frappe.get_doc(
{
"doctype": "Zoom Meeting",
"title": self.title,
"date": self.start_date,
"start_time": self.start_time,
"duration": int(time_diff_in_seconds(self.end_time, self.start_time)),
"timezone": self.time_zone,
}
).insert()

self.db_set("zoom_meeting", zoom_meeting.name)

return zoom_meeting

def on_update(self):
self.update_zoom_webinar()
self.update_zoom_meeting()

@only_if_app_installed("zoom_integration")
def update_zoom_webinar(self):
Expand All @@ -261,6 +283,28 @@ def update_zoom_webinar(self):
)
webinar.save()

@only_if_app_installed("zoom_integration")
def update_zoom_meeting(self):
if not self.zoom_meeting:
return

if (
self.has_value_changed("start_date")
or self.has_value_changed("end_time")
or self.has_value_changed("start_time")
or self.has_value_changed("time_zone")
):
meeting = frappe.get_doc("Zoom Meeting", self.zoom_meeting)
meeting.update(
{
"date": self.start_date,
"start_time": self.start_time,
"duration": int(time_diff_in_seconds(self.end_time, self.start_time)),
"timezone": self.time_zone,
}
)
meeting.save()


@frappe.whitelist()
def create_from_template(template_name: str, options: str, additional_fields: str = "{}") -> str:
Expand Down
64 changes: 64 additions & 0 deletions buzz/events/doctype/buzz_event/test_buzz_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -1022,3 +1022,67 @@ def test_backfill_patch_skips_events_missing_start_fields(self):
backfill_time_zone_labels()

self.assertEqual(frappe.db.get_value("Buzz Event", event.name, "time_zone_label"), "")


class TestBuzzEventZoomMeeting(FrappeTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
if not frappe.db.exists("Event Category", "Test Category"):
frappe.get_doc({"doctype": "Event Category", "category_name": "Test Category"}).insert(
ignore_permissions=True
)
if not frappe.db.exists("Event Host", "Test Host"):
frappe.get_doc({"doctype": "Event Host", "host_name": "Test Host"}).insert(
ignore_permissions=True
)

def tearDown(self):
frappe.db.rollback()

def _make_event(self):
return frappe.get_doc(
{
"doctype": "Buzz Event",
"title": "Meeting Event",
"category": "Test Category",
"host": "Test Host",
"start_date": "2026-08-01",
"end_date": "2026-08-01",
"start_time": "10:00:00",
"end_time": "11:00:00",
}
).insert(ignore_permissions=True)

def test_create_meeting_on_zoom_links_meeting_to_event(self):
from zoom_integration.tests.zoom_fixtures import CREATE_MEETING_RESPONSE

meeting_controller = "zoom_integration.zoom_integration.doctype.zoom_meeting.zoom_meeting"
event = self._make_event()

with patch(f"{meeting_controller}.create_zoom_session", return_value=CREATE_MEETING_RESPONSE):
meeting = event.create_meeting_on_zoom()

self.assertTrue(meeting.name)
event.reload()
self.assertEqual(event.zoom_meeting, meeting.name)
self.assertEqual(meeting.zoom_meeting_id, "91234567890")

def test_update_event_schedule_pushes_to_zoom_meeting(self):
from zoom_integration.tests.zoom_fixtures import CREATE_MEETING_RESPONSE

meeting_controller = "zoom_integration.zoom_integration.doctype.zoom_meeting.zoom_meeting"
event = self._make_event()

with patch(f"{meeting_controller}.create_zoom_session", return_value=CREATE_MEETING_RESPONSE):
event.create_meeting_on_zoom()

# Note: do not reload() — Time fields come back as timedelta and trip event
# validation's time diff. The in-memory doc keeps string times and has
# zoom_meeting set via db_set already.
with patch(f"{meeting_controller}.update_zoom_session") as mock_update:
event.end_time = "12:00:00"
event.save(ignore_permissions=True)

mock_update.assert_called_once()
self.assertEqual(mock_update.call_args.args[0], "meetings")
7 changes: 7 additions & 0 deletions buzz/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@
"options": "Zoom Webinar",
"insert_after": "zoom_integration_tab",
},
{
"fieldname": "zoom_meeting",
"label": "Zoom Meeting",
"fieldtype": "Link",
"options": "Zoom Meeting",
"insert_after": "zoom_webinar",
},
],
"Buzz Settings": [
{
Expand Down
27 changes: 17 additions & 10 deletions buzz/ticketing/doctype/event_ticket/event_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,28 @@ def create_zoom_registration_if_applicable(self):
event_doc = frappe.get_cached_doc("Buzz Event", self.event)

if event_doc.zoom_webinar:
doc = {
registration_ref = {"webinar": event_doc.zoom_webinar}
elif event_doc.get("zoom_meeting"):
registration_ref = {"meeting": event_doc.zoom_meeting}
else:
return

registration = frappe.get_doc(
{
"doctype": "Zoom Webinar Registration",
"webinar": event_doc.zoom_webinar,
**registration_ref,
"email": self.attendee_email,
"first_name": self.first_name,
"last_name": self.last_name or "-",
}
registration = frappe.get_doc(doc).insert(ignore_permissions=True)

try:
registration.submit()
# Store the registration reference on the ticket
self.db_set("zoom_webinar_registration", registration.name)
except Exception:
frappe.log_error("Failed to create registration on Zoom")
).insert(ignore_permissions=True)

try:
registration.submit()
# Store the registration reference on the ticket (holds meeting or webinar registration)
self.db_set("zoom_webinar_registration", registration.name)
except Exception:
frappe.log_error("Failed to create registration on Zoom")

def send_user_invitation(self):
invite_by_email(
Expand Down
58 changes: 58 additions & 0 deletions buzz/ticketing/doctype/event_ticket/test_event_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,61 @@ def test_generate_qr_code_file_creates_attachment(self):

# Cleanup
file_doc.delete()


class TestEventTicketZoomMeeting(IntegrationTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.event = frappe.get_doc("Buzz Event", {"route": "test-route"})
cls.ticket_type = frappe.get_doc(
{
"doctype": "Event Ticket Type",
"title": "Meeting TT",
"event": cls.event.name,
"currency": "USD",
}
).insert(ignore_permissions=True, ignore_if_duplicate=True)

def tearDown(self):
frappe.db.rollback()

def test_ticket_creates_meeting_registration_when_event_has_meeting(self):
from zoom_integration.tests.zoom_fixtures import (
ADD_MEETING_REGISTRANT_RESPONSE,
CREATE_MEETING_RESPONSE,
)

meeting_controller = "zoom_integration.zoom_integration.doctype.zoom_meeting.zoom_meeting"

with patch(f"{meeting_controller}.create_zoom_session", return_value=CREATE_MEETING_RESPONSE):
meeting = frappe.get_doc(
{
"doctype": "Zoom Meeting",
"title": "Ticket Meeting",
"date": "2026-08-01",
"start_time": "10:00:00",
"duration": 3600,
"timezone": "Asia/Calcutta",
}
).insert(ignore_permissions=True)

self.event.db_set("zoom_meeting", meeting.name)

with patch(f"{meeting_controller}.add_zoom_registrant", return_value=ADD_MEETING_REGISTRANT_RESPONSE):
ticket = frappe.get_doc(
{
"doctype": "Event Ticket",
"event": self.event.name,
"ticket_type": self.ticket_type.name,
"first_name": "Alice",
"last_name": "Smith",
"attendee_email": "alice@example.com",
}
).insert(ignore_permissions=True)
ticket.submit()

self.assertTrue(ticket.zoom_webinar_registration)
registration = frappe.get_doc("Zoom Webinar Registration", ticket.zoom_webinar_registration)
self.assertEqual(registration.meeting, meeting.name)
self.assertEqual(registration.registrant_id, "abcDEF12ghIJ")
Loading