diff --git a/lib/v2/bot/fetch_next_week_ptos_from_notion.rb b/lib/v2/bot/fetch_next_week_ptos_from_notion.rb new file mode 100644 index 0000000..b3e17cf --- /dev/null +++ b/lib/v2/bot/fetch_next_week_ptos_from_notion.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require_relative "./base" +require_relative "../read/default" +require_relative "../utils/notion/request" +require_relative "../write/postgres" + +module Bot + ## + # The Bot::FetchNextWeekPtosFromNotion class serves as a bot implementation to read next week + # PTO's from a notion database and write them on a PostgresDB table with a specific format. + # + #
+ # Example + # + # options = { + # process_options: { + # database_id: "notion database id", + # secret: "notion secret" + # }, + # write_options: { + # connection: { + # host: "host", + # port: 5432, + # dbname: "bas", + # user: "postgres", + # password: "postgres" + # }, + # db_table: "pto", + # bot_name: "FetchNextWeekPtosFromNotion" + # } + # } + # + # bot = Bot::FetchNextWeekPtosFromNotion.new(options) + # bot.execute + # + class FetchNextWeekPtosFromNotion < Bot::Base + # Read function to execute the default Read component + # + def read + reader = Read::Default.new + + reader.execute + end + + # Process function to execute the Notion utility to fetch next week PTO's from the notion database + # + def process(_read_response) + response = Utils::Notion::Request.execute(params) + + if response.code == 200 + ptos_list = normalize_response(response.parsed_response["results"]) + + { success: { ptos: ptos_list } } + else + { error: { message: response.parsed_response, status_code: response.code } } + end + end + + # Write function to execute the PostgresDB write component + # + def write(process_response) + write = Write::Postgres.new(write_options, process_response) + + write.execute + end + + private + + def params + { + endpoint: "databases/#{process_options[:database_id]}/query", + secret: process_options[:secret], + method: "post", + body: + } + end + + def body + monday, sunday = next_week_dates + + { + filter: { + or: [ + belong_next_week("StartDateTime", monday, sunday), + belong_next_week("EndDateTime", monday, sunday), + cover_next_week(monday, sunday) + ] + } + } + end + + def next_week_dates + monday = next_week_monday + sunday = monday + 6 + + [monday, sunday] + end + + def next_week_monday + today = Date.today + week_day = today.wday + + days = week_day.zero? ? 1 : 8 - week_day + + today + days + end + + def belong_next_week(property, after_day, before_day) + { + and: [ + { property:, date: { on_or_after: after_day } }, + { property:, date: { on_or_before: before_day } } + ] + } + end + + def cover_next_week(monday, sunday) + { + and: [ + { property: "EndDateTime", date: { on_or_after: sunday } }, + { property: "StartDateTime", date: { on_or_before: monday } } + ] + } + end + + def normalize_response(results) + return [] if results.nil? + + results.map do |pto| + pto_fields = pto["properties"] + + { + "Name" => extract_description_field_value(pto_fields["Description"]), + "StartDateTime" => extract_date_field_value(pto_fields["StartDateTime"]), + "EndDateTime" => extract_date_field_value(pto_fields["EndDateTime"]) + } + end + end + + def extract_description_field_value(data) + names = data["title"].map { |name| name["plain_text"] } + + names.join(" ") + end + + def extract_date_field_value(date) + { + from: extract_start_date(date), + to: extract_end_date(date) + } + end + + def extract_start_date(data) + data["date"]["start"] + end + + def extract_end_date(data) + data["date"]["end"] + end + end +end diff --git a/spec/v2/bot/fetch_next_week_ptos_from_notion_spec.rb b/spec/v2/bot/fetch_next_week_ptos_from_notion_spec.rb new file mode 100644 index 0000000..dc61085 --- /dev/null +++ b/spec/v2/bot/fetch_next_week_ptos_from_notion_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +require "v2/bot/fetch_next_week_ptos_from_notion" + +RSpec.describe Bot::FetchNextWeekPtosFromNotion do + before do + config = { + process_options: { + database_id: "database_id", + secret: "secret" + }, + write_options: { + connection: { + host: "host", + port: 5432, + dbname: "bas", + user: "postgres", + password: "postgres" + }, + db_table: "pto", + bot_name: "FetchPtosFromNotion" + } + } + + @bot = described_class.new(config) + end + + describe "attributes and arguments" do + it { expect(described_class).to respond_to(:new).with(1).arguments } + + it { expect(@bot).to respond_to(:execute).with(0).arguments } + it { expect(@bot).to respond_to(:read).with(0).arguments } + it { expect(@bot).to respond_to(:process).with(1).arguments } + it { expect(@bot).to respond_to(:write).with(1).arguments } + + it { expect(@bot).to respond_to(:read_options) } + it { expect(@bot).to respond_to(:process_options) } + it { expect(@bot).to respond_to(:write_options) } + end + + describe ".read" do + it { expect(@bot.read).to be_a Read::Types::Response } + end + + describe ".process" do + let(:pto) do + { + "properties" => { + "Description" => { "title" => [{ "plain_text" => "John Doe" }] }, + "StartDateTime" => { "date" => { "start" => "2024-05-01", "end" => "" } }, + "EndDateTime" => { "date" => { "start" => "2024-05-02", "end" => "" } } + } + } + end + + let(:formatted_pto) do + { + "Name" => "John Doe", + "StartDateTime" => { from: "2024-05-01", to: "" }, + "EndDateTime" => { from: "2024-05-02", to: "" } + } + end + + let(:error_response) { { "object" => "error", "status" => 404, "message" => "not found" } } + + let(:response) { double("http_response") } + + before do + @read_response = Read::Types::Response.new + + allow(HTTParty).to receive(:send).and_return(response) + end + + it "returns a success hash with the list of formatted ptos" do + allow(response).to receive(:code).and_return(200) + allow(response).to receive(:parsed_response).and_return({ "results" => [pto] }) + + processed = @bot.process(@read_response) + + expect(processed).to eq({ success: { ptos: [formatted_pto] } }) + end + + it "returns an error hash with the error message" do + allow(response).to receive(:code).and_return(404) + allow(response).to receive(:parsed_response).and_return(error_response) + + processed = @bot.process(@read_response) + + expect(processed).to eq({ error: { message: error_response, status_code: 404 } }) + end + end + + describe ".write" do + let(:pg_conn) { instance_double(PG::Connection) } + + let(:formatted_pto) do + { + "Name" => "John Doe", + "StartDateTime" => { from: "2024-05-01", to: "" }, + "EndDateTime" => { from: "2024-05-02", to: "" } + } + end + + let(:error_response) { { "object" => "error", "status" => 404, "message" => "not found" } } + + before do + pg_result = instance_double(PG::Result) + + allow(PG::Connection).to receive(:new).and_return(pg_conn) + allow(pg_conn).to receive(:exec_params).and_return(pg_result) + end + + it "save the process success response in a postgres table" do + process_response = { success: { ptos: [formatted_pto] } } + + expect(@bot.write(process_response)).to_not be_nil + end + + it "save the process fail response in a postgres table" do + process_response = { error: { message: error_response, status_code: 404 } } + + expect(@bot.write(process_response)).to_not be_nil + end + end +end