From 1e82309dcb8c2e4f772d949a0c1128fcf9134141 Mon Sep 17 00:00:00 2001 From: Celso Martins Date: Tue, 5 Dec 2023 21:40:42 -0300 Subject: [PATCH] fix: fixed team hour value realized chart logic --- app/controllers/demand_blocks_controller.rb | 1 - .../team_member_consolidation_list_type.rb | 7 - .../types/team_member_consolidation_type.rb | 9 - .../team_members_hourly_rate_list_type.rb | 7 - .../types/team_members_hourly_rate_type.rb | 8 - .../teams/member_efficiency_data_type.rb | 2 + .../teams/member_efficiency_list_type.rb | 1 + .../membership_hour_value_chart_data_type.rb | 11 + .../membership_hour_value_chart_list_type.rb | 10 + app/graphql/types/teams/membership_type.rb | 23 -- app/graphql/types/teams/team_member_type.rb | 25 +- app/graphql/types/teams/team_type.rb | 29 +- app/models/membership.rb | 10 +- .../slack/slack_notification_service.rb | 6 +- app/services/team_service.rb | 12 +- app/spa/src/modules/team/team.types.ts | 22 +- .../modules/teamMember/teamMember.types.ts | 6 - .../pages/TeamMembers/TeamMemberDashboard.tsx | 4 - .../src/pages/Teams/MemberEfficiencyTable.tsx | 2 + app/spa/src/pages/Teams/TeamDashboard.tsx | 46 ++- .../demand_blocks_controller_spec.rb | 331 +++++++++++++++--- spec/graphql/types/query_type_spec.rb | 36 +- spec/models/membership_spec.rb | 71 +++- spec/models/portfolio_unit_spec.rb | 22 +- spec/models/project_spec.rb | 13 +- 25 files changed, 490 insertions(+), 224 deletions(-) delete mode 100644 app/graphql/types/team_member_consolidation_list_type.rb delete mode 100644 app/graphql/types/team_member_consolidation_type.rb delete mode 100644 app/graphql/types/team_members_hourly_rate_list_type.rb delete mode 100644 app/graphql/types/team_members_hourly_rate_type.rb create mode 100644 app/graphql/types/teams/membership_hour_value_chart_data_type.rb create mode 100644 app/graphql/types/teams/membership_hour_value_chart_list_type.rb diff --git a/app/controllers/demand_blocks_controller.rb b/app/controllers/demand_blocks_controller.rb index 3433ab2c8..67aed4349 100644 --- a/app/controllers/demand_blocks_controller.rb +++ b/app/controllers/demand_blocks_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class DemandBlocksController < AuthenticatedController - before_action :user_gold_check before_action :assign_project, except: %i[index demand_blocks_csv search] before_action :assign_demand, except: %i[index demand_blocks_csv search] before_action :assign_demand_block, except: %i[index demand_blocks_csv search] diff --git a/app/graphql/types/team_member_consolidation_list_type.rb b/app/graphql/types/team_member_consolidation_list_type.rb deleted file mode 100644 index a9814bde7..000000000 --- a/app/graphql/types/team_member_consolidation_list_type.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module Types - class TeamMemberConsolidationListType < BaseObject - field :team_member_consolidations, [Types::TeamMemberConsolidationType], null: true - end -end diff --git a/app/graphql/types/team_member_consolidation_type.rb b/app/graphql/types/team_member_consolidation_type.rb deleted file mode 100644 index 23158b875..000000000 --- a/app/graphql/types/team_member_consolidation_type.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module Types - class TeamMemberConsolidationType < BaseObject - field :consolidation_date, GraphQL::Types::ISO8601Date, 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_list_type.rb b/app/graphql/types/team_members_hourly_rate_list_type.rb deleted file mode 100644 index af8f8ec6f..000000000 --- a/app/graphql/types/team_members_hourly_rate_list_type.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module Types - class TeamMembersHourlyRateListType < BaseObject - field :team_members_hourly_rate, [Types::TeamMembersHourlyRateType], null: true - end -end diff --git a/app/graphql/types/team_members_hourly_rate_type.rb b/app/graphql/types/team_members_hourly_rate_type.rb deleted file mode 100644 index 11f1e795f..000000000 --- a/app/graphql/types/team_members_hourly_rate_type.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -module Types - class TeamMembersHourlyRateType < BaseObject - field :period_date, GraphQL::Types::ISO8601Date, null: true - field :hour_value_realized, Float, null: true - end -end diff --git a/app/graphql/types/teams/member_efficiency_data_type.rb b/app/graphql/types/teams/member_efficiency_data_type.rb index 40dfe6342..473672d38 100644 --- a/app/graphql/types/teams/member_efficiency_data_type.rb +++ b/app/graphql/types/teams/member_efficiency_data_type.rb @@ -6,6 +6,8 @@ class MemberEfficiencyDataType < Types::BaseObject field :avg_hours_per_demand, Float field :cards_count, Int field :effort_in_month, Float + field :hour_value_expected, Float + field :hour_value_realized, Float field :member_capacity_value, Int field :membership, Types::Teams::MembershipType field :realized_money_in_month, Float diff --git a/app/graphql/types/teams/member_efficiency_list_type.rb b/app/graphql/types/teams/member_efficiency_list_type.rb index 35c47bb26..ab657b4a9 100644 --- a/app/graphql/types/teams/member_efficiency_list_type.rb +++ b/app/graphql/types/teams/member_efficiency_list_type.rb @@ -5,6 +5,7 @@ module Teams class MemberEfficiencyListType < Types::BaseObject field :avg_hours_per_member, Float, null: false field :avg_money_per_member, Float, null: false + field :date, GraphQL::Types::ISO8601Date, null: false field :members_efficiency, [Types::Teams::MemberEfficiencyDataType], null: false field :team_capacity_hours, Integer, null: false field :total_hours_produced, Float, null: false diff --git a/app/graphql/types/teams/membership_hour_value_chart_data_type.rb b/app/graphql/types/teams/membership_hour_value_chart_data_type.rb new file mode 100644 index 000000000..c0c5c6d91 --- /dev/null +++ b/app/graphql/types/teams/membership_hour_value_chart_data_type.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module Teams + class MembershipHourValueChartDataType < Types::BaseObject + field :date, GraphQL::Types::ISO8601Date + field :hour_value_expected, Float + field :hour_value_realized, Float + end + end +end diff --git a/app/graphql/types/teams/membership_hour_value_chart_list_type.rb b/app/graphql/types/teams/membership_hour_value_chart_list_type.rb new file mode 100644 index 000000000..3fe2798b4 --- /dev/null +++ b/app/graphql/types/teams/membership_hour_value_chart_list_type.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + module Teams + class MembershipHourValueChartListType < Types::BaseObject + field :membership, Types::Teams::MembershipType + field :membership_hour_value_chart_data, [Types::Teams::MembershipHourValueChartDataType] + end + end +end diff --git a/app/graphql/types/teams/membership_type.rb b/app/graphql/types/teams/membership_type.rb index 99f8fed55..a60b21dbb 100644 --- a/app/graphql/types/teams/membership_type.rb +++ b/app/graphql/types/teams/membership_type.rb @@ -16,34 +16,11 @@ class MembershipType < Types::BaseObject field :team_id, Integer, null: false field :team_member_id, Integer, null: false field :team_member_name, String, null: false - field :team_members_hourly_rate_list, [Types::TeamMembersHourlyRateType], null: true field :updated_at, GraphQL::Types::ISO8601DateTime, null: false def member_role_description I18n.t("activerecord.attributes.membership.enums.member_role.#{object.member_role}") end - - # TODO: fix logic - def team_members_hourly_rate_list - return unless object.monthly_payment != 0 && object.team_member.billable? - - hourly_rate = [] - (1..7).reverse_each { |date| hourly_rate << build_hour_rate(date) } - hourly_rate - end - - private - - def build_hour_rate(date) - { '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) - return monthly_payment if effort_in_period.zero? - - result = monthly_payment / effort_in_period - result.round(2) - end end end end diff --git a/app/graphql/types/teams/team_member_type.rb b/app/graphql/types/teams/team_member_type.rb index 0c2cb52e0..ede401d33 100644 --- a/app/graphql/types/teams/team_member_type.rb +++ b/app/graphql/types/teams/team_member_type.rb @@ -52,8 +52,6 @@ class TeamMemberType < Types::BaseObject argument :sort_direction, Types::Enums::SortDirection, required: false end - field :team_member_consolidation_list, [Types::TeamMemberConsolidationType], null: true - field :average_pull_interval_data, Types::Charts::SimpleDateChartDataType, null: true field :lead_time_control_chart_data, Types::Charts::ControlChartType, null: true field :lead_time_histogram_chart_data, Types::Charts::LeadTimeHistogramDataType, null: true @@ -85,15 +83,6 @@ def demands(status: 'ALL', type: 'ALL', limit: nil) demands.limit(limit) end - # TODO: Fix logic - def team_member_consolidation_list - membership = object - members_value_per_hour = [] - (1..13).reverse_each { |month| members_value_per_hour << build_member_value_per_hour(month, membership) } - - members_value_per_hour - end - def demand_efforts_list(from_date: nil, until_date: nil, page_number: nil) efforts = object.demand_efforts.to_dates(from_date, until_date).order(start_time_to_computation: :desc) efforts_paginated = efforts.page(page_number).per(10) @@ -177,11 +166,6 @@ def project_hours_data private - # TODO: Fix Logic - def build_member_value_per_hour(month, membership) - { '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 @operations_dashboards ||= Dashboards::OperationsDashboard.where( team_member: object, @@ -189,8 +173,13 @@ def operations_dashboards ).where('operations_dashboards.dashboard_date > :limit_date', limit_date: 6.months.ago.beginning_of_day).order(:dashboard_date) end - def member_effort_data_interval = 6.months.ago.at_beginning_of_month.beginning_of_day - def member_effort_daily_interval = 30.days.ago.beginning_of_day + def member_effort_data_interval + 6.months.ago.at_beginning_of_month.beginning_of_day + end + + def member_effort_daily_interval + 30.days.ago.beginning_of_day + end def first_day_of_six_months_hash accumulator = Hash.new { |hash, key| hash[key] = 0 } diff --git a/app/graphql/types/teams/team_type.rb b/app/graphql/types/teams/team_type.rb index 26eacdf4b..c69c0d9ac 100644 --- a/app/graphql/types/teams/team_type.rb +++ b/app/graphql/types/teams/team_type.rb @@ -36,10 +36,13 @@ class TeamType < Types::BaseObject field :lead_time_p80, Float, null: true field :lead_time_p95, Float, null: true field :max_work_in_progress, Int, null: false + field :membership_hour_value_chart_list_type, [Types::Teams::MembershipHourValueChartListType] do + argument :end_date, GraphQL::Types::ISO8601Date, required: false + argument :start_date, GraphQL::Types::ISO8601Date, required: false + end field :memberships, [Types::Teams::MembershipType] do argument :active, Boolean, required: false end - field :name, String, null: false field :number_of_demands_delivered, Int, null: true field :projects, [Types::ProjectType], null: true @@ -59,8 +62,6 @@ class TeamType < Types::BaseObject field :throughput_data, [Int], null: true field :work_in_progress, Int, null: true - field :team_members_hourly_rate_list, [Types::TeamMembersHourlyRateType], null: true - delegate :projects, to: :object def latest_deliveries(order_field: 'end_date', sort_direction: :desc, limit: 5, start_date: '') @@ -144,7 +145,7 @@ def team_consolidations_weekly(start_date: 6.months.ago, end_date: Time.zone.tod weekly_team_consolidations = object.team_consolidations.weekly_data.order(:consolidation_date) consolidations = Consolidations::TeamConsolidation - .where(id: weekly_team_consolidations.map(&:id) + [last_consolidation&.id]) + .where(id: weekly_team_consolidations.map(&:id) + [last_consolidation&.id]) consolidations = consolidations.where('consolidation_date >= :limit_date', limit_date: start_date) if start_date.present? consolidations = consolidations.where('consolidation_date <= :limit_date', limit_date: end_date) if end_date.present? consolidations.order(:consolidation_date) @@ -159,10 +160,7 @@ def team_monthly_investment(start_date: 6.months.ago, end_date: Time.zone.today) array_of_months.each do |date| total_cost_per_week.append(object.realized_money_in_month(date).round(2) - object.monthly_investment(date).round(2)) end - { - x_axis: array_of_months, - y_axis: total_cost_per_week - } + { x_axis: array_of_months, y_axis: total_cost_per_week } end def team_member_efficiency(month: Time.zone.today.month, year: Time.zone.today.year) @@ -172,6 +170,21 @@ def team_member_efficiency(month: Time.zone.today.month, year: Time.zone.today.y TeamService.instance.compute_memberships_realized_hours(object, start_date, end_date) end + def membership_hour_value_chart_list_type(start_date: 6.months.ago, end_date: Time.zone.now) + months = TimeService.instance.months_between_of(start_date, end_date) + + memberships_hour_value_list = [] + object.memberships.active.billable_member.each do |membership| + membership_hour_value_chart_data = [] + months.each do |month| + membership_hour_value_chart_data.push({ date: month, hour_value_expected: membership.expected_hour_value(month), hour_value_realized: membership.realized_hour_value(month) }) + end + memberships_hour_value_list.push({ membership: membership, membership_hour_value_chart_data: membership_hour_value_chart_data }) + end + + memberships_hour_value_list + end + def memberships(active: nil) membership = object.memberships membership = active ? membership.active : membership.inactive diff --git a/app/models/membership.rb b/app/models/membership.rb index 3e3abf5d5..b2c94cd85 100644 --- a/app/models/membership.rb +++ b/app/models/membership.rb @@ -37,6 +37,7 @@ 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,7 +49,6 @@ 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 @@ -109,6 +109,14 @@ def expected_hour_value(date = Time.zone.now) monthly_payment(date) / current_hours_per_month end + def realized_hour_value(date = Time.zone.now) + realized_effort = effort_in_period(date.beginning_of_month, date.end_of_month) + + return 0 if realized_effort.zero? + + monthly_payment(date) / realized_effort + end + def monthly_payment(date = Time.zone.now) return 0 if team_member.monthly_payment.blank? diff --git a/app/services/slack/slack_notification_service.rb b/app/services/slack/slack_notification_service.rb index 7f51bfab0..d056b32a4 100644 --- a/app/services/slack/slack_notification_service.rb +++ b/app/services/slack/slack_notification_service.rb @@ -251,10 +251,10 @@ def notify_item_blocked(demand_block, demand_url, edit_block_url, block_state = end def notify_team_efficiency(slack_notifier, team, start_date, end_date, title, notification_period) - average_team_efficiency = TeamService.instance.compute_memberships_realized_hours(team, start_date, end_date) - return if average_team_efficiency.blank? + efficiency_data = TeamService.instance.compute_memberships_realized_hours(team, start_date, end_date) + return if efficiency_data.blank? - members_efforts = average_team_efficiency[:members_efficiency].reject { |member_effort| member_effort.try(:[], :membership).try(:[], :hours_per_month).blank? && member_effort.try(:[], :membership).try(:[], :hours_per_month)&.zero? } + members_efforts = efficiency_data[:members_efficiency].reject { |member_effort| member_effort.try(:[], :membership).try(:[], :hours_per_month).blank? && member_effort.try(:[], :membership).try(:[], :hours_per_month)&.zero? } return if members_efforts.blank? diff --git a/app/services/team_service.rb b/app/services/team_service.rb index d565f2b4a..d61e261dd 100644 --- a/app/services/team_service.rb +++ b/app/services/team_service.rb @@ -70,10 +70,12 @@ def compute_memberships_realized_hours(team, 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).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, + 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: membership.realized_hour_value(end_date).to_f, hour_value_expected: membership.expected_hour_value(end_date).to_f } end + efficiency_data = efficiency_data.sort_by { |member_efficiency| member_efficiency[:effort_in_month] }.reverse build_members_efficiency(efficiency_data) @@ -81,12 +83,6 @@ def compute_memberships_realized_hours(team, start_date, end_date) private - 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) total_hours_produced = efficiency_data.pluck(:effort_in_month).sum avg_hours_per_member = efficiency_data.count.positive? ? total_hours_produced / efficiency_data.count : 0 diff --git a/app/spa/src/modules/team/team.types.ts b/app/spa/src/modules/team/team.types.ts index 795bd87cb..05a6192cb 100644 --- a/app/spa/src/modules/team/team.types.ts +++ b/app/spa/src/modules/team/team.types.ts @@ -9,11 +9,6 @@ import { Demand } from "../demand/demand.types" import { ProjectConsolidation } from "../project/projectConsolidation.types" import { ReplenishingConsolidation } from "../replenishing/replenishingConsolidation.types" -export type TeamMembersHourlyRate = { - periodDate?: string - valuePerHourPerformed?: number -} - export type Team = { id?: string name?: string @@ -48,6 +43,7 @@ export type Team = { teamMonthlyInvestment?: NumberChartData teamMemberEfficiency?: MembershipEfficiencyList memberships?: Membership[] + membershipHourValueChartList?: MembershipHourValueChartList[] } export type MembershipEfficiencyList = { @@ -65,8 +61,19 @@ export type MembershipEfficiencyData = { realizedMoneyInMonth?: number avgHoursPerDemand?: number cardsCount?: number - expectedHourValue?: number - realizedHourValue?: number + hourValueRealized?: number + hourValueExpected?: number +} + +type MembershipHourValueChartList = { + membership?: Membership + membershipHourValueChartData?: MembershipHourValueChartData[] +} + +type MembershipHourValueChartData = { + date?: string + hourValueExpected?: number + hourValueRealized?: number } export type Membership = { @@ -81,5 +88,4 @@ export type Membership = { endDate?: string memberRole?: number memberRoleDescription?: string - teamMembersHourlyRateList?: TeamMembersHourlyRate[] } diff --git a/app/spa/src/modules/teamMember/teamMember.types.ts b/app/spa/src/modules/teamMember/teamMember.types.ts index d82c12d72..909e1ef16 100644 --- a/app/spa/src/modules/teamMember/teamMember.types.ts +++ b/app/spa/src/modules/teamMember/teamMember.types.ts @@ -13,11 +13,6 @@ type LeadTimesChartData = { leadTimeP95?: number } -type TeamMemberConsolidation = { - consolidationDate?: string - valuePerHourPerformed?: number -} - export type TeamMember = { id: string name: string @@ -48,7 +43,6 @@ export type TeamMember = { } demandEfforts?: DemandEffort[] demandEffortsList?: DemandEffortsList - teamMemberConsolidationList?: TeamMemberConsolidation[] } type DemandEffortsList = { diff --git a/app/spa/src/pages/TeamMembers/TeamMemberDashboard.tsx b/app/spa/src/pages/TeamMembers/TeamMemberDashboard.tsx index 3c9370733..a1b24f101 100644 --- a/app/spa/src/pages/TeamMembers/TeamMemberDashboard.tsx +++ b/app/spa/src/pages/TeamMembers/TeamMemberDashboard.tsx @@ -150,10 +150,6 @@ const TEAM_MEMBER_QUERY = gql` automaticUpdate membershipEffortPercentage } - teamMemberConsolidationList { - consolidationDate - valuePerHourPerformed - } demandEffortsList( fromDate: $fromDate untilDate: $untilDate diff --git a/app/spa/src/pages/Teams/MemberEfficiencyTable.tsx b/app/spa/src/pages/Teams/MemberEfficiencyTable.tsx index 6af48bda9..753ba9468 100644 --- a/app/spa/src/pages/Teams/MemberEfficiencyTable.tsx +++ b/app/spa/src/pages/Teams/MemberEfficiencyTable.tsx @@ -184,6 +184,8 @@ export const MEMBER_EFFICIENCY_TABLE_QUERY = gql` realizedMoneyInMonth memberCapacityValue cardsCount + hourValueRealized + hourValueExpected } } } diff --git a/app/spa/src/pages/Teams/TeamDashboard.tsx b/app/spa/src/pages/Teams/TeamDashboard.tsx index 2499161f5..0e99c92a7 100644 --- a/app/spa/src/pages/Teams/TeamDashboard.tsx +++ b/app/spa/src/pages/Teams/TeamDashboard.tsx @@ -47,7 +47,7 @@ const TEAM_DASHBOARD_QUERY = gql` periodDate valuePerHourPerformed } - teamMemberName + teamMemberName hoursPerMonth } demandsFlowChartData(startDate: $startDate, endDate: $endDate) { @@ -80,6 +80,17 @@ const TEAM_DASHBOARD_QUERY = gql` xAxis yAxis } + + membershipHourValueChartList { + membership { + id + teamMemberName + } + membershipHourValueChartData { + date + hourValueRealized + } + } } } @@ -204,24 +215,21 @@ const TeamDashboard = () => { }, ] - const lineChartMembershipData = team?.memberships - ? team?.memberships?.map((membership) => { - const seila = { - id: membership?.teamMemberName ? membership.teamMemberName : "", - data: membership?.teamMembersHourlyRateList - ? membership?.teamMembersHourlyRateList?.map( - (teamMembersHourlyRate) => { - return { - x: String(teamMembersHourlyRate.periodDate || ""), - y: String(teamMembersHourlyRate.valuePerHourPerformed || 0), - } - } - ) - : [], - } - return seila - }) - : [] + const lineChartMembershipData = + team?.membershipHourValueChartList?.map((membershipHourValueList) => { + return { + id: membershipHourValueList.membership?.teamMemberName ?? "", + data: + membershipHourValueList.membershipHourValueChartData?.map( + (membershipHourValueChartData) => { + return { + x: String(membershipHourValueChartData.date || ""), + y: String(membershipHourValueChartData.hourValueRealized || 0), + } + } + ) ?? [], + } + }) ?? [] return ( [], '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', '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 }] }], + 'membershipHourValueChartListType' => [{ 'date' => '2022-08-31', 'membershipHourValueChartData' => [{ 'hourValueExpected' => 15.625, 'hourValueRealized' => 25.0, 'membership' => { 'id' => membership.id.to_s } }, { 'hourValueExpected' => 12.5, 'hourValueRealized' => 0.0, 'membership' => { 'id' => other_membership.id.to_s } }] }, { 'date' => '2022-09-30', 'membershipHourValueChartData' => [{ 'hourValueExpected' => 15.625, 'hourValueRealized' => 0.0, 'membership' => { 'id' => membership.id.to_s } }, { 'hourValueExpected' => 12.5, 'hourValueRealized' => 0.0, 'membership' => { 'id' => other_membership.id.to_s } }] }], + 'memberships' => [{ 'id' => other_membership.id.to_s, 'memberRoleDescription' => 'Cliente' }, + { 'id' => membership.id.to_s, 'memberRoleDescription' => 'Desenvolvedor' }], 'lastReplenishingConsolidations' => [ { 'id' => replenishing_consolidation.id.to_s, @@ -945,7 +952,7 @@ travel_to Time.zone.local(2023, 5, 3, 10) do product = Fabricate :product review = Fabricate :service_delivery_review, product: product, meeting_date: Time.zone.now - Fabricate :service_delivery_review, product: product, meeting_date: 4.weeks.ago + Fabricate :service_delivery_review, product: product, meeting_date: 5.weeks.ago query = %( @@ -971,13 +978,13 @@ ) project = Fabricate :project, products: [product] - Fabricate :flow_event, project: project, event_date: 2.days.ago - Fabricate :flow_event, project: project, event_date: 1.week.ago - Fabricate :flow_event, project: project, event_date: 2.days.from_now + Fabricate :flow_event, project: project, event_date: 2.days.ago, event_type: :other_team_dependency + Fabricate :flow_event, project: project, event_date: 1.week.ago, event_type: :api_not_ready + Fabricate :flow_event, project: project, event_date: 2.days.from_now, event_type: :customer_not_available Fabricate :demand, project: project, company: project.company, service_delivery_review: review, end_date: 4.weeks.ago, class_of_service: :expedite Fabricate :demand, project: project, company: project.company, service_delivery_review: review, end_date: 3.weeks.ago, class_of_service: :standard - Fabricate :demand, project: project, company: project.company, service_delivery_review: review, end_date: 1.week.ago, class_of_service: :standard + Fabricate :demand, project: project, company: project.company, service_delivery_review: review, end_date: 2.weeks.ago, class_of_service: :standard Fabricate :demand, project: project, company: project.company, service_delivery_review: review, end_date: 4.days.ago, class_of_service: :fixed_date review.save @@ -985,7 +992,7 @@ result = FlowClimateSchema.execute(query, variables: nil).as_json expect(result.dig('data', 'serviceDeliveryReview', 'id')).to eq review.id.to_s - expect(result.dig('data', 'serviceDeliveryReview', 'flowEventsChartData').count).to eq 2 + expect(result.dig('data', 'serviceDeliveryReview', 'flowEventsChartData').pluck('label')).to contain_exactly('Dependência de outro time', 'API não está pronta') expect(result.dig('data', 'serviceDeliveryReview', 'classOfServiceChartData').pluck('label')).to eq(['Padrão', 'Expedição', 'Data Fixa']) end end @@ -1904,10 +1911,6 @@ yAxisProjectsNames yAxisHours } - teamMemberConsolidationList { - consolidationDate - hourValueRealized - } memberThroughputData(numberOfWeeks: 3) } }) @@ -2056,7 +2059,6 @@ 'yAxisHours' => [170.0], 'yAxisProjectsNames' => [project.name] }, - '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/membership_spec.rb b/spec/models/membership_spec.rb index 3f3808a5c..52496b323 100644 --- a/spec/models/membership_spec.rb +++ b/spec/models/membership_spec.rb @@ -17,24 +17,22 @@ it { is_expected.to validate_presence_of :start_date } context 'unique active membership for team member' do - let(:team) { Fabricate :team } - let(:team_member) { Fabricate :team_member } - let(:other_team_member) { Fabricate :team_member } - - let!(:membership) { Fabricate :membership, team: team, team_member: team_member, end_date: nil } - let!(:duplicated_membership) { Fabricate.build :membership, team: team, team_member: team_member, end_date: nil } - let!(:second_valid_membership) { Fabricate.build :membership, team_member: team_member, end_date: nil } - let!(:third_valid_membership) { Fabricate.build :membership, team: team, end_date: nil } - let!(:fourth_valid_membership) { Fabricate.build :membership, team_member: team_member, team: team, end_date: Time.zone.today } - - before { duplicated_membership.valid? } - - it { expect(membership.valid?).to be true } - it { expect(second_valid_membership.valid?).to be true } - it { expect(third_valid_membership.valid?).to be true } - it { expect(fourth_valid_membership.valid?).to be true } - - it { expect(duplicated_membership.errors_on(:team_member)).to eq [I18n.t('activerecord.errors.models.membership.team_member.already_existent_active')] } + it 'refuses duplicated members' do + team = Fabricate :team + team_member = Fabricate :team_member + + membership = Fabricate :membership, team: team, team_member: team_member, end_date: nil + duplicated_membership = Fabricate.build :membership, team: team, team_member: team_member, end_date: nil + second_valid_membership = Fabricate.build :membership, team_member: team_member, end_date: nil + third_valid_membership = Fabricate.build :membership, team: team, end_date: nil + fourth_valid_membership = Fabricate.build :membership, team_member: team_member, team: team, end_date: Time.zone.today + expect(duplicated_membership).not_to be_valid + expect(duplicated_membership.errors_on(:team_member)).to eq [I18n.t('activerecord.errors.models.membership.team_member.already_existent_active')] + expect(membership).to be_valid + expect(second_valid_membership).to be_valid + expect(third_valid_membership).to be_valid + expect(fourth_valid_membership).to be_valid + end end end @@ -419,6 +417,43 @@ end end + describe '#realized_hour_value' do + context 'with efforts' do + it 'returns the realized effort' do + travel_to Time.zone.local(2023, 4, 13, 10) do + start_date = Time.zone.today.beginning_of_month.to_date + end_date = start_date.end_of_month + + team = Fabricate :team + demand = Fabricate :demand, team: team + other_demand = Fabricate :demand, team: team + team_member = Fabricate :team_member, monthly_payment: 7000, hours_per_month: 160 + membership = Fabricate :membership, team: team, team_member: team_member, hours_per_month: 120 + 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 + + expect(membership.realized_hour_value(end_date).to_f).to eq 26.25 + end + end + end + + context 'without efforts' do + it 'returns zero' do + travel_to Time.zone.local(2023, 4, 13, 10) do + start_date = Time.zone.today.beginning_of_month.to_date + end_date = start_date.end_of_month + + team = Fabricate :team + membership = Fabricate :membership, team: team + + expect(membership.realized_hour_value(end_date)).to eq 0 + end + end + end + end + describe '#expected_hour_value' do context 'with hours per month' do it 'returns the monthly payment divided by the hours per month' do diff --git a/spec/models/portfolio_unit_spec.rb b/spec/models/portfolio_unit_spec.rb index f98dab826..e4a42ed66 100644 --- a/spec/models/portfolio_unit_spec.rb +++ b/spec/models/portfolio_unit_spec.rb @@ -20,18 +20,18 @@ end context 'with complex ones' do - let(:product) { Fabricate :product } - it 'rejects the one duplicated on name and parent' do - parent = Fabricate.build :portfolio_unit, product: product, name: 'parent' - first_portfolio_unit = Fabricate :portfolio_unit, product: product, parent: parent, name: 'aaa' - second_portfolio_unit = Fabricate.build :portfolio_unit, product: product, name: 'aaa' - third_portfolio_unit = Fabricate.build :portfolio_unit, product: product, parent: parent, name: 'aaa' - - expect(parent.valid?).to be true - expect(first_portfolio_unit.valid?).to be true - expect(second_portfolio_unit.valid?).to be true - expect(third_portfolio_unit.valid?).to be false + product = Fabricate :product + parent = Fabricate.build :portfolio_unit, product: product, name: 'parent', children: [] + expect(parent).to be_valid + + first_portfolio_unit = Fabricate :portfolio_unit, product: product, parent: parent, name: 'aaa', children: [] + second_portfolio_unit = Fabricate.build :portfolio_unit, product: product, name: 'aaa', children: [] + third_portfolio_unit = Fabricate.build :portfolio_unit, product: product, parent: parent, name: 'aaa', children: [] + + expect(first_portfolio_unit).to be_valid + expect(second_portfolio_unit).to be_valid + expect(third_portfolio_unit).not_to be_valid expect(third_portfolio_unit.errors_on(:name)).to eq [I18n.t('portfolio_unit.validations.name')] end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ac93720ab..85e92126d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1496,11 +1496,14 @@ let(:project) { Fabricate :project } context 'with project consolidations' do - let!(:first_consolidation) { Fabricate :project_consolidation, project: project, consolidation_date: 1.day.ago, project_throughput: 10 } - let!(:second_consolidation) { Fabricate :project_consolidation, project: project, consolidation_date: 2.days.ago, project_throughput: 5 } - let!(:third_consolidation) { Fabricate :project_consolidation, project: project, consolidation_date: 2.days.ago, project_throughput: 1 } - - it { expect(project.last_weekly_throughput(10)).to eq [4, 5] } + it 'returns the throughtput' do + travel_to Time.zone.local(2020, 12, 2, 10, 0, 0) do + Fabricate :project_consolidation, project: project, consolidation_date: 1.day.ago, project_throughput: 10 + Fabricate :project_consolidation, project: project, consolidation_date: 2.days.ago, project_throughput: 5 + Fabricate :project_consolidation, project: project, consolidation_date: 2.days.ago, project_throughput: 1 + expect(project.last_weekly_throughput(10)).to eq [4, 5] + end + end end context 'with no consolidations' do