diff --git a/client.go b/client.go index 2fc842b..714a389 100644 --- a/client.go +++ b/client.go @@ -146,3 +146,11 @@ func (c *Client) GetAllAgencyCollection(agency string) ([]models.AgencyMonthlyIn } return collections, nil } + +func (c *Client) GetAveragePerCapita(agency string, year int) (*models.PerCapitaData, error) { + avg, err := c.Db.GetAveragePerCapita(agency, year) + if err != nil { + return nil, fmt.Errorf("GetAveragePerCapita() error: %w", err) + } + return avg, nil +} diff --git a/models/monthlyInfo.go b/models/monthlyInfo.go index 7a435fc..1e32e75 100644 --- a/models/monthlyInfo.go +++ b/models/monthlyInfo.go @@ -79,16 +79,20 @@ type GeneralMonthlyInfo struct { } type AnnualSummary struct { - Year int `json:"year,omitempty"` // Year of the data - AverageCount int `json:"average_count,omitempty"` // Average number of employees - TotalCount int `json:"total_count,omitempty"` // Total number of employees - BaseRemuneration float64 `json:"base_remuneration,omitempty"` // Statistics (Max, Min, Median, Total) - OtherRemunerations float64 `json:"other_remunerations,omitempty"` // Statistics (Max, Min, Median, Total) - Discounts float64 `json:"discounts,omitempty"` // Statistics (Max, Min, Median, Total) - Remunerations float64 `json:"remunerations,omitempty"` // Statistics (Max, Min, Median, Total) - NumMonthsWithData int `json:"months_with_data,omitempty"` - Package *Backup `json:"package,omitempty"` - ItemSummary ItemSummary `json:"item_summary,omitempty"` + Year int `json:"year,omitempty"` // Year of the data + AverageCount int `json:"average_count,omitempty"` // Average number of employees + TotalCount int `json:"total_count,omitempty"` // Total number of employees + BaseRemuneration float64 `json:"base_remuneration,omitempty"` // Statistics (Max, Min, Median, Total) + OtherRemunerations float64 `json:"other_remunerations,omitempty"` // Statistics (Max, Min, Median, Total) + BaseRemunerationPerCapita float64 `json:"base_remuneration_member,omitempty"` + OtherRemunerationsPerCapita float64 `json:"other_remunerations_member,omitempty"` + DiscountsPerCapita float64 `json:"discounts_member,omitempty"` + RemunerationsPerCapita float64 `json:"remunerations_member,omitempty"` + Discounts float64 `json:"discounts,omitempty"` // Statistics (Max, Min, Median, Total) + Remunerations float64 `json:"remunerations,omitempty"` // Statistics (Max, Min, Median, Total) + NumMonthsWithData int `json:"months_with_data,omitempty"` + Package *Backup `json:"package,omitempty"` + ItemSummary ItemSummary `json:"item_summary,omitempty"` } type RemmunerationSummary struct { diff --git a/models/perCapitaData.go b/models/perCapitaData.go new file mode 100644 index 0000000..73d42c8 --- /dev/null +++ b/models/perCapitaData.go @@ -0,0 +1,10 @@ +package models + +type PerCapitaData struct { + AgencyID string `json:"orgao,omitempty"` + Year int `json:"ano,omitempty"` + BaseRemuneration float64 `json:"remuneracao_base,omitempty"` + OtherRemunerations float64 `json:"outras_remuneracoes,omitempty"` + Discounts float64 `json:"descontos,omitempty"` + Remunerations float64 `json:"remuneracoes,omitempty"` +} diff --git a/repo/database/database_mock.go b/repo/database/database_mock.go index ce75d49..1d99a9c 100644 --- a/repo/database/database_mock.go +++ b/repo/database/database_mock.go @@ -152,6 +152,21 @@ func (mr *MockInterfaceMockRecorder) GetAnnualSummary(agency interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAnnualSummary", reflect.TypeOf((*MockInterface)(nil).GetAnnualSummary), agency) } +// GetAveragePerCapita mocks base method. +func (m *MockInterface) GetAveragePerCapita(agency string, year int) (*models.PerCapitaData, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAveragePerCapita", agency, year) + ret0, _ := ret[0].(*models.PerCapitaData) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAveragePerCapita indicates an expected call of GetAveragePerCapita. +func (mr *MockInterfaceMockRecorder) GetAveragePerCapita(agency, year interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAveragePerCapita", reflect.TypeOf((*MockInterface)(nil).GetAveragePerCapita), agency, year) +} + // GetFirstDateWithMonthlyInfo mocks base method. func (m *MockInterface) GetFirstDateWithMonthlyInfo() (int, int, error) { m.ctrl.T.Helper() diff --git a/repo/database/dto/annuaISummary.go b/repo/database/dto/annuaISummary.go index 9fb9ec5..d527bd3 100644 --- a/repo/database/dto/annuaISummary.go +++ b/repo/database/dto/annuaISummary.go @@ -5,27 +5,35 @@ import ( ) type AnnualSummaryDTO struct { - Year int `gorm:"column:ano"` - AverageCount int `gorm:"column:media_num_membros"` - TotalCount int `gorm:"column:total_num_membros"` - BaseRemuneration float64 `gorm:"column:remuneracao_base"` - OtherRemunerations float64 `gorm:"column:outras_remuneracoes"` - Discounts float64 `gorm:"column:descontos"` - Remunerations float64 `gorm:"column:remuneracoes"` - NumMonthsWithData int `gorm:"column:meses_com_dados"` - ItemSummary ItemSummary `gorm:"embedded"` + Year int `gorm:"column:ano"` + AverageCount int `gorm:"column:media_num_membros"` + TotalCount int `gorm:"column:total_num_membros"` + BaseRemuneration float64 `gorm:"column:remuneracao_base"` + OtherRemunerations float64 `gorm:"column:outras_remuneracoes"` + Discounts float64 `gorm:"column:descontos"` + Remunerations float64 `gorm:"column:remuneracoes"` + BaseRemunerationPerCapita float64 `gorm:"column:remuneracao_base_membro"` + OtherRemunerationsPerCapita float64 `gorm:"column:outras_remuneracoes_membro"` + DiscountsPerCapita float64 `gorm:"column:descontos_membro"` + RemunerationsPerCapita float64 `gorm:"column:remuneracoes_membro"` + NumMonthsWithData int `gorm:"column:meses_com_dados"` + ItemSummary ItemSummary `gorm:"embedded"` } func NewAnnualSummaryDTO(ami models.AnnualSummary) *AnnualSummaryDTO { return &AnnualSummaryDTO{ - Year: ami.Year, - AverageCount: ami.AverageCount, - TotalCount: ami.TotalCount, - BaseRemuneration: ami.BaseRemuneration, - OtherRemunerations: ami.OtherRemunerations, - Discounts: ami.Discounts, - Remunerations: ami.Remunerations, - NumMonthsWithData: ami.NumMonthsWithData, + Year: ami.Year, + AverageCount: ami.AverageCount, + TotalCount: ami.TotalCount, + BaseRemuneration: ami.BaseRemuneration, + OtherRemunerations: ami.OtherRemunerations, + BaseRemunerationPerCapita: ami.BaseRemunerationPerCapita, + OtherRemunerationsPerCapita: ami.OtherRemunerationsPerCapita, + DiscountsPerCapita: ami.DiscountsPerCapita, + RemunerationsPerCapita: ami.RemunerationsPerCapita, + Discounts: ami.Discounts, + Remunerations: ami.Remunerations, + NumMonthsWithData: ami.NumMonthsWithData, ItemSummary: ItemSummary{ FoodAllowance: ami.ItemSummary.FoodAllowance, BonusLicense: ami.ItemSummary.BonusLicense, @@ -41,14 +49,18 @@ func NewAnnualSummaryDTO(ami models.AnnualSummary) *AnnualSummaryDTO { func (ami *AnnualSummaryDTO) ConvertToModel() *models.AnnualSummary { return &models.AnnualSummary{ - Year: ami.Year, - AverageCount: ami.AverageCount, - TotalCount: ami.TotalCount, - BaseRemuneration: ami.BaseRemuneration, - OtherRemunerations: ami.OtherRemunerations, - Discounts: ami.Discounts, - Remunerations: ami.Remunerations, - NumMonthsWithData: ami.NumMonthsWithData, + Year: ami.Year, + AverageCount: ami.AverageCount, + TotalCount: ami.TotalCount, + BaseRemuneration: ami.BaseRemuneration, + OtherRemunerations: ami.OtherRemunerations, + BaseRemunerationPerCapita: ami.BaseRemunerationPerCapita, + OtherRemunerationsPerCapita: ami.OtherRemunerationsPerCapita, + DiscountsPerCapita: ami.DiscountsPerCapita, + RemunerationsPerCapita: ami.RemunerationsPerCapita, + Discounts: ami.Discounts, + Remunerations: ami.Remunerations, + NumMonthsWithData: ami.NumMonthsWithData, ItemSummary: models.ItemSummary{ FoodAllowance: ami.ItemSummary.FoodAllowance, BonusLicense: ami.ItemSummary.BonusLicense, diff --git a/repo/database/dto/perCapitaDataDTO.go b/repo/database/dto/perCapitaDataDTO.go new file mode 100644 index 0000000..f528f47 --- /dev/null +++ b/repo/database/dto/perCapitaDataDTO.go @@ -0,0 +1,27 @@ +package dto + +import "github.com/dadosjusbr/storage/models" + +type PerCapitaData struct { + AgencyID string `gorm:"column:orgao"` + Year int `gorm:"column:ano"` + BaseRemuneration float64 `gorm:"column:salario"` + OtherRemunerations float64 `gorm:"column:beneficios"` + Discounts float64 `gorm:"column:descontos"` + Remunerations float64 `gorm:"column:remuneracao"` +} + +func (PerCapitaData) TableName() string { + return "media_por_membro" +} + +func (a *PerCapitaData) ConvertToModel() *models.PerCapitaData { + return &models.PerCapitaData{ + AgencyID: a.AgencyID, + Year: a.Year, + BaseRemuneration: a.BaseRemuneration, + OtherRemunerations: a.OtherRemunerations, + Discounts: a.Discounts, + Remunerations: a.Remunerations, + } +} diff --git a/repo/database/init_db.sql b/repo/database/init_db.sql index 1c1c8d1..cdde4d2 100644 --- a/repo/database/init_db.sql +++ b/repo/database/init_db.sql @@ -99,3 +99,25 @@ create table remuneracoes constraint pk_remuneracoes primary key (id, id_contracheque, orgao, mes, ano), constraint fk_remuneracoes foreign key (id_contracheque, orgao, mes, ano) references contracheques(id, orgao, mes, ano) on delete cascade ); + +CREATE MATERIALIZED VIEW public.media_por_membro +TABLESPACE pg_default +AS SELECT media_por_membro.orgao, + media_por_membro.ano, + avg(media_por_membro.salario) AS salario, + avg(media_por_membro.beneficios) AS beneficios, + avg(media_por_membro.descontos) AS descontos, + avg(media_por_membro.remuneracao) AS remuneracao + FROM ( SELECT c.orgao, + c.ano, + c.nome_sanitizado, + count(*) AS num_meses, + avg(c.salario) AS salario, + avg(c.beneficios) AS beneficios, + avg(c.descontos) AS descontos, + avg(c.remuneracao) AS remuneracao + FROM contracheques c + GROUP BY c.orgao, c.ano, c.nome_sanitizado) media_por_membro + WHERE media_por_membro.num_meses > 1 + GROUP BY media_por_membro.orgao, media_por_membro.ano +WITH DATA; \ No newline at end of file diff --git a/repo/database/interface.go b/repo/database/interface.go index ea457b5..011ee29 100644 --- a/repo/database/interface.go +++ b/repo/database/interface.go @@ -32,4 +32,5 @@ type Interface interface { GetAllAgencyCollection(agency string) ([]models.AgencyMonthlyInfo, error) GetPaychecks(agency models.Agency, year int) ([]models.Paycheck, error) GetPaycheckItems(agency models.Agency, year int) ([]models.PaycheckItem, error) + GetAveragePerCapita(agency string, year int) (*models.PerCapitaData, error) } diff --git a/repo/database/postgres.go b/repo/database/postgres.go index 23bd74c..1fc1d13 100644 --- a/repo/database/postgres.go +++ b/repo/database/postgres.go @@ -307,7 +307,7 @@ func (p *PostgresDB) GetAnnualSummary(agency string) ([]models.AnnualSummary, er var dtoAmis []dto.AnnualSummaryDTO agency = strings.ToLower(agency) query := ` - ano, + coletas.ano, id_orgao, TRUNC(AVG((sumario -> 'membros')::text::int)) AS media_num_membros, SUM((sumario -> 'membros')::text::int) AS total_num_membros, @@ -323,10 +323,16 @@ func (p *PostgresDB) GetAnnualSummary(agency string) ([]models.AnnualSummary, er SUM(CAST(sumario -> 'resumo_rubricas' ->> 'auxilio_saude' AS DECIMAL)) AS auxilio_saude, SUM(CAST(sumario -> 'resumo_rubricas' ->> 'outras' AS DECIMAL)) AS outras, SUM(CAST(sumario -> 'resumo_rubricas' ->> 'ferias' AS DECIMAL)) AS ferias, - COUNT(*) AS meses_com_dados` - m := p.db.Model(&dtoAgmi).Select(query) + COUNT(*) AS meses_com_dados, + MAX(media_por_membro.salario) AS remuneracao_base_membro, + MAX(media_por_membro.beneficios) AS outras_remuneracoes_membro, + MAX(media_por_membro.descontos) AS descontos_membro, + MAX(media_por_membro.remuneracao) AS remuneracoes_membro` + + join := `LEFT JOIN media_por_membro on coletas.ano = media_por_membro.ano and coletas.id_orgao = media_por_membro.orgao` + m := p.db.Model(&dtoAgmi).Select(query).Joins(join) m = m.Where("id_orgao = ? AND atual = TRUE AND (procinfo::text = 'null' OR procinfo IS NULL) ", agency) - m = m.Group("ano, id_orgao").Order("ano ASC") + m = m.Group("coletas.ano, id_orgao").Order("coletas.ano ASC") if err := m.Scan(&dtoAmis).Error; err != nil { return nil, fmt.Errorf("error getting annual monthly info: %q", err) } @@ -549,3 +555,14 @@ func (p *PostgresDB) GetPaycheckItems(agency models.Agency, year int) ([]models. } return results, nil } + +func (p *PostgresDB) GetAveragePerCapita(agency string, ano int) (*models.PerCapitaData, error) { + var dtoAvg dto.PerCapitaData + m := p.db.Model(&dto.PerCapitaData{}) + m = m.Where("orgao = ? AND ano = ?", agency, ano) + if err := m.Find(&dtoAvg).Error; err != nil { + return nil, fmt.Errorf("error getting average per capita: %q", err) + } + avg := dtoAvg.ConvertToModel() + return avg, nil +} diff --git a/repo/database/postgres_test.go b/repo/database/postgres_test.go index cadcba9..77b1de8 100644 --- a/repo/database/postgres_test.go +++ b/repo/database/postgres_test.go @@ -1023,11 +1023,61 @@ func (g getAnnualSummary) testWhenMonthlyInfoExists(t *testing.T) { }, }, }, + { + AgencyID: "tjal", + Year: 2023, + Month: 5, + CrawlingTimestamp: timestamppb.Now(), + Summary: &models.Summary{ + Count: 300, + BaseRemuneration: models.DataSummary{ + Total: 1000, + }, + OtherRemunerations: models.DataSummary{ + Total: 1200, + }, + Discounts: models.DataSummary{ + Total: 200, + }, + Remunerations: models.DataSummary{ + Total: 2000, + }, + }, + }, + { + AgencyID: "tjal", + Year: 2023, + Month: 4, + CrawlingTimestamp: timestamppb.Now(), + Summary: &models.Summary{ + Count: 300, + BaseRemuneration: models.DataSummary{ + Total: 1000, + }, + OtherRemunerations: models.DataSummary{ + Total: 1200, + }, + Discounts: models.DataSummary{ + Total: 200, + }, + Remunerations: models.DataSummary{ + Total: 2000, + }, + }, + }, } if err := insertMonthlyInfos(agmis); err != nil { t.Fatalf("error inserting agency monthly info: %q", err) } + p, pi := paychecks() + _ = postgresDb.StorePaychecks(p, pi) + + result := postgresDb.db.Exec("REFRESH MATERIALIZED VIEW media_por_membro;") + if result.Error != nil { + t.Fatalf("Erro ao fazer o refresh: %v\n", result.Error) + } + var amis []models.AnnualSummary //Realizando a soma das remunerações por ano for _, agmi := range agmis { @@ -1085,6 +1135,8 @@ func (g getAnnualSummary) testWhenMonthlyInfoExists(t *testing.T) { assert.Equal(t, amis[1].ItemSummary.CompensatoryLicense, returnedAmis[1].ItemSummary.CompensatoryLicense) assert.Equal(t, amis[1].ItemSummary.HealthAllowance, returnedAmis[1].ItemSummary.HealthAllowance) assert.Equal(t, amis[1].ItemSummary.Vacation, returnedAmis[1].ItemSummary.Vacation) + assert.Equal(t, 1000.0, returnedAmis[2].BaseRemunerationPerCapita) + assert.Equal(t, 1200.0, returnedAmis[2].OtherRemunerationsPerCapita) truncateTables() } @@ -2166,7 +2218,7 @@ func (paycheck) testStorePaychecks(t *testing.T) { itemSanitizado := "subsidio" assert.Nil(t, err) - assert.Equal(t, len(dtoPaychecks), 1) + assert.Equal(t, len(dtoPaychecks), 2) assert.Equal(t, len(dtoPaycheckItems), 3) assert.Equal(t, dtoPaychecks[0].Name, "nome") assert.Equal(t, dtoPaychecks[0].SanitizedName, "nome") @@ -2199,9 +2251,9 @@ func (paycheck) testGetPaychecks(t *testing.T) { t.Fatalf("error GetPaychecks(): %v", err) } assert.Nil(t, err) - assert.Equal(t, 1, len(ps)) + assert.Equal(t, 2, len(ps)) assert.Equal(t, 2023, ps[0].Year) - assert.Equal(t, p[0], ps[0]) + assert.Equal(t, p[0], ps[1]) } func (paycheck) testGetPaycheckItems(t *testing.T) { @@ -2216,6 +2268,26 @@ func (paycheck) testGetPaycheckItems(t *testing.T) { assert.Equal(t, pi[0], pis[0]) } +type averagePerCapita struct{} + +func TestAveragePerCapita(t *testing.T) { + tests := averagePerCapita{} + + t.Run("Test GetAverage()", tests.testGetAveragePerCapita) +} + +func (averagePerCapita) testGetAveragePerCapita(t *testing.T) { + avg, err := postgresDb.GetAveragePerCapita("tjal", 2023) + if err != nil { + t.Fatalf("error GetAveragePerCapita(): %v", err) + } + assert.Nil(t, err) + assert.Equal(t, 1000.0, avg.BaseRemuneration) + assert.Equal(t, 1200.0, avg.OtherRemunerations) + assert.Equal(t, 200.0, avg.Discounts) + assert.Equal(t, 2000.0, avg.Remunerations) +} + func insertAgencies(agencies []models.Agency) error { for _, agency := range agencies { agencyDto, err := dto.NewAgencyDTO(agency) @@ -2313,6 +2385,23 @@ func paychecks() ([]models.Paycheck, []models.PaycheckItem) { Situation: &situation, SanitizedName: "nome", }, + { + ID: 1, + Agency: "tjal", + Month: 4, + Year: 2023, + CollectKey: "tjal/04/2023", + Name: "nome", + RegisterID: "123", + Role: "funcao", + Workplace: "local de trabalho", + Salary: 1000, + Benefits: 1200, + Discounts: 200, + Remuneration: 2000, + Situation: &situation, + SanitizedName: "nome", + }, } itemSanitizado := []string{"subsidio", "descontos diversos"} pi := []models.PaycheckItem{