bns 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/._.rspec_status +0 -0
  3. data/CHANGELOG.md +9 -0
  4. data/Gemfile +2 -0
  5. data/README.md +159 -3
  6. data/lib/bns/dispatcher/discord/exceptions/invalid_webhook_token.rb +2 -2
  7. data/lib/bns/dispatcher/discord/implementation.rb +1 -1
  8. data/lib/bns/dispatcher/slack/exceptions/invalid_webhook_token.rb +16 -0
  9. data/lib/bns/dispatcher/slack/implementation.rb +51 -0
  10. data/lib/bns/dispatcher/slack/types/response.rb +21 -0
  11. data/lib/bns/domain/work_items_limit.rb +39 -0
  12. data/lib/bns/fetcher/base.rb +13 -0
  13. data/lib/bns/fetcher/notion/{pto.rb → base.rb} +11 -7
  14. data/lib/bns/fetcher/notion/types/response.rb +1 -1
  15. data/lib/bns/fetcher/notion/use_case/birthday_next_week.rb +41 -0
  16. data/lib/bns/fetcher/notion/use_case/birthday_today.rb +29 -0
  17. data/lib/bns/fetcher/notion/use_case/pto_next_week.rb +71 -0
  18. data/lib/bns/fetcher/notion/use_case/pto_today.rb +30 -0
  19. data/lib/bns/fetcher/notion/use_case/work_items_limit.rb +37 -0
  20. data/lib/bns/fetcher/postgres/base.rb +46 -0
  21. data/lib/bns/fetcher/postgres/helper.rb +16 -0
  22. data/lib/bns/fetcher/postgres/types/response.rb +42 -0
  23. data/lib/bns/fetcher/postgres/use_case/pto_today.rb +32 -0
  24. data/lib/bns/formatter/base.rb +11 -8
  25. data/lib/bns/formatter/birthday.rb +34 -0
  26. data/lib/bns/formatter/exceptions/invalid_data.rb +15 -0
  27. data/lib/bns/formatter/pto.rb +76 -0
  28. data/lib/bns/formatter/work_items_limit.rb +43 -0
  29. data/lib/bns/mapper/notion/{birthday.rb → birthday_today.rb} +13 -21
  30. data/lib/bns/mapper/notion/{pto.rb → pto_today.rb} +15 -41
  31. data/lib/bns/mapper/notion/work_items_limit.rb +65 -0
  32. data/lib/bns/mapper/postgres/pto_today.rb +47 -0
  33. data/lib/bns/use_cases/use_cases.rb +227 -49
  34. data/lib/bns/version.rb +1 -1
  35. metadata +25 -9
  36. data/lib/bns/fetcher/notion/birthday.rb +0 -53
  37. data/lib/bns/formatter/discord/birthday.rb +0 -36
  38. data/lib/bns/formatter/discord/exceptions/invalid_data.rb +0 -17
  39. data/lib/bns/formatter/discord/pto.rb +0 -49
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pg"
4
+
5
+ require_relative "../base"
6
+ require_relative "./types/response"
7
+ require_relative "./helper"
8
+
9
+ module Fetcher
10
+ module Postgres
11
+ ##
12
+ # This class is an implementation of the Fetcher::Base interface, specifically designed
13
+ # for fetching data from Postgres.
14
+ #
15
+ class Base < Fetcher::Base
16
+ protected
17
+
18
+ # Implements the data fetching logic from a Postgres database. It use the PG gem
19
+ # to request data from a local or external database and returns a validated response.
20
+ #
21
+ # Gem: pg (https://rubygems.org/gems/pg)
22
+ #
23
+ def execute(query)
24
+ pg_connection = PG::Connection.new(config[:connection])
25
+
26
+ pg_result = execute_query(pg_connection, query)
27
+
28
+ postgres_response = Fetcher::Postgres::Types::Response.new(pg_result)
29
+
30
+ Fetcher::Postgres::Helper.validate_response(postgres_response)
31
+ end
32
+
33
+ private
34
+
35
+ def execute_query(pg_connection, query)
36
+ if query.is_a? String
37
+ pg_connection.exec(query)
38
+ else
39
+ sentence, params = query
40
+
41
+ pg_connection.exec_params(sentence, params)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fetcher
4
+ module Postgres
5
+ ##
6
+ # Provides common fuctionalities along the Postgres domain.
7
+ #
8
+ module Helper
9
+ def self.validate_response(response)
10
+ response.response.check_result
11
+
12
+ response
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fetcher
4
+ module Postgres
5
+ module Types
6
+ ##
7
+ # Represents a response received from the Postgres API. It encapsulates essential information about the response,
8
+ # providing a structured way to handle and analyze it's responses.
9
+ class Response
10
+ attr_reader :status, :message, :response, :fields, :records
11
+
12
+ SUCCESS_STATUS = "PGRES_TUPLES_OK"
13
+
14
+ def initialize(response)
15
+ if response.res_status == SUCCESS_STATUS
16
+ success_response(response)
17
+ else
18
+ failure_response(response)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def success_response(response)
25
+ @status = response.res_status
26
+ @message = "success"
27
+ @response = response
28
+ @fields = response.fields
29
+ @records = response.values
30
+ end
31
+
32
+ def failure_response(response)
33
+ @status = response.res_status
34
+ @message = response.result_error_message
35
+ @response = response
36
+ @fields = nil
37
+ @records = nil
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+
5
+ module Fetcher
6
+ module Postgres
7
+ ##
8
+ # This class is an implementation of the Fetcher::Postgres::Base interface, specifically designed
9
+ # for fetching Paid Time Off (PTO) data from a Postgres Database.
10
+ #
11
+ class PtoToday < Base
12
+ # Implements the data fetching query for todays PTO data from a Postgres database.
13
+ #
14
+ def fetch
15
+ execute(build_query)
16
+ end
17
+
18
+ private
19
+
20
+ def build_query
21
+ today = Time.now.utc.strftime("%F").to_s
22
+
23
+ start_time = "#{today}T00:00:00"
24
+ end_time = "#{today}T23:59:59"
25
+
26
+ where = "(start_date <= $1 AND end_date >= $1) OR (start_date>= $2 AND end_date <= $3)"
27
+
28
+ ["SELECT * FROM pto WHERE #{where}", [today, start_time, end_time]]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -10,8 +10,17 @@ module Formatter
10
10
  # formatters tailored to different use cases.
11
11
  #
12
12
  class Base
13
- # A method meant to give an specified format depending on the implementation to the data coming from an
14
- # implementation of the Mapper::Base interface.
13
+ attr_reader :template
14
+
15
+ # Initializes the fetcher with essential configuration parameters.
16
+ #
17
+ def initialize(config = {})
18
+ @config = config
19
+ @template = config[:template]
20
+ end
21
+
22
+ # This method is designed to provide a specified format for data from any implementation of
23
+ # the Mapper::Base interface.
15
24
  # Must be overridden by subclasses, with specific logic based on the use case.
16
25
  #
17
26
  # <br>
@@ -23,12 +32,6 @@ module Formatter
23
32
  #
24
33
  # <b>returns</b> <tt>String</tt> Formatted payload suitable for a Dispatcher::Base implementation.
25
34
  #
26
- attr_reader :template
27
-
28
- def initialize(config = {})
29
- @template = config[:template]
30
- end
31
-
32
35
  def format(_domain_data)
33
36
  raise Domain::Exceptions::FunctionNotImplemented
34
37
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../domain/birthday"
4
+ require_relative "./exceptions/invalid_data"
5
+ require_relative "./base"
6
+
7
+ module Formatter
8
+ ##
9
+ # This class implements methods from the Formatter::Base module, tailored to format the
10
+ # Domain::Birthday structure for a dispatcher.
11
+ class Birthday < Base
12
+ # Implements the logic for building a formatted payload with the given template for birthdays.
13
+ #
14
+ # <br>
15
+ # <b>Params:</b>
16
+ # * <tt>List<Domain::Birthday></tt> birthdays_list: list of mapped birthdays.
17
+ #
18
+ # <br>
19
+ # <b>raises</b> <tt>Formatter::Exceptions::InvalidData</tt> when invalid data is provided.
20
+ #
21
+ # <br>
22
+ # <b>returns</b> <tt>String</tt> payload: formatted payload suitable for a Dispatcher.
23
+ #
24
+ def format(birthdays_list)
25
+ raise Formatter::Exceptions::InvalidData unless birthdays_list.all? do |brithday|
26
+ brithday.is_a?(Domain::Birthday)
27
+ end
28
+
29
+ birthdays_list.reduce("") do |payload, birthday|
30
+ payload + build_template(Domain::Birthday::ATTRIBUTES, birthday)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formatter
4
+ module Exceptions
5
+ ##
6
+ # Provides a domain-specific representation for errors that occurs when trying to process invalid
7
+ # data on a Fetcher::Base implementation
8
+ #
9
+ class InvalidData < StandardError
10
+ def initialize(message = "")
11
+ super(message)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../domain/pto"
4
+ require_relative "./exceptions/invalid_data"
5
+ require_relative "./base"
6
+
7
+ module Formatter
8
+ ##
9
+ # This class implements methods from the Formatter::Base module, tailored to format the
10
+ # Domain::Pto structure for a dispatcher.
11
+ class Pto < Base
12
+ # Initializes the Slack formatter with essential configuration parameters.
13
+ #
14
+ # <b>timezone</b> : expect an string with the time difference relative to the UTC. Example: "-05:00"
15
+ def initialize(config = {})
16
+ super(config)
17
+
18
+ @timezone = config[:timezone]
19
+ end
20
+
21
+ # Implements the logic for building a formatted payload with the given template for PTO's.
22
+ #
23
+ # <br>
24
+ # <b>Params:</b>
25
+ # * <tt>List<Domain::Pto></tt> pto_list: List of mapped PTO's.
26
+ #
27
+ # <br>
28
+ # <b>raises</b> <tt>Formatter::Exceptions::InvalidData</tt> when invalid data is provided.
29
+ #
30
+ # <br>
31
+ # <b>returns</b> <tt>String</tt> payload, formatted payload suitable for a Dispatcher.
32
+ #
33
+
34
+ def format(ptos_list)
35
+ raise Formatter::Exceptions::InvalidData unless ptos_list.all? { |pto| pto.is_a?(Domain::Pto) }
36
+
37
+ ptos_list.reduce("") do |payload, pto|
38
+ built_template = build_template(Domain::Pto::ATTRIBUTES, pto)
39
+ payload + format_message_by_case(built_template.gsub("\n", ""), pto)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def format_message_by_case(built_template, pto)
46
+ date_start = format_timezone(pto.start_date).strftime("%F")
47
+ date_end = format_timezone(pto.end_date).strftime("%F")
48
+
49
+ if date_start == date_end
50
+ interval = same_day_interval(pto)
51
+ day_message = today?(date_start) ? "today" : "the day #{date_start}"
52
+
53
+ "#{built_template} #{day_message} #{interval}\n"
54
+ else
55
+ "#{built_template} from #{date_start} to #{date_end}\n"
56
+ end
57
+ end
58
+
59
+ def same_day_interval(pto)
60
+ time_start = format_timezone(pto.start_date).strftime("%I:%M %P")
61
+ time_end = format_timezone(pto.end_date).strftime("%I:%M %P")
62
+
63
+ time_start == time_end ? "all day" : "from #{time_start} to #{time_end}"
64
+ end
65
+
66
+ def format_timezone(date)
67
+ @timezone.nil? ? Time.new(date) : Time.new(date, in: @timezone)
68
+ end
69
+
70
+ def today?(date)
71
+ time_now = Time.now.strftime("%F")
72
+
73
+ date == format_timezone(time_now).strftime("%F")
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../domain/work_items_limit"
4
+ require_relative "./exceptions/invalid_data"
5
+ require_relative "./base"
6
+
7
+ module Formatter
8
+ ##
9
+ # This class implements methods from the Formatter::Base module, tailored to format the
10
+ # Domain::WorkItemsLimit structure for a dispatcher.
11
+ class WorkItemsLimit < Base
12
+ attr_reader :limit
13
+
14
+ # Implements the logic for building a formatted payload with the given template for wip limits.
15
+ #
16
+ # <br>
17
+ # <b>Params:</b>
18
+ # * <tt>List<Domain::WorkItemsLimit></tt> work_items_list: List of mapped work items limits.
19
+ #
20
+ # <br>
21
+ # <b>raises</b> <tt>Formatter::Exceptions::InvalidData</tt> when invalid data is provided.
22
+ #
23
+ # <br>
24
+ # <b>returns</b> <tt>String</tt> payload, formatted payload suitable for a Dispatcher.
25
+ #
26
+
27
+ def format(work_items_list)
28
+ raise Formatter::Exceptions::InvalidData unless work_items_list.all? do |work_item|
29
+ work_item.is_a?(Domain::WorkItemsLimit)
30
+ end
31
+
32
+ exceeded_domains(work_items_list).reduce("") do |payload, work_items_limit|
33
+ payload + build_template(Domain::WorkItemsLimit::ATTRIBUTES, work_items_limit)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def exceeded_domains(work_items_list)
40
+ work_items_list.filter { |work_item| work_item.total > work_item.wip_limit }
41
+ end
42
+ end
43
+ end
@@ -8,9 +8,11 @@ module Mapper
8
8
  ##
9
9
  # This class implementats the methods of the Mapper::Base module, specifically designed for preparing or
10
10
  # shaping birthdays data coming from a Fetcher::Base implementation.
11
- class Birthday
11
+ class BirthdayToday
12
12
  include Base
13
13
 
14
+ BIRTHDAY_PARAMS = ["Complete Name", "BD_this_year"].freeze
15
+
14
16
  # Implements the logic for shaping the results from a fetcher response.
15
17
  #
16
18
  # <br>
@@ -27,7 +29,7 @@ module Mapper
27
29
  normalized_notion_data = normalize_response(notion_response.results)
28
30
 
29
31
  normalized_notion_data.map do |birthday|
30
- Domain::Birthday.new(birthday["name"], birthday["birth_date"])
32
+ Domain::Birthday.new(birthday["Complete Name"], birthday["BD_this_year"])
31
33
  end
32
34
  end
33
35
 
@@ -36,32 +38,22 @@ module Mapper
36
38
  def normalize_response(results)
37
39
  return [] if results.nil?
38
40
 
39
- normalized_results = []
40
-
41
41
  results.map do |value|
42
- properties = value["properties"]
43
- properties.delete("Name")
42
+ birthday_fields = value["properties"].slice(*BIRTHDAY_PARAMS)
44
43
 
45
- normalized_value = normalize(properties)
44
+ birthday_fields.each do |field, birthday_value|
45
+ birthday_fields[field] = extract_birthday_value(field, birthday_value)
46
+ end
46
47
 
47
- normalized_results.append(normalized_value)
48
+ birthday_fields
48
49
  end
49
-
50
- normalized_results
51
50
  end
52
51
 
53
- def normalize(properties)
54
- normalized_value = {}
55
-
56
- properties.each do |k, v|
57
- if k == "Complete Name"
58
- normalized_value["name"] = extract_rich_text_field_value(v)
59
- elsif k == "BD_this_year"
60
- normalized_value["birth_date"] = extract_date_field_value(v)
61
- end
52
+ def extract_birthday_value(field, value)
53
+ case field
54
+ when "Complete Name" then extract_rich_text_field_value(value)
55
+ when "BD_this_year" then extract_date_field_value(value)
62
56
  end
63
-
64
- normalized_value
65
57
  end
66
58
 
67
59
  def extract_rich_text_field_value(data)
@@ -9,9 +9,11 @@ module Mapper
9
9
  # This class implementats the methods of the Mapper::Base module, specifically designed for preparing or
10
10
  # shaping PTO's data coming from a Fetcher::Base implementation.
11
11
  #
12
- class Pto
12
+ class PtoToday
13
13
  include Base
14
14
 
15
+ PTO_PARAMS = ["Person", "Desde?", "Hasta?"].freeze
16
+
15
17
  # Implements the logic for shaping the results from a fetcher response.
16
18
  #
17
19
  # <br>
@@ -26,8 +28,9 @@ module Mapper
26
28
  return [] if notion_response.results.empty?
27
29
 
28
30
  normalized_notion_data = normalize_response(notion_response.results)
31
+
29
32
  normalized_notion_data.map do |pto|
30
- Domain::Pto.new(pto["name"], format_date(pto["start"]), format_date(pto["end"]))
33
+ Domain::Pto.new(pto["Person"], pto["Desde?"], pto["Hasta?"])
31
34
  end
32
35
  end
33
36
 
@@ -36,39 +39,22 @@ module Mapper
36
39
  def normalize_response(response)
37
40
  return [] if response.nil?
38
41
 
39
- normalized_response = []
40
-
41
42
  response.map do |value|
42
- properties = value["properties"]
43
- properties.delete("Name")
43
+ pto_fields = value["properties"].slice(*PTO_PARAMS)
44
44
 
45
- normalized_value = normalize(properties)
45
+ pto_fields.each do |field, pto_value|
46
+ pto_fields[field] = extract_pto_value(field, pto_value)
47
+ end
46
48
 
47
- normalized_response.append(normalized_value)
49
+ pto_fields
48
50
  end
49
-
50
- normalized_response
51
51
  end
52
52
 
53
- def normalize(properties)
54
- normalized_value = {}
55
-
56
- properties.each do |k, v|
57
- extract_pto_fields(k, v, normalized_value)
58
- end
59
-
60
- normalized_value
61
- end
62
-
63
- def extract_pto_fields(key, value, normalized_value)
64
- case key
65
- when "Person"
66
- user_name = extract_person_field_value(value)
67
- normalized_value["name"] = user_name
68
- when "Desde?"
69
- normalized_value["start"] = extract_date_field_value(value)
70
- when "Hasta?"
71
- normalized_value["end"] = extract_date_field_value(value)
53
+ def extract_pto_value(field, value)
54
+ case field
55
+ when "Person" then extract_person_field_value(value)
56
+ when "Desde?" then extract_date_field_value(value)
57
+ when "Hasta?" then extract_date_field_value(value)
72
58
  end
73
59
  end
74
60
 
@@ -79,18 +65,6 @@ module Mapper
79
65
  def extract_date_field_value(data)
80
66
  data["date"]["start"]
81
67
  end
82
-
83
- def format_date(str_date)
84
- return "" if str_date.nil?
85
-
86
- if str_date.include?("T")
87
- format = "%Y-%m-%d|%I:%M %p"
88
- datetime = Time.new(str_date)
89
- datetime.strftime(format)
90
- else
91
- str_date
92
- end
93
- end
94
68
  end
95
69
  end
96
70
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../domain/work_items_limit"
4
+ require_relative "../base"
5
+
6
+ module Mapper
7
+ module Notion
8
+ ##
9
+ # This class implementats the methods of the Mapper::Base module, specifically designed
10
+ # for preparing or shaping work items data coming from a Fetcher::Base implementation.
11
+ class WorkItemsLimit
12
+ include Base
13
+
14
+ WORK_ITEM_PARAMS = ["Responsible domain"].freeze
15
+
16
+ # Implements the logic for shaping the results from a fetcher response.
17
+ #
18
+ # <br>
19
+ # <b>Params:</b>
20
+ # * <tt>Fetcher::Notion::Types::Response</tt> notion_response: Notion response object.
21
+ #
22
+ # <br>
23
+ # <b>return</b> <tt>List<Domain::WorkItem></tt> work_items_list, mapped work items to be used by a
24
+ # Formatter::Base implementation.
25
+ #
26
+ def map(notion_response)
27
+ return [] if notion_response.results.empty?
28
+
29
+ normalized_notion_data = normalize_response(notion_response.results)
30
+
31
+ domain_items_count = count_domain_items(normalized_notion_data)
32
+
33
+ domain_items_count.map do |domain, items_count|
34
+ Domain::WorkItemsLimit.new(domain, items_count)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def normalize_response(results)
41
+ return [] if results.nil?
42
+
43
+ results.map do |value|
44
+ work_item_fields = value["properties"].slice(*WORK_ITEM_PARAMS)
45
+
46
+ work_item_fields.each do |field, work_item_value|
47
+ work_item_fields[field] = extract_domain_field_value(work_item_value)
48
+ end
49
+
50
+ work_item_fields
51
+ end
52
+ end
53
+
54
+ def extract_domain_field_value(data)
55
+ data["select"]["name"]
56
+ end
57
+
58
+ def count_domain_items(work_items_list)
59
+ domain_work_items = work_items_list.group_by { |work_item| work_item["Responsible domain"] }
60
+
61
+ domain_work_items.transform_values(&:count)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../domain/pto"
4
+ require_relative "../base"
5
+
6
+ module Mapper
7
+ module Postgres
8
+ ##
9
+ # This class implementats the methods of the Mapper::Base module, specifically designed for preparing or
10
+ # shaping PTO's data coming from the Fetcher::Postgres::Pto class.
11
+ #
12
+ class PtoToday
13
+ # Implements the logic for shaping the results from a fetcher response.
14
+ #
15
+ # <br>
16
+ # <b>Params:</b>
17
+ # * <tt>Fetcher::Postgres::Types::Response</tt> pg_response: Postgres response object.
18
+ #
19
+ # <br>
20
+ # <b>returns</b> <tt>List<Domain::Pto></tt> ptos_list, mapped PTO's to be used by a Formatter::Base
21
+ # implementation.
22
+ #
23
+ def map(pg_response)
24
+ return [] if pg_response.records.empty?
25
+
26
+ ptos = build_map(pg_response)
27
+
28
+ ptos.map do |pto|
29
+ name = pto["name"]
30
+ start_date = pto["start_date"]
31
+ end_date = pto["end_date"]
32
+
33
+ Domain::Pto.new(name, start_date, end_date)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def build_map(pg_response)
40
+ fields = pg_response.fields
41
+ values = pg_response.records
42
+
43
+ values.map { |value| Hash[fields.zip(value)] }
44
+ end
45
+ end
46
+ end
47
+ end