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 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/spec/controllers/bookings_spec.cr b/spec/controllers/bookings_spec.cr index 31d5e9b5..079fda4f 100644 --- a/spec/controllers/bookings_spec.cr +++ b/spec/controllers/bookings_spec.cr @@ -1432,6 +1432,67 @@ 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"]) + + # 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 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") diff --git a/src/controllers/bookings.cr b/src/controllers/bookings.cr index c6b72088..ad206c90 100644 --- a/src/controllers/bookings.cr +++ b/src/controllers/bookings.cr @@ -963,10 +963,30 @@ 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) + .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 end # marks the standalone visitor as checked-in or checked-out based on the state param