Skip to content

Commit dc1dad6

Browse files
fix #82
1 parent b7d4b21 commit dc1dad6

File tree

8 files changed

+195
-17
lines changed

8 files changed

+195
-17
lines changed

src/main/kotlin/br/com/webbudget/domain/entities/registration/MovementClass.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class MovementClass(
3030
var description: String? = null,
3131
) : PersistentEntity<Long>() {
3232

33+
fun isForExpense() = type == Type.EXPENSE
34+
35+
fun isForIncome() = type == Type.INCOME
36+
3337
enum class Type {
3438
INCOME, EXPENSE
3539
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package br.com.webbudget.domain.projections.registration
2+
3+
import java.math.BigDecimal
4+
5+
interface BudgetAllocated {
6+
val total: BigDecimal
7+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package br.com.webbudget.domain.validators.registration
2+
3+
import br.com.webbudget.domain.entities.registration.MovementClass
4+
import br.com.webbudget.domain.entities.registration.MovementClass.Type
5+
import br.com.webbudget.domain.exceptions.BusinessException
6+
import br.com.webbudget.domain.validators.OnCreateValidation
7+
import br.com.webbudget.domain.validators.OnUpdateValidation
8+
import br.com.webbudget.infrastructure.repository.registration.MovementClassRepository
9+
import org.springframework.stereotype.Component
10+
import java.math.BigDecimal
11+
12+
@Component
13+
@OnUpdateValidation
14+
@OnCreateValidation
15+
class BudgetLimitValidator(
16+
private val movementClassRepository: MovementClassRepository
17+
) : MovementClassValidator {
18+
19+
override fun validate(value: MovementClass) {
20+
21+
// no budget limit, so the movement class is valid
22+
if (value.budget == null) {
23+
return
24+
}
25+
26+
val costCenter = value.costCenter
27+
28+
// expenses have no budget limit, so the movement class is valid
29+
if (value.isForExpense() && costCenter.expenseBudget == null) {
30+
return
31+
}
32+
33+
// incomes have no budget limit, so the movement class is valid
34+
if (value.isForIncome() && costCenter.incomeBudget == null) {
35+
return
36+
}
37+
38+
// get the cost center budget based on the movement class type
39+
val costCenterBudget = if (value.isForIncome()) {
40+
costCenter.incomeBudget!!
41+
} else {
42+
costCenter.expenseBudget!!
43+
}
44+
45+
if (value.isSaved()) {
46+
this.validateSaved(value, costCenterBudget)
47+
} else {
48+
this.validateNotSaved(value, costCenterBudget)
49+
}
50+
}
51+
52+
private fun validateSaved(value: MovementClass, costCenterBudget: BigDecimal) {
53+
54+
val currentMovementClass = movementClassRepository.findByExternalId(value.externalId!!)
55+
?: error("Can't find movement class with external id [${value.externalId}]")
56+
57+
val allocatedBudget = movementClassRepository.findBudgetAllocatedByCostCenter(value.costCenter, value.type)
58+
59+
// before checking, remove the current movement class budget from the total allocated budget
60+
val remainingBudget = costCenterBudget.subtract(allocatedBudget.total.subtract(currentMovementClass.budget!!))
61+
62+
check(value.budget!!, remainingBudget, value.type)
63+
}
64+
65+
private fun validateNotSaved(value: MovementClass, costCenterBudget: BigDecimal) {
66+
67+
val allocatedBudget = movementClassRepository.findBudgetAllocatedByCostCenter(value.costCenter, value.type)
68+
val remainingBudget = costCenterBudget.subtract(allocatedBudget.total)
69+
70+
check(value.budget!!, remainingBudget, value.type)
71+
}
72+
73+
private fun check(budget: BigDecimal, remainingBudget: BigDecimal, type: Type) {
74+
if (budget > remainingBudget) {
75+
throw BusinessException(
76+
"movement-class.errors.budget-limit-exceeded",
77+
"Only [$remainingBudget] of income budget is available for [$type]"
78+
)
79+
}
80+
}
81+
}

src/main/kotlin/br/com/webbudget/infrastructure/repository/registration/MovementClassRepository.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package br.com.webbudget.infrastructure.repository.registration
22

3+
import br.com.webbudget.domain.entities.registration.CostCenter
34
import br.com.webbudget.domain.entities.registration.MovementClass
5+
import br.com.webbudget.domain.entities.registration.MovementClass.Type
6+
import br.com.webbudget.domain.projections.registration.BudgetAllocated
47
import br.com.webbudget.infrastructure.repository.BaseRepository
58
import br.com.webbudget.infrastructure.repository.SpecificationHelpers
69
import org.springframework.data.jpa.domain.Specification
10+
import org.springframework.data.jpa.repository.Query
711
import org.springframework.stereotype.Repository
812
import java.util.UUID
913

@@ -14,6 +18,16 @@ interface MovementClassRepository : BaseRepository<MovementClass> {
1418

1519
fun findByNameIgnoreCaseAndExternalIdNot(name: String, externalId: UUID): MovementClass?
1620

21+
@Query(
22+
"""
23+
select coalesce(sum(mc.budget), 0.0) as total
24+
from MovementClass mc
25+
where mc.costCenter = :costCenter
26+
and mc.type = :type
27+
"""
28+
)
29+
fun findBudgetAllocatedByCostCenter(costCenter: CostCenter, type: Type): BudgetAllocated
30+
1731
object Specifications : SpecificationHelpers {
1832

1933
fun byName(name: String?) = Specification<MovementClass> { root, _, builder ->

src/test/kotlin/br/com/webbudget/services/registration/MovementClassServiceITest.kt

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package br.com.webbudget.services.registration
22

33
import br.com.webbudget.BaseIntegrationTest
4+
import br.com.webbudget.domain.entities.registration.MovementClass
5+
import br.com.webbudget.domain.exceptions.BusinessException
46
import br.com.webbudget.domain.exceptions.ConflictingPropertyException
57
import br.com.webbudget.domain.services.registration.MovementClassService
68
import br.com.webbudget.infrastructure.repository.registration.CostCenterRepository
@@ -11,10 +13,14 @@ import org.assertj.core.api.Assertions.assertThatThrownBy
1113
import org.junit.jupiter.api.Disabled
1214
import org.junit.jupiter.api.Test
1315
import org.junit.jupiter.api.fail
16+
import org.junit.jupiter.params.ParameterizedTest
17+
import org.junit.jupiter.params.provider.Arguments
18+
import org.junit.jupiter.params.provider.MethodSource
1419
import org.springframework.beans.factory.annotation.Autowired
1520
import org.springframework.test.context.jdbc.Sql
1621
import java.math.BigDecimal
1722
import java.util.UUID
23+
import java.util.stream.Stream
1824

1925
class MovementClassServiceITest : BaseIntegrationTest() {
2026

@@ -148,4 +154,68 @@ class MovementClassServiceITest : BaseIntegrationTest() {
148154
fun `should fail to delete when in use`() {
149155
// TODO this should be done after the movement feature is created
150156
}
157+
158+
@Sql(
159+
"/sql/registration/clear-tables.sql",
160+
"/sql/registration/create-cost-centers.sql"
161+
)
162+
@ParameterizedTest
163+
@MethodSource("movementClassesToCreate")
164+
fun `should validate cost center budget limit on create`(toCreate: MovementClass) {
165+
166+
val costCenter = costCenterRepository.findByExternalId(UUID.fromString("3cb5732d-2551-4eb9-8b41-f5d312ba7aac"))
167+
?: fail { OBJECT_NOT_FOUND_ERROR }
168+
169+
toCreate.costCenter = costCenter
170+
171+
assertThatThrownBy { movementClassService.create(toCreate) }
172+
.isInstanceOf(BusinessException::class.java)
173+
}
174+
175+
@Sql(
176+
"/sql/registration/clear-tables.sql",
177+
"/sql/registration/create-cost-centers.sql",
178+
"/sql/registration/create-movement-classes.sql"
179+
)
180+
@ParameterizedTest
181+
@MethodSource("movementClassesToUpdate")
182+
fun `should validate cost center budget limit on update`(externalId: UUID) {
183+
184+
val toUpdate = movementClassRepository.findByExternalId(externalId)
185+
?: fail { OBJECT_NOT_FOUND_ERROR }
186+
187+
toUpdate.apply {
188+
this.budget = BigDecimal.valueOf(1001)
189+
}
190+
191+
assertThatThrownBy { movementClassService.update(toUpdate) }
192+
.isInstanceOf(BusinessException::class.java)
193+
}
194+
195+
companion object {
196+
197+
@JvmStatic
198+
fun movementClassesToCreate(): Stream<Arguments> = Stream.of(
199+
Arguments.of(
200+
createMovementClass(
201+
name = "Impostos",
202+
type = MovementClass.Type.EXPENSE,
203+
budget = BigDecimal.valueOf(1001),
204+
)
205+
),
206+
Arguments.of(
207+
createMovementClass(
208+
name = "Trabalho como uber",
209+
type = MovementClass.Type.INCOME,
210+
budget = BigDecimal.valueOf(1001),
211+
)
212+
)
213+
)
214+
215+
@JvmStatic
216+
fun movementClassesToUpdate(): Stream<Arguments> = Stream.of(
217+
Arguments.of(UUID.fromString("86158792-f34e-4cdf-bce6-44394d645d0d")),
218+
Arguments.of(UUID.fromString("067e62d5-725f-44c6-bfdf-79f9cf19fff8"))
219+
)
220+
}
151221
}

src/test/resources/config/application-test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ logging:
1818
level:
1919
br.com.webbudget: debug
2020
org.springframework.web: trace
21+
org.hibernate.SQL: debug
22+
org.hibernate.type.descriptor.sql.BasicBinder: trace
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
INSERT INTO registration.cost_centers (id, external_id, version, created_on, last_update, name, description, active)
2-
VALUES (999, '52e3456b-1b0d-42c5-8be0-07ddaecce441', 0, current_timestamp, current_timestamp, 'Outros',
3-
'Centro de custo geral', true);
2+
VALUES (999, '52e3456b-1b0d-42c5-8be0-07ddaecce441', 0, current_timestamp, current_timestamp, 'Outros','Centro de custo geral', true);
3+
INSERT INTO registration.cost_centers (id, external_id, version, created_on, last_update, active, name, expense_budget, income_budget)
4+
VALUES(888, '3cb5732d-2551-4eb9-8b41-f5d312ba7aac', 0, current_timestamp, current_timestamp, true, 'Carro', 1000, 1000);
Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
INSERT INTO registration.movement_classes (external_id, version, created_on, last_update, active, name, type,
2-
description, budget, id_cost_center)
3-
VALUES ('f21d94d2-d28e-4aa3-b12d-8a520023edd9', 0, current_timestamp, current_timestamp, true, 'Mercado', 'EXPENSE',
4-
'Despesas no mercado', 2000.00, 999);
5-
6-
INSERT INTO registration.movement_classes (external_id, version, created_on, last_update, active, name, type,
7-
description, budget, id_cost_center)
8-
VALUES ('686f3fa3-6a08-4a5a-8587-d27b94a64097', 0, current_timestamp, current_timestamp, true, 'Conta de Luz',
9-
'EXPENSE',
10-
'Despesas com casa, conta de luz', 500, 999);
11-
12-
INSERT INTO registration.movement_classes (external_id, version, created_on, last_update, active, name, type,
13-
description, budget, id_cost_center)
14-
VALUES ('98cb4961-5cde-46fb-abfd-8461be7d628b', 0, current_timestamp, current_timestamp, true, 'Vendas', 'INCOME',
15-
'Receita com vendas', 3500, 999);
1+
INSERT INTO registration.movement_classes (external_id, version, created_on, last_update, active, name, type,description, budget, id_cost_center)
2+
VALUES ('f21d94d2-d28e-4aa3-b12d-8a520023edd9', 0, current_timestamp, current_timestamp, true, 'Mercado', 'EXPENSE','Despesas no mercado', 2000.00, 999);
3+
4+
INSERT INTO registration.movement_classes (external_id, version, created_on, last_update, active, name, type,description, budget, id_cost_center)
5+
VALUES ('686f3fa3-6a08-4a5a-8587-d27b94a64097', 0, current_timestamp, current_timestamp, true, 'Conta de Luz','EXPENSE','Despesas com casa, conta de luz', 500, 999);
6+
7+
INSERT INTO registration.movement_classes (external_id, version, created_on, last_update, active, name, type,description, budget, id_cost_center)
8+
VALUES ('98cb4961-5cde-46fb-abfd-8461be7d628b', 0, current_timestamp, current_timestamp, true, 'Vendas', 'INCOME','Receita com vendas', 3500, 999);
9+
10+
INSERT INTO registration.movement_classes (external_id, version, created_on, last_update, active, name, type, budget,id_cost_center)
11+
VALUES ('86158792-f34e-4cdf-bce6-44394d645d0d', 0, current_timestamp, current_timestamp, true, 'Manutenção', 'EXPENSE',500, 888);
12+
13+
INSERT INTO registration.movement_classes (external_id, version, created_on, last_update, active, name, type, budget,id_cost_center)
14+
VALUES ('067e62d5-725f-44c6-bfdf-79f9cf19fff8', 0, current_timestamp, current_timestamp, true, 'Aluguel','INCOME', 500, 888);

0 commit comments

Comments
 (0)