From d7444f9269c69ffd06124a2188104840be462003 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 14 May 2026 17:37:35 +0930 Subject: [PATCH 1/6] fix(events): support #guest_list ical_uid lookup --- src/controllers/events.cr | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/controllers/events.cr b/src/controllers/events.cr index 8e520d6c..8f1721c4 100644 --- a/src/controllers/events.cr +++ b/src/controllers/events.cr @@ -1706,16 +1706,24 @@ class Events < Application event_id : String, @[AC::Param::Info(description: "the event space associated with this event", example: "sys-1234")] system_id : String, + @[AC::Param::Info(description: "the ical uid of the event you are looking for", example: "sqvitruh3ho3mrq896tplad4v8")] + ical_uid : String? = nil, ) : Array(Guest) - cal_id = get_placeos_client.systems.fetch(system_id).email - return [] of Guest unless cal_id + if ical_uid.presence + metadata = EventMetadata.by_tenant(tenant.id).where(ical_uid: ical_uid, system_id: system_id).first? + parent_meta = false + else + cal_id = get_placeos_client.systems.fetch(system_id).email + return [] of Guest unless cal_id - event = client.get_event(user.email, id: event_id, calendar_id: cal_id) - raise Error::NotFound.new("event #{event_id} not found on system calendar #{cal_id}") unless event + event = client.get_event(user.email, id: event_id, calendar_id: cal_id) + raise Error::NotFound.new("event #{event_id} not found on system calendar #{cal_id}") unless event + + # Grab meeting metadata if it exists + metadata = get_event_metadata(event, system_id) + parent_meta = !metadata.try &.for_event_instance?(event, client.client_id) + end - # Grab meeting metadata if it exists - metadata = get_event_metadata(event, system_id) - parent_meta = !metadata.try &.for_event_instance?(event, client.client_id) return [] of Guest unless metadata # Find anyone who is attending From 844dc7b0bdd3c6cf0f597b59a290d9fe58e38356 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Thu, 14 May 2026 17:42:49 +0930 Subject: [PATCH 2/6] docs(openapi.yml) --- OPENAPI_DOC.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/OPENAPI_DOC.yml b/OPENAPI_DOC.yml index 2d29e73e..189deb6d 100644 --- a/OPENAPI_DOC.yml +++ b/OPENAPI_DOC.yml @@ -6268,6 +6268,13 @@ paths: required: true schema: type: string + - name: ical_uid + in: query + description: the ical uid of the event you are looking for + example: sqvitruh3ho3mrq896tplad4v8 + schema: + type: string + nullable: true responses: 200: description: OK From 24e543adc4f97cf449113c487524772992814b28 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Fri, 15 May 2026 13:41:43 +0930 Subject: [PATCH 3/6] feat(bookings): add include_linked guests (PPT-2375) --- OPENAPI_DOC.yml | 6 ++++++ src/controllers/bookings.cr | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/OPENAPI_DOC.yml b/OPENAPI_DOC.yml index 189deb6d..a2db8cf4 100644 --- a/OPENAPI_DOC.yml +++ b/OPENAPI_DOC.yml @@ -3253,6 +3253,12 @@ paths: schema: type: integer format: Int64 + - name: include_linked + in: query + description: include guests from linked (child) bookings + example: "true" + schema: + type: boolean - name: instance in: query description: a recurring instance id diff --git a/src/controllers/bookings.cr b/src/controllers/bookings.cr index c6b72088..7eae1446 100644 --- a/src/controllers/bookings.cr +++ b/src/controllers/bookings.cr @@ -963,10 +963,23 @@ class Bookings < Application # returns a list of guests associated with a booking @[AC::Route::GET("/:id/guests")] - def guest_list : Array(Guest) - booking.attendees.to_a.map do |visitor| + def guest_list( + @[AC::Param::Info(description: "include guests from linked (child) bookings", example: "true")] + include_linked : Bool = false, + ) : Array(Guest) + guests = booking.attendees.to_a.map do |visitor| visitor.guest.not_nil!.for_booking_to_h(visitor, booking) end + + if include_linked && booking.parent? + Booking.where(parent_id: booking.id).each do |child| + child.attendees.to_a.each do |visitor| + guests << visitor.guest.not_nil!.for_booking_to_h(visitor, child) + end + end + end + + guests end # marks the standalone visitor as checked-in or checked-out based on the state param From edc19ac64904d09bf9bc46349cd267c6b62dfe18 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Fri, 15 May 2026 14:14:27 +0930 Subject: [PATCH 4/6] tests(visitor_mailer): test #guests linked_bookings (PPT-2375) --- spec/controllers/bookings_spec.cr | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/spec/controllers/bookings_spec.cr b/spec/controllers/bookings_spec.cr index 31d5e9b5..d30ff1d9 100644 --- a/spec/controllers/bookings_spec.cr +++ b/spec/controllers/bookings_spec.cr @@ -1432,6 +1432,49 @@ describe Bookings do body.map(&.["name"]).should eq([guest.name]) end + it "#guest_list with include_linked should include guests from child bookings" do + tenant = get_tenant + user_email = Faker::Internet.email + + parent = BookingsHelper.create_booking( + tenant_id: tenant.id.not_nil!, + user_email: user_email, + booking_type: "group", + ) + + parent_guest = GuestsHelper.create_guest(tenant.id, "parent-guest@example.com") + Attendee.create!(booking_id: parent.id.not_nil!, + guest_id: parent_guest.id, + tenant_id: parent_guest.tenant_id, + checked_in: false, + visit_expected: true, + ) + + child = BookingsHelper.create_booking( + tenant_id: tenant.id.not_nil!, + user_email: user_email, + booking_type: "visitor", + parent_id: parent.id, + ) + + child_guest = GuestsHelper.create_guest(tenant.id, "child-guest@example.com") + Attendee.create!(booking_id: child.id.not_nil!, + guest_id: child_guest.id, + tenant_id: child_guest.tenant_id, + checked_in: false, + visit_expected: true, + ) + + # Without include_linked: only the parent's own guest is returned + body = JSON.parse(client.get("#{BOOKINGS_BASE}/#{parent.id}/guests", headers: headers).body).as_a + body.map(&.["email"]).should eq([parent_guest.email]) + + # With include_linked: guests from both parent and child are returned + body = JSON.parse(client.get("#{BOOKINGS_BASE}/#{parent.id}/guests?include_linked=true", headers: headers).body).as_a + emails = body.map(&.["email"].as_s).sort! + emails.should eq(["child-guest@example.com", "parent-guest@example.com"]) + end + describe "permission", tags: ["auth", "group-event"] do it "#add_attendee should NOT allow adding public or same tenant users to PRIVATE bookings" do WebMock.stub(:post, "#{ENV["PLACE_URI"]}/auth/oauth/token") From f32324ee228e2b8f8f78a48e33f498d017724a3f Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Fri, 15 May 2026 15:22:42 +0930 Subject: [PATCH 5/6] refactor(visitor_mailer): (PPT-2375) --- spec/controllers/bookings_spec.cr | 18 ++++++++++++++++++ src/controllers/bookings.cr | 9 ++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/spec/controllers/bookings_spec.cr b/spec/controllers/bookings_spec.cr index d30ff1d9..079fda4f 100644 --- a/spec/controllers/bookings_spec.cr +++ b/spec/controllers/bookings_spec.cr @@ -1473,6 +1473,24 @@ describe Bookings do body = JSON.parse(client.get("#{BOOKINGS_BASE}/#{parent.id}/guests?include_linked=true", headers: headers).body).as_a emails = body.map(&.["email"].as_s).sort! emails.should eq(["child-guest@example.com", "parent-guest@example.com"]) + + # include_linked on a child booking should be silently ignored — + # only the child's own guests are returned + body = JSON.parse(client.get("#{BOOKINGS_BASE}/#{child.id}/guests?include_linked=true", headers: headers).body).as_a + body.map(&.["email"].as_s).should eq([child_guest.email]) + + # Deduplication: add the same guest (parent_guest) to the child booking. + # The API should return only one entry per email address. + Attendee.create!(booking_id: child.id.not_nil!, + guest_id: parent_guest.id, + tenant_id: parent_guest.tenant_id, + checked_in: false, + visit_expected: true, + ) + + body = JSON.parse(client.get("#{BOOKINGS_BASE}/#{parent.id}/guests?include_linked=true", headers: headers).body).as_a + emails = body.map(&.["email"].as_s).sort! + emails.should eq(["child-guest@example.com", "parent-guest@example.com"]) end describe "permission", tags: ["auth", "group-event"] do diff --git a/src/controllers/bookings.cr b/src/controllers/bookings.cr index 7eae1446..ad206c90 100644 --- a/src/controllers/bookings.cr +++ b/src/controllers/bookings.cr @@ -972,11 +972,18 @@ class Bookings < Application end if include_linked && booking.parent? - Booking.where(parent_id: booking.id).each do |child| + Booking.where(parent_id: booking.id) + .join(:left, Attendee, :booking_id) + .join(:left, Guest, "guests.id = attendees.guest_id") + .to_a.each do |child| child.attendees.to_a.each do |visitor| guests << visitor.guest.not_nil!.for_booking_to_h(visitor, child) end end + + # Deduplicate by email in case a guest appears on both the parent + # and a child booking + guests.uniq! { |guest| guest.email.downcase } end guests From 710a9caea2a3e726302bf021d8c2c5c0b30a6f13 Mon Sep 17 00:00:00 2001 From: Mia Bennett Date: Fri, 15 May 2026 15:31:07 +0930 Subject: [PATCH 6/6] test(ameba): reduce warnings --- .ameba.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.ameba.yml b/.ameba.yml index a661a83d..e336f5c0 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -54,8 +54,7 @@ Documentation/DocumentationAdmonition: Enabled: false Style/MultilineStringLiteral: - Excluded: - - spec/**/* + Enabled: false Lint/BeTruthy: Enabled: false @@ -68,3 +67,15 @@ Lint/SpecFilename: Lint/UselessAssign: Excluded: - spec/**/* + +Lint/WhitespaceAroundMacroExpression: + Enabled: false + +Style/PercentLiteralDelimiters: + Enabled: false + +Style/RedundantSelf: + Enabled: false + +Lint/SpecEqWithBoolOrNilLiteral: + Enabled: false