diff --git a/OPENAPI_DOC.yml b/OPENAPI_DOC.yml index baf7f09b..a858dc33 100644 --- a/OPENAPI_DOC.yml +++ b/OPENAPI_DOC.yml @@ -10156,7 +10156,7 @@ components: trigger: type: object description: 'Triggers on booking states: RESERVED, CHECKEDIN, CHECKEDOUT, - REJECTED, CANCELLED' + REJECTED, CANCELLED, VISITOR_CHECKEDIN, VISITOR_CHECKEDOUT' nullable: true zone_id: type: string diff --git a/docker-compose.yml b/docker-compose.yml index 8fcde1ec..09be2465 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,9 +13,6 @@ services: POSTGRES_USER: postgres POSTGRES_PASSWORD: password POSTGRES_DB: staff_api_test - healthcheck: - test: /usr/bin/pg_isready - interval: 5s ports: - 5432:5432 diff --git a/spec/controllers/helpers/booking_helper.cr b/spec/controllers/helpers/booking_helper.cr index 669c023d..ea2dc27c 100644 --- a/spec/controllers/helpers/booking_helper.cr +++ b/spec/controllers/helpers/booking_helper.cr @@ -35,6 +35,17 @@ module BookingsHelper booking.save! end + def create_booking(tenant_id : Int64, user_email : String, zones : Array(String), booking_start : Int64, booking_end : Int64) + booking = create_booking( + tenant_id: tenant_id, + user_email: user_email, + zones: zones, + ) + booking.booking_start = booking_start + booking.booking_end = booking_end + booking.save! + end + def create_booking(tenant_id : Int64, booking_start : Int64, booking_end : Int64, asset_id : String) booking = create_booking(tenant_id) booking.booking_start = booking_start diff --git a/spec/controllers/surveys/triggers_spec.cr b/spec/controllers/surveys/triggers_spec.cr index 379f77bc..8cfeab93 100644 --- a/spec/controllers/surveys/triggers_spec.cr +++ b/spec/controllers/surveys/triggers_spec.cr @@ -7,9 +7,24 @@ describe "Survey Triggers", tags: ["survey"] do client = AC::SpecHelper.client headers = Mock::Headers.office365_guest - pending "should create an invitation on booking state change" do + before_each do + # Booking.query.each(&.delete) + WebMock.stub(:post, "#{ENV["PLACE_URI"]}/auth/oauth/token") + .to_return(body: File.read("./spec/fixtures/tokens/placeos_token.json")) + WebMock.stub(:post, "#{ENV["PLACE_URI"]}/api/engine/v2/signal?channel=staff/booking/changed") + .to_return(body: "") + + Timecop.scale(600) # 1 second == 10 minutes + end + + after_all do + WebMock.reset + Timecop.scale(1) + end + + it "should create an invitation on RESERVED trigger" do survey = SurveyHelper.create_survey( - zone_id: "zone-3", + zone_id: "zone-2", building_id: "zone-1", trigger: "RESERVED", ) @@ -22,10 +37,168 @@ describe "Survey Triggers", tags: ["survey"] do ) invitations = Survey::Invitation.query.select("id").map(&.id) - # pp "########################################" - # pp! booking.current_state - # pp! booking.history.map(&.state) - # pp! invitations - # pp "########################################" + invitations.size.should eq(1) + end + + it "should create an invitation on CHECKEDIN trigger" do + survey = SurveyHelper.create_survey( + zone_id: "zone-2", + building_id: "zone-1", + trigger: "CHECKEDIN", + ) + + tenant = Tenant.query.find! { domain == "toby.staff-api.dev" } + booking = BookingsHelper.create_booking( + tenant_id: tenant.id, + user_email: "user@example.com", + zones: ["zone-1", "zone-2"], + booking_start: 1.minutes.from_now.to_unix, + booking_end: 9.minutes.from_now.to_unix, + ) + + client.post("#{BOOKINGS_BASE}/#{booking.id}/check_in?state=true", headers: headers) + + invitations = Survey::Invitation.query.select("id").map(&.id) + invitations.size.should eq(1) + end + + it "should create an invitation on CHECKEDOUT trigger" do + survey = SurveyHelper.create_survey( + zone_id: "zone-2", + building_id: "zone-1", + trigger: "CHECKEDOUT", + ) + + tenant = Tenant.query.find! { domain == "toby.staff-api.dev" } + booking = BookingsHelper.create_booking( + tenant_id: tenant.id, + user_email: "user@example.com", + zones: ["zone-1", "zone-2"], + booking_start: 1.minutes.from_now.to_unix, + booking_end: 9.minutes.from_now.to_unix, + ) + + client.post("#{BOOKINGS_BASE}/#{booking.id}/check_in?state=false", headers: headers) + + invitations = Survey::Invitation.query.select("id").map(&.id) + invitations.size.should eq(1) + end + + it "should create an invitation on REJECTED trigger" do + survey = SurveyHelper.create_survey( + zone_id: "zone-2", + building_id: "zone-1", + trigger: "REJECTED", + ) + + tenant = Tenant.query.find! { domain == "toby.staff-api.dev" } + booking = BookingsHelper.create_booking( + tenant_id: tenant.id, + user_email: "user@example.com", + zones: ["zone-1", "zone-2"], + booking_start: 1.minutes.from_now.to_unix, + booking_end: 9.minutes.from_now.to_unix, + ) + + client.post("#{BOOKINGS_BASE}/#{booking.id}/reject", headers: headers) + + invitations = Survey::Invitation.query.select("id").map(&.id) + invitations.size.should eq(1) + end + + it "should create an invitation on CANCELLED trigger" do + survey = SurveyHelper.create_survey( + zone_id: "zone-2", + building_id: "zone-1", + trigger: "CANCELLED", + ) + + tenant = Tenant.query.find! { domain == "toby.staff-api.dev" } + booking = BookingsHelper.create_booking( + tenant_id: tenant.id, + user_email: "user@example.com", + zones: ["zone-1", "zone-2"], + booking_start: 1.minutes.from_now.to_unix, + booking_end: 9.minutes.from_now.to_unix, + ) + + client.delete("#{BOOKINGS_BASE}/#{booking.id}", headers: headers) + + invitations = Survey::Invitation.query.select("id").map(&.id) + invitations.size.should eq(1) + end + + it "should create an invitation on VISITOR_CHECKEDIN trigger" do + survey = SurveyHelper.create_survey( + zone_id: "zone-2", + building_id: "zone-1", + trigger: "VISITOR_CHECKEDIN", + ) + + tenant = Tenant.query.find! { domain == "toby.staff-api.dev" } + booking = BookingsHelper.create_booking( + tenant_id: tenant.id, + user_email: "user@example.com", + zones: ["zone-1", "zone-2"], + booking_start: 1.minutes.from_now.to_unix, + booking_end: 9.minutes.from_now.to_unix, + ) + guest = Guest.create!({ + name: Faker::Name.name, + email: "visitor@example.com", + tenant_id: tenant.id, + banned: false, + dangerous: false, + }) + visitor = Attendee.create!({ + tenant_id: guest.tenant_id, + booking_id: booking.id, + guest_id: guest.id, + checked_in: false, + visit_expected: true, + }) + + visitor.checked_in = true + visitor.save! + + invitations = Survey::Invitation.query.select("id").map(&.id) + invitations.size.should eq(1) + end + + pending "should create an invitation on VISITOR_CHECKEDOUT trigger" do + survey = SurveyHelper.create_survey( + zone_id: "zone-2", + building_id: "zone-1", + trigger: "VISITOR_CHECKEDOUT", + ) + + tenant = Tenant.query.find! { domain == "toby.staff-api.dev" } + booking = BookingsHelper.create_booking( + tenant_id: tenant.id, + user_email: "user@example.com", + zones: ["zone-1", "zone-2"], + booking_start: 1.minutes.from_now.to_unix, + booking_end: 9.minutes.from_now.to_unix, + ) + guest = Guest.create!({ + name: Faker::Name.name, + email: "visitor@example.com", + tenant_id: tenant.id, + banned: false, + dangerous: false, + }) + _visitor = Attendee.create!({ + tenant_id: guest.tenant_id, + booking_id: booking.id, + guest_id: guest.id, + checked_in: true, + visit_expected: true, + }) + + visitor.checked_in = false + visitor.save! + + invitations = Survey::Invitation.query.select("id").map(&.id) + invitations.size.should eq(1) end end diff --git a/src/migrations/0030_alter_enum_trigger_type.cr b/src/migrations/0030_alter_enum_trigger_type.cr new file mode 100644 index 00000000..77ba6a7e --- /dev/null +++ b/src/migrations/0030_alter_enum_trigger_type.cr @@ -0,0 +1,12 @@ +class AlterEnumTriggerType + include Clear::Migration + + def change(dir) + dir.up do + execute("ALTER TYPE survey_trigger_type ADD VALUE 'VISITOR_CHECKEDIN'") + execute("ALTER TYPE survey_trigger_type ADD VALUE 'VISITOR_CHECKEDOUT'") + end + + # No down migration, as enum does not support removal of values + end +end diff --git a/src/models/attendee.cr b/src/models/attendee.cr index f79a6825..5ed3f638 100644 --- a/src/models/attendee.cr +++ b/src/models/attendee.cr @@ -26,6 +26,33 @@ class Attendee delegate email, name, preferred_name, phone, organisation, notes, photo, to: guest + before(:save) do |m| + attendee_model = m.as(Attendee) + attendee_model.survey_trigger + end + + def survey_trigger + return unless checked_in_column.changed? + state = checked_in ? "VISITOR_CHECKEDIN" : "VISITOR_CHECKEDOUT" + + query = Survey.query.select("id").where(trigger: state) + + if (b = booking) && (zones = b.zones) && !zones.empty? + query = query.where { var("zone_id").in?(zones) & var("building_id").in?(zones) } + end + + email = guest.email + unless email.empty? + surveys = query.to_a + surveys.each do |survey| + Survey::Invitation.create!( + survey_id: survey.id, + email: email, + ) + end + end + end + struct AttendeeResponse include JSON::Serializable include AutoInitialize diff --git a/src/models/survey.cr b/src/models/survey.cr index cca0a585..6b2f89b7 100644 --- a/src/models/survey.cr +++ b/src/models/survey.cr @@ -1,6 +1,7 @@ require "./survey/*" -Clear.enum TriggerType, "NONE", "RESERVED", "CHECKEDIN", "CHECKEDOUT", "NOSHOW", "REJECTED", "CANCELLED", "ENDED" +# No trigger actually fires on NOSHOW or ENDED, but Postgres doesn't support removing enum values +Clear.enum TriggerType, "NONE", "RESERVED", "CHECKEDIN", "CHECKEDOUT", "NOSHOW", "REJECTED", "CANCELLED", "ENDED", "VISITOR_CHECKEDIN", "VISITOR_CHECKEDOUT" class Survey include Clear::Model @@ -24,7 +25,7 @@ class Survey getter id : Int64? getter title : String? = nil getter description : String? = nil - @[JSON::Field(description: "Triggers on booking states: RESERVED, CHECKEDIN, CHECKEDOUT, REJECTED, CANCELLED")] + @[JSON::Field(description: "Triggers on booking states: RESERVED, CHECKEDIN, CHECKEDOUT, REJECTED, CANCELLED, VISITOR_CHECKEDIN, VISITOR_CHECKEDOUT")] getter trigger : TriggerType? = nil getter zone_id : String? = nil getter building_id : String? = nil