From 4d2b7aa9e5042da61783453e28de3b4347bb9730 Mon Sep 17 00:00:00 2001 From: Ali Naqvi Date: Fri, 28 Jun 2024 21:56:17 +0800 Subject: [PATCH] feat: PPT-1326 Add endpoint to provide a list of booking ids --- OPENAPI_DOC.yml | 302 ++++++++++++++++++++++++++++++ spec/controllers/bookings_spec.cr | 27 +++ src/controllers/bookings.cr | 70 ++++++- 3 files changed, 395 insertions(+), 4 deletions(-) diff --git a/OPENAPI_DOC.yml b/OPENAPI_DOC.yml index 198fb614..758265ac 100644 --- a/OPENAPI_DOC.yml +++ b/OPENAPI_DOC.yml @@ -194,6 +194,15 @@ paths: schema: type: integer format: Int32 + - name: permission + in: query + description: 'filters bookings based on the permission level. Options: PRIVATE, + OPEN, PUBLIC' + example: PUBLIC + required: false + schema: + type: string + nullable: true responses: 200: description: OK @@ -405,6 +414,287 @@ paths: application/json: schema: $ref: '#/components/schemas/Bookings__BookingError' + /api/staff/v1/bookings/booked: + get: + summary: lists bookings IDs based on the parameters provided + description: 'lists bookings IDs based on the parameters provided + + + booking_type is required unless event_id or ical_uid is present' + tags: + - Bookings + operationId: Bookings#booked + parameters: + - name: period_start + in: query + description: booking period start as a unix epoch + example: "1661725146" + required: false + schema: + type: integer + format: Int64 + - name: period_end + in: query + description: booking period end as a unix epoch + example: "1661743123" + required: false + schema: + type: integer + format: Int64 + - name: type + in: query + description: the generic name of the asset whose bookings you wish to view + example: desk + required: false + schema: + type: string + nullable: true + - name: deleted + in: query + description: when true, it returns deleted bookings + example: "true" + required: false + schema: + type: boolean + - name: include_checked_out + in: query + description: when true, returns all bookings including checked out ones + example: "true" + required: false + schema: + type: boolean + - name: checked_out + in: query + description: when true, only returns checked out bookings, unless `include_checked_out=true` + example: "true" + required: false + schema: + type: boolean + - name: zones + in: query + description: this filters only bookings in the zones provided, multiple zones + can be provided comma seperated + example: zone-123,zone-456 + required: false + schema: + type: string + nullable: true + - name: email + in: query + description: filters bookings owned by this user email + example: user@org.com + required: false + schema: + type: string + nullable: true + - name: user + in: query + description: filters bookings owned by this user id + example: user-1234 + required: false + schema: + type: string + nullable: true + - name: include_booked_by + in: query + description: if `email` or `user` parameters are set, this includes bookings + that user booked on behalf of others + example: "true" + required: false + schema: + type: boolean + nullable: true + - name: checked_in + in: query + description: filters bookings that have been checked in or not + example: "true" + required: false + schema: + type: boolean + nullable: true + - name: created_before + in: query + description: filters bookings that were created before the unix epoch specified + example: "1661743123" + required: false + schema: + type: integer + format: Int64 + nullable: true + - name: created_after + in: query + description: filters bookings that were created after the unix epoch specified + example: "1661743123" + required: false + schema: + type: integer + format: Int64 + nullable: true + - name: approved + in: query + description: filters bookings that are approved or not + example: "true" + required: false + schema: + type: boolean + nullable: true + - name: rejected + in: query + description: filters bookings that are rejected or not + example: "true" + required: false + schema: + type: boolean + nullable: true + - name: extension_data + in: query + description: filters bookings with matching extension data entries + example: '{"entry1":"value to match","entry2":1234}' + required: false + schema: + type: string + nullable: true + - name: state + in: query + description: filters on the booking process state, a user defined value + example: pending-approval + required: false + schema: + type: string + nullable: true + - name: department + in: query + description: filters bookings owned by a department, a user defined value + example: accounting + required: false + schema: + type: string + nullable: true + - name: event_id + in: query + description: filters bookings associated with an event, such as an Office365 + Calendar event id + example: AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZe + required: false + schema: + type: string + nullable: true + - name: ical_uid + in: query + description: filters bookings associated with an event, such as an Office365 + Calendar event ical_uid + example: 19rh93h5t893h5v@calendar.iCloud.com + required: false + schema: + type: string + nullable: true + - name: limit + in: query + description: the maximum number of results to return + example: "10000" + required: false + schema: + type: integer + format: Int32 + - name: offset + in: query + description: the starting offset of the result set. Used to implement pagination + required: false + schema: + type: integer + format: Int32 + - name: permission + in: query + description: 'filters bookings based on the permission level. Options: PRIVATE, + OPEN, PUBLIC' + example: PUBLIC + required: false + schema: + type: string + nullable: true + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Int64' + 429: + description: Too Many Requests + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 403: + description: Forbidden + 404: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 511: + description: Network Authentication Required + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 406: + description: Not Acceptable + content: + application/json: + schema: + $ref: '#/components/schemas/Application__ContentError' + 415: + description: Unsupported Media Type + content: + application/json: + schema: + $ref: '#/components/schemas/Application__ContentError' + 422: + description: Unprocessable Entity + content: + application/json: + schema: + $ref: '#/components/schemas/Application__ValidationError' + 500: + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 405: + description: Method Not Allowed + content: + application/json: + schema: + $ref: '#/components/schemas/Application__CommonError' + 409: + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/Bookings__BookingError' + 410: + description: Gone + content: + application/json: + schema: + $ref: '#/components/schemas/Bookings__BookingError' /api/staff/v1/bookings/{id}: get: summary: returns the booking requested @@ -8294,6 +8584,9 @@ components: type: integer format: Int64 nullable: true + Int64: + type: integer + format: Int64 PlaceOS__Model__Guest: type: object properties: @@ -9600,8 +9893,14 @@ components: nullable: true email: type: string + phone: + type: string + nullable: true username: type: string + suspended: + type: boolean + nullable: true required: - id - email @@ -9646,6 +9945,9 @@ components: office_location: type: string nullable: true + suspended: + type: boolean + nullable: true PlaceOS__Model__Survey: type: object properties: diff --git a/spec/controllers/bookings_spec.cr b/spec/controllers/bookings_spec.cr index 8beb8b71..f4e9bb6e 100644 --- a/spec/controllers/bookings_spec.cr +++ b/spec/controllers/bookings_spec.cr @@ -39,6 +39,33 @@ describe Bookings do body.size.should eq(1) end + it "should return a list of bookings ids" do + tenant = get_tenant + + booking1 = BookingsHelper.create_booking(tenant.id.not_nil!) + booking2 = BookingsHelper.create_booking(tenant.id.not_nil!) + + starting = 5.minutes.from_now.to_unix + ending = 90.minutes.from_now.to_unix + route = "#{BOOKINGS_BASE}/booked?period_start=#{starting}&period_end=#{ending}&user=#{booking1.user_email}&type=desk" + body = JSON.parse(client.get(route, headers: headers).body).as_a + + body.first.should eq(booking1.id) + + # filter by zones + zones1 = booking1.zones.not_nil! + zones_string = "#{zones1.first},#{booking2.zones.not_nil!.last}" + route = "#{BOOKINGS_BASE}/booked?period_start=#{starting}&period_end=#{ending}&type=desk&zones=#{zones_string}" + + body = JSON.parse(client.get(route, headers: headers).body).as_a + body.map(&.as_i).sort!.should eq [booking1.id, booking2.id] + + # More filters by zones + route = "#{BOOKINGS_BASE}/booked?period_start=#{starting}&period_end=#{ending}&type=desk&zones=#{zones1.first}" + body = JSON.parse(client.get(route, headers: headers).body).as_a + body.first.in?([booking1.id, booking2.id]).should be_true + end + it "should supports pagination on list of bookings request" do tenant = get_tenant diff --git a/src/controllers/bookings.cr b/src/controllers/bookings.cr index bd47487f..bbd464ba 100644 --- a/src/controllers/bookings.cr +++ b/src/controllers/bookings.cr @@ -27,13 +27,13 @@ class Bookings < Application # Skip actions that requres login # If a user is logged in then they will be run as part of # #set_tenant_from_domain - skip_action :determine_tenant_from_domain, only: [:index, :add_attendee] - skip_action :check_jwt_scope, only: [:index, :add_attendee] + skip_action :determine_tenant_from_domain, only: [:index, :add_attendee, :booked] + skip_action :check_jwt_scope, only: [:index, :add_attendee, :booked] # Set the tenant based on the domain # This allows unauthenticated requests through # (for public bookings, further checks are done later) - @[AC::Route::Filter(:before_action, only: [:index, :add_attendee])] + @[AC::Route::Filter(:before_action, only: [:index, :add_attendee, :booked])] private def set_tenant_from_domain if auth_token_present? check_jwt_scope @@ -46,7 +46,7 @@ class Bookings < Application end end - @[AC::Route::Filter(:before_action, except: [:index, :create])] + @[AC::Route::Filter(:before_action, except: [:index, :create, :booked])] private def find_booking(id : Int64) @booking = Booking .by_tenant(tenant.id) @@ -259,6 +259,68 @@ class Bookings < Application result end + # lists bookings IDs based on the parameters provided + # + # booking_type is required unless event_id or ical_uid is present + @[AC::Route::GET("/booked")] + def booked( + @[AC::Param::Info(name: "period_start", description: "booking period start as a unix epoch", example: "1661725146")] + starting : Int64 = Time.utc.to_unix, + @[AC::Param::Info(name: "period_end", description: "booking period end as a unix epoch", example: "1661743123")] + ending : Int64 = 1.hours.from_now.to_unix, + @[AC::Param::Info(name: "type", description: "the generic name of the asset whose bookings you wish to view", example: "desk")] + booking_type : String? = nil, + @[AC::Param::Info(name: "deleted", description: "when true, it returns deleted bookings", example: "true")] + deleted_flag : Bool = false, + @[AC::Param::Info(description: "when true, returns all bookings including checked out ones", example: "true")] + include_checked_out : Bool = false, + @[AC::Param::Info(name: "checked_out", description: "when true, only returns checked out bookings, unless `include_checked_out=true`", example: "true")] + checked_out_flag : Bool = false, + @[AC::Param::Info(description: "this filters only bookings in the zones provided, multiple zones can be provided comma seperated", example: "zone-123,zone-456")] + zones : String? = nil, + @[AC::Param::Info(name: "email", description: "filters bookings owned by this user email", example: "user@org.com")] + user_email : String? = nil, + @[AC::Param::Info(name: "user", description: "filters bookings owned by this user id", example: "user-1234")] + user_id : String? = nil, + @[AC::Param::Info(description: "if `email` or `user` parameters are set, this includes bookings that user booked on behalf of others", example: "true")] + include_booked_by : Bool? = nil, + + @[AC::Param::Info(description: "filters bookings that have been checked in or not", example: "true")] + checked_in : Bool? = nil, + @[AC::Param::Info(description: "filters bookings that were created before the unix epoch specified", example: "1661743123")] + created_before : Int64? = nil, + @[AC::Param::Info(description: "filters bookings that were created after the unix epoch specified", example: "1661743123")] + created_after : Int64? = nil, + @[AC::Param::Info(description: "filters bookings that are approved or not", example: "true")] + approved : Bool? = nil, + @[AC::Param::Info(description: "filters bookings that are rejected or not", example: "true")] + rejected : Bool? = nil, + @[AC::Param::Info(description: "filters bookings with matching extension data entries", example: %({"entry1":"value to match","entry2":1234}))] + extension_data : String? = nil, + @[AC::Param::Info(description: "filters on the booking process state, a user defined value", example: "pending-approval")] + state : String? = nil, + @[AC::Param::Info(description: "filters bookings owned by a department, a user defined value", example: "accounting")] + department : String? = nil, + + @[AC::Param::Info(description: "filters bookings associated with an event, such as an Office365 Calendar event id", example: "AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZe")] + event_id : String? = nil, + @[AC::Param::Info(description: "filters bookings associated with an event, such as an Office365 Calendar event ical_uid", example: "19rh93h5t893h5v@calendar.iCloud.com")] + ical_uid : String? = nil, + @[AC::Param::Info(description: "the maximum number of results to return", example: "10000")] + limit : Int32 = 100, + @[AC::Param::Info(description: "the starting offset of the result set. Used to implement pagination")] + offset : Int32 = 0, + @[AC::Param::Info(description: "filters bookings based on the permission level. Options: PRIVATE, OPEN, PUBLIC", example: "PUBLIC")] + permission : String? = nil + ) : Array(Int64) + result = index(starting: starting, ending: ending, booking_type: booking_type, deleted_flag: deleted_flag, include_checked_out: include_checked_out, + checked_out_flag: checked_out_flag, zones: zones, user_email: user_email, user_id: user_id, include_booked_by: include_booked_by, checked_in: checked_in, + created_before: created_before, created_after: created_after, approved: approved, rejected: rejected, extension_data: extension_data, state: state, + department: department, event_id: event_id, ical_uid: ical_uid, limit: limit, offset: offset, permission: permission) + + result.map(&.id.as(Int64)) + end + # creates a new booking @[AC::Route::POST("/", body: :booking, status_code: HTTP::Status::CREATED)] def create(