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