From aca539543d69b68299fb15b5c4e37731e6801612 Mon Sep 17 00:00:00 2001 From: Celso Martins Date: Tue, 5 Dec 2023 12:46:01 -0300 Subject: [PATCH] fix: fixed hour value logic --- .../mutations/save_membership_mutation.rb | 1 - .../types/team_member_consolidation_type.rb | 3 +- .../types/team_members_hourly_rate_type.rb | 2 +- app/graphql/types/teams/membership_type.rb | 2 +- app/graphql/types/teams/team_member_type.rb | 2 +- app/models/demand_effort.rb | 33 +- .../membership_available_hours_history.rb | 10 +- app/models/membership.rb | 53 +- .../service_delivery_review_action_item.rb | 2 +- .../slack/slack_notification_service.rb | 2 +- app/services/team_service.rb | 17 +- ...09_change_membership_hours_history_null.rb | 10 + db/structure.sql | 482 +++++++++--------- spec/graphql/types/query_type_spec.rb | 10 +- ...membership_available_hours_history_spec.rb | 25 +- spec/models/membership_spec.rb | 75 ++- spec/services/team_service_spec.rb | 27 +- 17 files changed, 430 insertions(+), 326 deletions(-) create mode 100644 db/migrate/20231205130509_change_membership_hours_history_null.rb diff --git a/app/graphql/mutations/save_membership_mutation.rb b/app/graphql/mutations/save_membership_mutation.rb index c5c07b562..5a5bd5069 100644 --- a/app/graphql/mutations/save_membership_mutation.rb +++ b/app/graphql/mutations/save_membership_mutation.rb @@ -17,7 +17,6 @@ def resolve(membership_id:, member_role:, hours_per_month:, effort_percentage:, membership = Membership.find_by(id: membership_id) if membership.present? - History::MembershipAvailableHoursHistory.create(membership_id: membership_id, available_hours: hours_per_month) membership.update(member_role: member_role, hours_per_month: hours_per_month, effort_percentage: effort_percentage, start_date: start_date, end_date: end_date) { status_message: 'SUCCESS', membership: membership, message: 'Membership updated.' } else diff --git a/app/graphql/types/team_member_consolidation_type.rb b/app/graphql/types/team_member_consolidation_type.rb index 50329d8b7..23158b875 100644 --- a/app/graphql/types/team_member_consolidation_type.rb +++ b/app/graphql/types/team_member_consolidation_type.rb @@ -3,6 +3,7 @@ module Types class TeamMemberConsolidationType < BaseObject field :consolidation_date, GraphQL::Types::ISO8601Date, null: false - field :value_per_hour_performed, Float, null: false + field :hour_value_expected, Float, null: false + field :hour_value_realized, Float, null: false end end diff --git a/app/graphql/types/team_members_hourly_rate_type.rb b/app/graphql/types/team_members_hourly_rate_type.rb index 50e374842..11f1e795f 100644 --- a/app/graphql/types/team_members_hourly_rate_type.rb +++ b/app/graphql/types/team_members_hourly_rate_type.rb @@ -3,6 +3,6 @@ module Types class TeamMembersHourlyRateType < BaseObject field :period_date, GraphQL::Types::ISO8601Date, null: true - field :value_per_hour_performed, Float, null: true + field :hour_value_realized, Float, null: true end end diff --git a/app/graphql/types/teams/membership_type.rb b/app/graphql/types/teams/membership_type.rb index 491f2f494..99f8fed55 100644 --- a/app/graphql/types/teams/membership_type.rb +++ b/app/graphql/types/teams/membership_type.rb @@ -35,7 +35,7 @@ def team_members_hourly_rate_list private def build_hour_rate(date) - { 'value_per_hour_performed' => compute_hours_per_month(object.monthly_payment, object.effort_in_period(Time.zone.today.ago(date.month).beginning_of_month, Time.zone.today.ago(date.month).end_of_month)), 'period_date' => Time.zone.today.ago(date.month).end_of_month } + { 'hour_value_realized' => compute_hours_per_month(object.monthly_payment, object.effort_in_period(Time.zone.today.ago(date.month).beginning_of_month, Time.zone.today.ago(date.month).end_of_month)), 'period_date' => Time.zone.today.ago(date.month).end_of_month } end def compute_hours_per_month(monthly_payment, effort_in_period) diff --git a/app/graphql/types/teams/team_member_type.rb b/app/graphql/types/teams/team_member_type.rb index 4bb706df6..0c2cb52e0 100644 --- a/app/graphql/types/teams/team_member_type.rb +++ b/app/graphql/types/teams/team_member_type.rb @@ -179,7 +179,7 @@ def project_hours_data # TODO: Fix Logic def build_member_value_per_hour(month, membership) - { 'consolidation_date' => month.month.ago.beginning_of_month, 'value_per_hour_performed' => compute_hours_per_month(membership.monthly_payment, membership.demand_efforts.to_dates(month.month.ago.beginning_of_month, month.month.ago.end_of_month).sum(&:effort_value).to_f) } + { 'consolidation_date' => month.month.ago.beginning_of_month, 'hour_value_realized' => compute_hours_per_month(membership.monthly_payment, membership.demand_efforts.to_dates(month.month.ago.beginning_of_month, month.month.ago.end_of_month).sum(&:effort_value).to_f) } end def operations_dashboards diff --git a/app/models/demand_effort.rb b/app/models/demand_effort.rb index 3c8c4263a..90ad42554 100644 --- a/app/models/demand_effort.rb +++ b/app/models/demand_effort.rb @@ -4,22 +4,23 @@ # # Table name: demand_efforts # -# id :bigint not null, primary key -# automatic_update :boolean default(TRUE), not null -# effort_value :decimal(, ) default(0.0), not null -# finish_time_to_computation :datetime not null -# lock_version :integer -# main_effort_in_transition :boolean default(FALSE), not null -# management_percentage :decimal(, ) default(0.0), not null -# pairing_percentage :decimal(, ) default(0.0), not null -# stage_percentage :decimal(, ) default(0.0), not null -# start_time_to_computation :datetime not null -# total_blocked :decimal(, ) default(0.0), not null -# created_at :datetime not null -# updated_at :datetime not null -# demand_id :integer not null -# demand_transition_id :integer not null -# item_assignment_id :integer not null +# id :bigint not null, primary key +# automatic_update :boolean default(TRUE), not null +# effort_value :decimal(, ) default(0.0), not null +# finish_time_to_computation :datetime not null +# lock_version :integer +# main_effort_in_transition :boolean default(FALSE), not null +# management_percentage :decimal(, ) default(0.0), not null +# membership_effort_percentage :decimal(, ) +# pairing_percentage :decimal(, ) default(0.0), not null +# stage_percentage :decimal(, ) default(0.0), not null +# start_time_to_computation :datetime not null +# total_blocked :decimal(, ) default(0.0), not null +# created_at :datetime not null +# updated_at :datetime not null +# demand_id :integer not null +# demand_transition_id :integer not null +# item_assignment_id :integer not null # # Indexes # diff --git a/app/models/history/membership_available_hours_history.rb b/app/models/history/membership_available_hours_history.rb index 1e95ba91c..df0ff7333 100644 --- a/app/models/history/membership_available_hours_history.rb +++ b/app/models/history/membership_available_hours_history.rb @@ -5,8 +5,8 @@ # Table name: membership_available_hours_histories # # id :bigint not null, primary key -# available_hours :integer -# change_date :date +# available_hours :integer not null +# change_date :datetime not null # created_at :datetime not null # updated_at :datetime not null # membership_id :integer not null @@ -23,10 +23,8 @@ module History class MembershipAvailableHoursHistory < ApplicationRecord belongs_to :membership - before_save :update_avaliable_hours + validates :available_hours, :change_date, presence: true - def update_avaliable_hours - self.change_date = Time.zone.now - end + scope :until_date, ->(date) { where('change_date <= :limit_date', limit_date: date) } end end diff --git a/app/models/membership.rb b/app/models/membership.rb index 0b76605b8..3e3abf5d5 100644 --- a/app/models/membership.rb +++ b/app/models/membership.rb @@ -4,15 +4,16 @@ # # Table name: memberships # -# id :bigint not null, primary key -# end_date :date -# hours_per_month :integer -# member_role :integer default("developer"), not null -# start_date :date not null -# created_at :datetime not null -# updated_at :datetime not null -# team_id :integer not null -# team_member_id :integer not null +# id :bigint not null, primary key +# effort_percentage :decimal(, ) +# end_date :date +# hours_per_month :integer +# member_role :integer default("developer"), not null +# start_date :date not null +# created_at :datetime not null +# updated_at :datetime not null +# team_id :integer not null +# team_member_id :integer not null # # Indexes # @@ -36,7 +37,6 @@ class Membership < ApplicationRecord has_many :membership_available_hours_histories, class_name: 'History::MembershipAvailableHoursHistory', dependent: :destroy validates :start_date, :member_role, presence: true - validate :active_team_member_unique scope :active, -> { where('memberships.end_date' => nil) } scope :inactive, -> { where.not('memberships.end_date' => nil) } @@ -48,6 +48,9 @@ class Membership < ApplicationRecord delegate :company, to: :team delegate :projects, to: :team_member + before_create :active_team_member_unique + before_update :save_hours_history + def to_hash { member_name: team_member_name, jira_account_id: team_member.jira_account_id } end @@ -98,17 +101,21 @@ def stages_to_work_on stages_to_work_on end - def expected_hour_value - return 0 if hours_per_month.zero? + def expected_hour_value(date = Time.zone.now) + current_hours_per_month = current_hours_per_month(date) + + return 0 if current_hours_per_month.zero? - monthly_payment / hours_per_month + monthly_payment(date) / current_hours_per_month end - def monthly_payment + def monthly_payment(date = Time.zone.now) return 0 if team_member.monthly_payment.blank? - membership_share = if hours_per_month.present? && team_member.hours_per_month.present? && hours_per_month < team_member.hours_per_month - hours_per_month.to_f / team_member.hours_per_month + current_hours_per_month = current_hours_per_month(date) + + membership_share = if current_hours_per_month.present? && team_member.hours_per_month.present? && current_hours_per_month < team_member.hours_per_month + current_hours_per_month.to_f / team_member.hours_per_month else 1 end @@ -138,6 +145,10 @@ def cards_count(start_date, end_date) demand_efforts.to_dates(start_date, end_date).map(&:demand).uniq.count end + def current_hours_per_month(date = Time.zone.now) + membership_available_hours_histories.until_date(date).order(:change_date).last&.available_hours || hours_per_month + end + private def pairing_members_in_demand(demand) @@ -157,9 +168,17 @@ def pairing_members_in_demand(demand) end def active_team_member_unique + return if end_date.present? + existent_memberships = Membership.where(team: team, team_member: team_member, end_date: nil) - return if existent_memberships == [self] || end_date.present? + return if existent_memberships == [self] errors.add(:team_member, I18n.t('activerecord.errors.models.membership.team_member.already_existent_active')) if existent_memberships.present? end + + def save_hours_history + return if hours_per_month_was == hours_per_month + + History::MembershipAvailableHoursHistory.create(membership_id: id, available_hours: hours_per_month, change_date: Time.zone.now) + end end diff --git a/app/models/service_delivery_review_action_item.rb b/app/models/service_delivery_review_action_item.rb index a9a3fdb62..fb1ed4878 100644 --- a/app/models/service_delivery_review_action_item.rb +++ b/app/models/service_delivery_review_action_item.rb @@ -5,7 +5,7 @@ # Table name: service_delivery_review_action_items # # id :bigint not null, primary key -# action_type :integer default("cadences_change"), not null +# action_type :integer default("technical_change"), not null # deadline :date not null # description :string not null # done_date :date diff --git a/app/services/slack/slack_notification_service.rb b/app/services/slack/slack_notification_service.rb index 73f055b9e..7f51bfab0 100644 --- a/app/services/slack/slack_notification_service.rb +++ b/app/services/slack/slack_notification_service.rb @@ -261,7 +261,7 @@ def notify_team_efficiency(slack_notifier, team, start_date, end_date, title, no effort_text = title members_efforts.each_with_index do |member, index| - effort_text += "• #{medal_of_honor(index)} #{member[:membership].team_member.name} | Demandas: #{member[:cards_count]} | Horas: #{number_with_precision(member[:effort_in_month])} | Capacidade: #{member[:membership][:hours_per_month]} #{notification_period == 'month' ? "| Vl Hr: #{number_with_precision(member[:value_per_hour_performed])}" : ''}\n" + effort_text += "• #{medal_of_honor(index)} #{member[:membership].team_member.name} | Demandas: #{member[:cards_count]} | Horas: #{number_with_precision(member[:effort_in_month])} | Capacidade: #{member[:membership][:hours_per_month]} #{notification_period == 'month' ? "| Vl Hr: #{number_with_precision(member[:hour_value_realized])}" : ''}\n" end effort_info_block = { type: 'section', text: { type: 'mrkdwn', text: effort_text } } diff --git a/app/services/team_service.rb b/app/services/team_service.rb index b06fe71ed..d565f2b4a 100644 --- a/app/services/team_service.rb +++ b/app/services/team_service.rb @@ -67,21 +67,24 @@ def compute_memberships_realized_hours(team, start_date, end_date) memberships = team.memberships.active.billable_member efficiency_data = memberships.map do |membership| - { membership: membership, effort_in_month: membership.effort_in_period(start_date, end_date), - avg_hours_per_demand: membership.avg_hours_per_demand(start_date, end_date), + { membership: membership, effort_in_month: membership.effort_in_period(start_date, end_date).to_f, + avg_hours_per_demand: membership.avg_hours_per_demand(start_date, end_date).to_f, cards_count: membership.cards_count(start_date, end_date), - realized_money_in_month: membership.realized_money_in_period(start_date, end_date), member_capacity_value: membership.hours_per_month || 0, - value_per_hour_performed: calculate_hours_per_month(membership.monthly_payment, membership.effort_in_period(start_date, end_date)) } + realized_money_in_month: membership.realized_money_in_period(start_date, end_date).to_f, member_capacity_value: membership.current_hours_per_month(end_date) || 0, + hour_value_realized: compute_hour_value(membership.monthly_payment(end_date), membership.effort_in_period(start_date, end_date)).to_f, + hour_value_expected: membership.expected_hour_value(end_date).to_f } end - efficiency_data = efficiency_data.sort_by { |member_ef| member_ef[:effort_in_month] }.reverse + efficiency_data = efficiency_data.sort_by { |member_efficiency| member_efficiency[:effort_in_month] }.reverse build_members_efficiency(efficiency_data) end private - def calculate_hours_per_month(sallary, month_hours) - month_hours.zero? ? 0.0 : sallary / month_hours + def compute_hour_value(monthly_payment, hours_per_month) + return 0 if hours_per_month.zero? + + monthly_payment / hours_per_month end def build_members_efficiency(efficiency_data) diff --git a/db/migrate/20231205130509_change_membership_hours_history_null.rb b/db/migrate/20231205130509_change_membership_hours_history_null.rb new file mode 100644 index 000000000..920865350 --- /dev/null +++ b/db/migrate/20231205130509_change_membership_hours_history_null.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class ChangeMembershipHoursHistoryNull < ActiveRecord::Migration[7.1] + def change + change_table :membership_available_hours_histories do |t| + t.change_null :available_hours, false + t.change_null :change_date, false + end + end +end diff --git a/db/structure.sql b/db/structure.sql index becf42706..b3377082f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1370,8 +1370,8 @@ ALTER SEQUENCE public.jira_project_configs_id_seq OWNED BY public.jira_project_c CREATE TABLE public.membership_available_hours_histories ( id bigint NOT NULL, membership_id integer NOT NULL, - available_hours integer, - change_date timestamp(6) without time zone, + available_hours integer NOT NULL, + change_date timestamp(6) without time zone NOT NULL, created_at timestamp(6) without time zone NOT NULL, updated_at timestamp(6) without time zone NOT NULL ); @@ -6660,244 +6660,244 @@ ALTER TABLE ONLY public.stages SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES -('20180111164501'), -('20180111170136'), -('20180111180016'), -('20180111232828'), -('20180111234624'), -('20180112002920'), -('20180112010014'), -('20180112010152'), -('20180112161621'), -('20180112182233'), -('20180113231517'), -('20180115152551'), -('20180116022142'), -('20180116205144'), -('20180116235900'), -('20180117150255'), -('20180122211258'), -('20180123032144'), -('20180126021945'), -('20180126152312'), -('20180126155811'), -('20180126175210'), -('20180127180639'), -('20180128150500'), -('20180128155627'), -('20180203152518'), -('20180204121055'), -('20180204213721'), -('20180206183551'), -('20180207231739'), -('20180208112930'), -('20180209180125'), -('20180209223011'), -('20180213155318'), -('20180215151505'), -('20180215201832'), -('20180216160706'), -('20180216231515'), -('20180221160521'), -('20180223211920'), -('20180224031304'), -('20180224142451'), -('20180302152036'), -('20180302225234'), -('20180303002459'), -('20180306142224'), -('20180307203657'), -('20180312220710'), -('20180313152829'), -('20180315163004'), -('20180316131931'), -('20180316210405'), -('20180320180443'), -('20180331235053'), -('20180403230254'), -('20180407032019'), -('20180410163615'), -('20180411164401'), -('20180412202504'), -('20180417193029'), -('20180510203203'), -('20180514210852'), -('20180516150858'), -('20180529194024'), -('20180530210436'), -('20180604224141'), -('20180615182356'), -('20180618185639'), -('20180619150458'), -('20180620014718'), -('20180627232834'), -('20180703233113'), -('20180731181345'), -('20180820175021'), -('20180822231503'), -('20180830205543'), -('20180915020210'), -('20181008191022'), -('20181022220910'), -('20181210181733'), -('20181210193253'), -('20190108182426'), -('20190121231612'), -('20190124222658'), -('20190211141716'), -('20190212180057'), -('20190212180201'), -('20190212181729'), -('20190212183127'), -('20190215153227'), -('20190216181219'), -('20190318221048'), -('20190323215103'), -('20190402135917'), -('20190403153943'), -('20190403162125'), -('20190423164537'), -('20190430205947'), -('20190430215107'), -('20190501044600'), -('20190507183550'), -('20190507222549'), -('20190517141230'), -('20190525161036'), -('20190527172016'), -('20190527200450'), -('20190531184111'), -('20190531191855'), -('20190531215933'), -('20190603153315'), -('20190606144211'), -('20190606204533'), -('20190607143157'), -('20190611195749'), -('20190612195656'), -('20190613135818'), -('20190613192708'), -('20190614134919'), -('20190621150621'), -('20190621191628'), -('20190624141355'), -('20190701193809'), -('20190701194645'), -('20190704193534'), -('20190705190605'), -('20190708211541'), -('20190709144816'), -('20190711211958'), -('20190716135342'), -('20190719194438'), -('20190723195649'), -('20190730122201'), -('20190805181747'), -('20190806135316'), -('20190807202613'), -('20190812154723'), -('20190815151526'), -('20190816185103'), -('20190821145655'), -('20190830144220'), -('20190905151751'), -('20190905215441'), -('20190906135154'), -('20190917120310'), -('20191002140915'), -('20191015185615'), -('20191021222025'), -('20191024212617'), -('20191025150906'), -('20191028155108'), -('20191223134739'), -('20200114153736'), -('20200114190057'), -('20200130181814'), -('20200328160133'), -('20200330185149'), -('20200406175435'), -('20200423204628'), -('20200423211631'), -('20200430140032'), -('20200504193716'), -('20200507203439'), -('20200511192312'), -('20200520142236'), -('20200528154520'), -('20200601145121'), -('20200615173415'), -('20200627151758'), -('20200703124334'), -('20200707184608'), -('20200711165002'), -('20200714214845'), -('20200716155407'), -('20200716215041'), -('20200717214156'), -('20200721155315'), -('20200807131518'), -('20200812153534'), -('20200813131313'), -('20200831153123'), -('20200928150830'), -('20200929125717'), -('20201019125426'), -('20201020185804'), -('20201111160327'), -('20201209134542'), -('20201214235753'), -('20201215181752'), -('20210105172949'), -('20210107143637'), -('20210418214342'), -('20210430222819'), -('20210513135325'), -('20210518140127'), -('20210519163200'), -('20210913214858'), -('20210920220915'), -('20210927183909'), -('20210927200741'), -('20211011222247'), -('20211110122935'), -('20220113132547'), -('20220113160252'), -('20220113202204'), -('20220113205250'), -('20220113205638'), -('20220114200925'), -('20220115003017'), -('20220120130408'), -('20220125153405'), -('20220127194418'), -('20220128154551'), -('20220128210845'), -('20220131144645'), -('20220202200413'), -('20220214141346'), -('20220221210259'), -('20220311184239'), -('20220401124201'), -('20220408194012'), -('20220503152313'), -('20220503213916'), -('20220509115356'), -('20220512123859'), -('20220602123818'), -('20220622174041'), -('20220705145931'), -('20220711193708'), -('20220714235702'), -('20220718205253'), -('20220718213803'), -('20220804162133'), -('20220914141949'), -('20221130114226'), -('20221205155616'), -('20230131205424'), -('20230518190043'), +('20231205130509'), +('20230920134031'), ('20230919172827'), -('20230920134031'); - +('20230518190043'), +('20230131205424'), +('20221205155616'), +('20221130114226'), +('20220914141949'), +('20220804162133'), +('20220718213803'), +('20220718205253'), +('20220714235702'), +('20220711193708'), +('20220705145931'), +('20220622174041'), +('20220602123818'), +('20220512123859'), +('20220509115356'), +('20220503213916'), +('20220503152313'), +('20220408194012'), +('20220401124201'), +('20220311184239'), +('20220221210259'), +('20220214141346'), +('20220202200413'), +('20220131144645'), +('20220128210845'), +('20220128154551'), +('20220127194418'), +('20220125153405'), +('20220120130408'), +('20220115003017'), +('20220114200925'), +('20220113205638'), +('20220113205250'), +('20220113202204'), +('20220113160252'), +('20220113132547'), +('20211110122935'), +('20211011222247'), +('20210927200741'), +('20210927183909'), +('20210920220915'), +('20210913214858'), +('20210519163200'), +('20210518140127'), +('20210513135325'), +('20210430222819'), +('20210418214342'), +('20210107143637'), +('20210105172949'), +('20201215181752'), +('20201214235753'), +('20201209134542'), +('20201111160327'), +('20201020185804'), +('20201019125426'), +('20200929125717'), +('20200928150830'), +('20200831153123'), +('20200813131313'), +('20200812153534'), +('20200807131518'), +('20200721155315'), +('20200717214156'), +('20200716215041'), +('20200716155407'), +('20200714214845'), +('20200711165002'), +('20200707184608'), +('20200703124334'), +('20200627151758'), +('20200615173415'), +('20200601145121'), +('20200528154520'), +('20200520142236'), +('20200511192312'), +('20200507203439'), +('20200504193716'), +('20200430140032'), +('20200423211631'), +('20200423204628'), +('20200406175435'), +('20200330185149'), +('20200328160133'), +('20200130181814'), +('20200114190057'), +('20200114153736'), +('20191223134739'), +('20191028155108'), +('20191025150906'), +('20191024212617'), +('20191021222025'), +('20191015185615'), +('20191002140915'), +('20190917120310'), +('20190906135154'), +('20190905215441'), +('20190905151751'), +('20190830144220'), +('20190821145655'), +('20190816185103'), +('20190815151526'), +('20190812154723'), +('20190807202613'), +('20190806135316'), +('20190805181747'), +('20190730122201'), +('20190723195649'), +('20190719194438'), +('20190716135342'), +('20190711211958'), +('20190709144816'), +('20190708211541'), +('20190705190605'), +('20190704193534'), +('20190701194645'), +('20190701193809'), +('20190624141355'), +('20190621191628'), +('20190621150621'), +('20190614134919'), +('20190613192708'), +('20190613135818'), +('20190612195656'), +('20190611195749'), +('20190607143157'), +('20190606204533'), +('20190606144211'), +('20190603153315'), +('20190531215933'), +('20190531191855'), +('20190531184111'), +('20190527200450'), +('20190527172016'), +('20190525161036'), +('20190517141230'), +('20190507222549'), +('20190507183550'), +('20190501044600'), +('20190430215107'), +('20190430205947'), +('20190423164537'), +('20190403162125'), +('20190403153943'), +('20190402135917'), +('20190323215103'), +('20190318221048'), +('20190216181219'), +('20190215153227'), +('20190212183127'), +('20190212181729'), +('20190212180201'), +('20190212180057'), +('20190211141716'), +('20190124222658'), +('20190121231612'), +('20190108182426'), +('20181210193253'), +('20181210181733'), +('20181022220910'), +('20181008191022'), +('20180915020210'), +('20180830205543'), +('20180822231503'), +('20180820175021'), +('20180731181345'), +('20180703233113'), +('20180627232834'), +('20180620014718'), +('20180619150458'), +('20180618185639'), +('20180615182356'), +('20180604224141'), +('20180530210436'), +('20180529194024'), +('20180516150858'), +('20180514210852'), +('20180510203203'), +('20180417193029'), +('20180412202504'), +('20180411164401'), +('20180410163615'), +('20180407032019'), +('20180403230254'), +('20180331235053'), +('20180320180443'), +('20180316210405'), +('20180316131931'), +('20180315163004'), +('20180313152829'), +('20180312220710'), +('20180307203657'), +('20180306142224'), +('20180303002459'), +('20180302225234'), +('20180302152036'), +('20180224142451'), +('20180224031304'), +('20180223211920'), +('20180221160521'), +('20180216231515'), +('20180216160706'), +('20180215201832'), +('20180215151505'), +('20180213155318'), +('20180209223011'), +('20180209180125'), +('20180208112930'), +('20180207231739'), +('20180206183551'), +('20180204213721'), +('20180204121055'), +('20180203152518'), +('20180128155627'), +('20180128150500'), +('20180127180639'), +('20180126175210'), +('20180126155811'), +('20180126152312'), +('20180126021945'), +('20180123032144'), +('20180122211258'), +('20180117150255'), +('20180116235900'), +('20180116205144'), +('20180116022142'), +('20180115152551'), +('20180113231517'), +('20180112182233'), +('20180112161621'), +('20180112010152'), +('20180112010014'), +('20180112002920'), +('20180111234624'), +('20180111232828'), +('20180111180016'), +('20180111170136'), +('20180111164501'); diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index cf7a4bcce..86a59c9bd 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -266,7 +266,7 @@ memberRoleDescription teamMembersHourlyRateList{ periodDate - valuePerHourPerformed + hourValueRealized } } lastReplenishingConsolidations { @@ -361,8 +361,8 @@ 'teamConsolidationsWeekly' => [], 'teamMonthlyInvestment' => { 'xAxis' => ['2022-09-30'], 'yAxis' => [-4500.0] }, 'teamMemberEfficiency' => { 'membersEfficiency' => [{ 'effortInMonth' => 0.0, 'membership' => { 'teamMemberName' => 'aaa' }, 'realizedMoneyInMonth' => 0.0 }, { 'effortInMonth' => 0.0, 'membership' => { 'teamMemberName' => 'ddd' }, 'realizedMoneyInMonth' => 0.0 }] }, - 'memberships' => [{ 'id' => other_membership.id.to_s, 'memberRoleDescription' => 'Cliente', 'teamMembersHourlyRateList' => [{ 'periodDate' => '2022-02-28', 'valuePerHourPerformed' => 2000.0 }, { 'periodDate' => '2022-03-31', 'valuePerHourPerformed' => 2000.0 }, { 'periodDate' => '2022-04-30', 'valuePerHourPerformed' => 2000.0 }, { 'periodDate' => '2022-05-31', 'valuePerHourPerformed' => 2000.0 }, { 'periodDate' => '2022-06-30', 'valuePerHourPerformed' => 2000.0 }, { 'periodDate' => '2022-07-31', 'valuePerHourPerformed' => 2000.0 }, { 'periodDate' => '2022-08-31', 'valuePerHourPerformed' => 2000.0 }] }, - { 'id' => membership.id.to_s, 'memberRoleDescription' => 'Desenvolvedor', 'teamMembersHourlyRateList' => [{ 'periodDate' => '2022-02-28', 'valuePerHourPerformed' => 2500.0 }, { 'periodDate' => '2022-03-31', 'valuePerHourPerformed' => 2500.0 }, { 'periodDate' => '2022-04-30', 'valuePerHourPerformed' => 2500.0 }, { 'periodDate' => '2022-05-31', 'valuePerHourPerformed' => 2500.0 }, { 'periodDate' => '2022-06-30', 'valuePerHourPerformed' => 2500.0 }, { 'periodDate' => '2022-07-31', 'valuePerHourPerformed' => 20.83 }, { 'periodDate' => '2022-08-31', 'valuePerHourPerformed' => 25.0 }] }], + 'memberships' => [{ 'id' => other_membership.id.to_s, 'memberRoleDescription' => 'Cliente', 'teamMembersHourlyRateList' => [{ 'periodDate' => '2022-02-28', 'hourValueRealized' => 2000.0 }, { 'periodDate' => '2022-03-31', 'hourValueRealized' => 2000.0 }, { 'periodDate' => '2022-04-30', 'hourValueRealized' => 2000.0 }, { 'periodDate' => '2022-05-31', 'hourValueRealized' => 2000.0 }, { 'periodDate' => '2022-06-30', 'hourValueRealized' => 2000.0 }, { 'periodDate' => '2022-07-31', 'hourValueRealized' => 2000.0 }, { 'periodDate' => '2022-08-31', 'hourValueRealized' => 2000.0 }] }, + { 'id' => membership.id.to_s, 'memberRoleDescription' => 'Desenvolvedor', 'teamMembersHourlyRateList' => [{ 'periodDate' => '2022-02-28', 'hourValueRealized' => 2500.0 }, { 'periodDate' => '2022-03-31', 'hourValueRealized' => 2500.0 }, { 'periodDate' => '2022-04-30', 'hourValueRealized' => 2500.0 }, { 'periodDate' => '2022-05-31', 'hourValueRealized' => 2500.0 }, { 'periodDate' => '2022-06-30', 'hourValueRealized' => 2500.0 }, { 'periodDate' => '2022-07-31', 'hourValueRealized' => 20.83 }, { 'periodDate' => '2022-08-31', 'hourValueRealized' => 25.0 }] }], 'lastReplenishingConsolidations' => [ { 'id' => replenishing_consolidation.id.to_s, @@ -1906,7 +1906,7 @@ } teamMemberConsolidationList { consolidationDate - valuePerHourPerformed + hourValueRealized } memberThroughputData(numberOfWeeks: 3) } @@ -2056,7 +2056,7 @@ 'yAxisHours' => [170.0], 'yAxisProjectsNames' => [project.name] }, - 'teamMemberConsolidationList' => [{ 'consolidationDate' => '2021-04-01', 'valuePerHourPerformed' => 1000.0 }, { 'consolidationDate' => '2021-05-01', 'valuePerHourPerformed' => 1000.0 }, { 'consolidationDate' => '2021-06-01', 'valuePerHourPerformed' => 1000.0 }, { 'consolidationDate' => '2021-07-01', 'valuePerHourPerformed' => 1000.0 }, { 'consolidationDate' => '2021-08-01', 'valuePerHourPerformed' => 1000.0 }, { 'consolidationDate' => '2021-09-01', 'valuePerHourPerformed' => 1000.0 }, { 'consolidationDate' => '2021-10-01', 'valuePerHourPerformed' => 1000.0 }, { 'consolidationDate' => '2021-11-01', 'valuePerHourPerformed' => 1000.0 }, { 'consolidationDate' => '2021-12-01', 'valuePerHourPerformed' => 1000.0 }, { 'consolidationDate' => '2022-01-01', 'valuePerHourPerformed' => 1000.0 }, { 'consolidationDate' => '2022-02-01', 'valuePerHourPerformed' => 1000.0 }, { 'consolidationDate' => '2022-03-01', 'valuePerHourPerformed' => 10.0 }, { 'consolidationDate' => '2022-04-01', 'valuePerHourPerformed' => 10.0 }], + 'teamMemberConsolidationList' => [{ 'consolidationDate' => '2021-04-01', 'hourValueRealized' => 1000.0 }, { 'consolidationDate' => '2021-05-01', 'hourValueRealized' => 1000.0 }, { 'consolidationDate' => '2021-06-01', 'hourValueRealized' => 1000.0 }, { 'consolidationDate' => '2021-07-01', 'hourValueRealized' => 1000.0 }, { 'consolidationDate' => '2021-08-01', 'hourValueRealized' => 1000.0 }, { 'consolidationDate' => '2021-09-01', 'hourValueRealized' => 1000.0 }, { 'consolidationDate' => '2021-10-01', 'hourValueRealized' => 1000.0 }, { 'consolidationDate' => '2021-11-01', 'hourValueRealized' => 1000.0 }, { 'consolidationDate' => '2021-12-01', 'hourValueRealized' => 1000.0 }, { 'consolidationDate' => '2022-01-01', 'hourValueRealized' => 1000.0 }, { 'consolidationDate' => '2022-02-01', 'hourValueRealized' => 1000.0 }, { 'consolidationDate' => '2022-03-01', 'hourValueRealized' => 10.0 }, { 'consolidationDate' => '2022-04-01', 'hourValueRealized' => 10.0 }], 'memberThroughputData' => [0, 0, 0, 2], 'demandEfforts' => [{ 'finishTimeToComputation' => '2022-05-03T10:00:00-03:00' diff --git a/spec/models/history/membership_available_hours_history_spec.rb b/spec/models/history/membership_available_hours_history_spec.rb index c3715563d..ae51c17f0 100644 --- a/spec/models/history/membership_available_hours_history_spec.rb +++ b/spec/models/history/membership_available_hours_history_spec.rb @@ -1,23 +1,24 @@ # frozen_string_literal: true RSpec.describe History::MembershipAvailableHoursHistory do - describe 'associations' do + context 'for associations' do it { is_expected.to belong_to(:membership) } end - describe 'callbacks' do - describe 'before_save' do - it 'updates the change_date to the current time' do - travel_to Time.zone.local(2023, 1, 30, 10, 0, 0) do - team = Fabricate :team - team_member = Fabricate :team_member - membership = Fabricate :membership, team: team, team_member: team_member, end_date: nil - membership_available_hours_history = Fabricate :membership_available_hours_history, membership: membership + context 'for validations' do + it { is_expected.to validate_presence_of :available_hours } + it { is_expected.to validate_presence_of :change_date } + end - membership_available_hours_history.save + context 'for scopes' do + describe '.until_date' do + it 'returns the histories until date' do + first_history = Fabricate :membership_available_hours_history, change_date: 2.months.ago + second_history = Fabricate :membership_available_hours_history, change_date: 3.months.ago + third_history = Fabricate :membership_available_hours_history, change_date: 4.months.ago + Fabricate :membership_available_hours_history, change_date: 15.days.ago - expect(membership_available_hours_history.change_date).to eq Time.zone.now - end + expect(described_class.until_date(1.month.ago)).to contain_exactly(first_history, second_history, third_history) end end end diff --git a/spec/models/membership_spec.rb b/spec/models/membership_spec.rb index 4c9ed3948..3f3808a5c 100644 --- a/spec/models/membership_spec.rb +++ b/spec/models/membership_spec.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true RSpec.describe Membership do - context 'enums' do + context 'for enums' do it { is_expected.to define_enum_for(:member_role).with_values(developer: 0, manager: 1, client: 2, designer: 3) } end - context 'associations' do + context 'for associations' do it { is_expected.to belong_to :team } it { is_expected.to belong_to :team_member } it { is_expected.to have_many(:item_assignments).dependent(:destroy) } @@ -13,7 +13,7 @@ it { is_expected.to have_many(:membership_available_hours_histories).dependent(:destroy) } end - context 'validations' do + context 'for validations' do it { is_expected.to validate_presence_of :start_date } context 'unique active membership for team member' do @@ -38,7 +38,7 @@ end end - context 'scopes' do + context 'for scopes' do let!(:active) { Fabricate :membership, end_date: nil } let!(:other_active) { Fabricate :membership, end_date: nil } let!(:inactive) { Fabricate :membership, end_date: Time.zone.today } @@ -56,13 +56,35 @@ end end - context 'delegations' do + context 'for delegations' do it { is_expected.to delegate_method(:name).to(:team_member).with_prefix } it { is_expected.to delegate_method(:jira_account_id).to(:team_member) } it { is_expected.to delegate_method(:company).to(:team) } it { is_expected.to delegate_method(:projects).to(:team_member) } end + context 'for callbacks' do + describe '#save_hours_history' do + context 'if the hours changed' do + it 'saves the history' do + membership = Fabricate :membership, hours_per_month: 100 + + membership.update(hours_per_month: 110, start_date: 2.days.ago) + expect(History::MembershipAvailableHoursHistory.all.map(&:available_hours)).to eq [110] + end + end + + context 'if the hours did not change' do + it 'does not save the history' do + membership = Fabricate :membership, hours_per_month: 100 + + membership.update(hours_per_month: 100, start_date: 2.days.ago) + expect(History::MembershipAvailableHoursHistory.count).to eq 0 + end + end + end + end + describe '#hours_per_day' do let(:team_membership) { Fabricate :membership, hours_per_month: 60 } let(:other_membership) { Fabricate :membership, hours_per_month: nil } @@ -415,5 +437,48 @@ expect(membership.expected_hour_value).to eq 0 end end + + context 'with histories' do + it 'returns based on the history' do + team_member = Fabricate :team_member, hours_per_month: 128, monthly_payment: 10_000 + membership = Fabricate :membership, team_member: team_member, hours_per_month: 200 + Fabricate :membership_available_hours_history, membership: membership, change_date: 3.months.ago, available_hours: 160 + Fabricate :membership_available_hours_history, membership: membership, change_date: 2.months.ago, available_hours: 90 + + expect(membership.expected_hour_value(1.month.ago)).to eq 78.125 + end + end + end + + describe '#current_hours_per_month' do + context 'without histories' do + it 'returns the value in the membership' do + membership = Fabricate :membership, hours_per_month: 100 + + expect(membership.current_hours_per_month).to eq 100 + end + end + + context 'with histories' do + context 'without date' do + it 'returns the value in the last history' do + membership = Fabricate :membership, hours_per_month: 100 + Fabricate :membership_available_hours_history, membership: membership, change_date: 3.months.ago, available_hours: 160 + Fabricate :membership_available_hours_history, membership: membership, change_date: 2.months.ago, available_hours: 90 + + expect(membership.current_hours_per_month).to eq 90 + end + end + + context 'with date' do + it 'returns the value in the last history' do + membership = Fabricate :membership, hours_per_month: 100 + Fabricate :membership_available_hours_history, membership: membership, change_date: 3.months.ago, available_hours: 160 + Fabricate :membership_available_hours_history, membership: membership, change_date: 2.months.ago, available_hours: 90 + + expect(membership.current_hours_per_month(65.days.ago)).to eq 160 + end + end + end end end diff --git a/spec/services/team_service_spec.rb b/spec/services/team_service_spec.rb index 11ee85086..96c783b1b 100644 --- a/spec/services/team_service_spec.rb +++ b/spec/services/team_service_spec.rb @@ -91,23 +91,30 @@ context 'with data' do it 'returns the average demand cost informations in a hash' do - team_member = Fabricate :team_member, billable_type: :outsourcing, monthly_payment: 10_000, start_date: 1.month.ago, end_date: nil - other_team_member = Fabricate :team_member, billable_type: :outsourcing, monthly_payment: 10_000, start_date: 2.months.ago, end_date: nil - inactive_team_member = Fabricate :team_member, billable_type: :outsourcing, monthly_payment: 10_000, start_date: 2.months.ago, end_date: 1.month.ago + project = Fabricate :project, hour_value: 200 + team_member = Fabricate :team_member, billable_type: :outsourcing, hours_per_month: 120, monthly_payment: 10_000, start_date: 1.month.ago, end_date: nil + other_team_member = Fabricate :team_member, billable_type: :outsourcing, hours_per_month: 120, monthly_payment: 10_000, start_date: 2.months.ago, end_date: nil + inactive_team_member = Fabricate :team_member, billable_type: :outsourcing, hours_per_month: 120, monthly_payment: 10_000, start_date: 2.months.ago, end_date: 1.month.ago membership = Fabricate :membership, team: team, team_member: team_member, hours_per_month: 120, start_date: 1.month.ago, end_date: nil other_membership = Fabricate :membership, team: team, team_member: other_team_member, hours_per_month: 40, start_date: 2.months.ago, end_date: nil Fabricate :membership, team: team, team_member: inactive_team_member, hours_per_month: 40, start_date: 2.months.ago, end_date: 1.day.ago - Fabricate :demand, team: team, end_date: 1.week.ago - Fabricate :demand, team: team, end_date: 1.week.ago + demand = Fabricate :demand, team: team, project: project, end_date: 1.week.ago + other_demand = Fabricate :demand, team: team, project: project, end_date: 1.week.ago Fabricate :demand, team: team, end_date: Time.zone.now + assignment = Fabricate :item_assignment, membership: membership, demand: demand + other_assignment = Fabricate :item_assignment, membership: membership, demand: other_demand + Fabricate :demand_effort, demand: demand, item_assignment: assignment, start_time_to_computation: 1.day.ago, finish_time_to_computation: Time.zone.now, effort_value: 100 + Fabricate :demand_effort, demand: other_demand, item_assignment: other_assignment, start_time_to_computation: 1.day.ago, finish_time_to_computation: Time.zone.now, effort_value: 100 + start_date = Time.zone.today.beginning_of_month end_date = Time.zone.today.end_of_month - expect(described_class.instance.compute_memberships_realized_hours(team, start_date, end_date)[:members_efficiency]).to contain_exactly({ membership: membership, avg_hours_per_demand: 0, cards_count: 0, effort_in_month: 0, realized_money_in_month: 0, member_capacity_value: 120, value_per_hour_performed: 0.0 }, { membership: other_membership, avg_hours_per_demand: 0, cards_count: 0, effort_in_month: 0, realized_money_in_month: 0, member_capacity_value: 40, value_per_hour_performed: 0.0 }) - expect(described_class.instance.compute_memberships_realized_hours(team, start_date, end_date)[:total_hours_produced]).to eq 0 - expect(described_class.instance.compute_memberships_realized_hours(team, start_date, end_date)[:total_money_produced]).to eq 0 - expect(described_class.instance.compute_memberships_realized_hours(team, start_date, end_date)[:avg_hours_per_member]).to eq 0 - expect(described_class.instance.compute_memberships_realized_hours(team, start_date, end_date)[:avg_money_per_member]).to eq 0 + expect(described_class.instance.compute_memberships_realized_hours(team, start_date, end_date)[:members_efficiency][0]).to eq({ membership: membership, avg_hours_per_demand: 100.0, cards_count: 2, effort_in_month: 200.0, realized_money_in_month: 40_000.0, member_capacity_value: 120, hour_value_realized: 50.0, hour_value_expected: 83.333333333333325 }) + expect(described_class.instance.compute_memberships_realized_hours(team, start_date, end_date)[:members_efficiency][1]).to eq({ membership: other_membership, avg_hours_per_demand: 0, cards_count: 0, effort_in_month: 0, realized_money_in_month: 0, member_capacity_value: 40, hour_value_realized: 0, hour_value_expected: 83.333333333333333333333333333333333333 }) + expect(described_class.instance.compute_memberships_realized_hours(team, start_date, end_date)[:total_hours_produced]).to eq 200 + expect(described_class.instance.compute_memberships_realized_hours(team, start_date, end_date)[:total_money_produced]).to eq 40_000 + expect(described_class.instance.compute_memberships_realized_hours(team, start_date, end_date)[:avg_hours_per_member]).to eq 100 + expect(described_class.instance.compute_memberships_realized_hours(team, start_date, end_date)[:avg_money_per_member]).to eq 20_000 expect(described_class.instance.compute_memberships_realized_hours(team, start_date, end_date)[:team_capacity_hours]).to eq 160 end end