Before we go deep into the process, we decided to take a high-level look at the existing code. We observe following points:
It was possible to untangle existing billing calculation logic from orchestration logic which would give us better code readability and the possibility to implement detailed automation testing scenarios by using pure functions.
There was a possibility to enhance calculation logic by modularizing it which will be easier to maintain in the longer run.
Let me share how iteratively pseudo codes were generated.
Note: You might find the length of the pseudo-code is larger, as I have shared the original pseudo-code as it is. In case you want to skip that part, you can directly jump to key learning section at the end.
Pseudo code version 1:
save or submit
extract List<TimeBasedBillingInfo> BillingInfoForWorkers from incoming dto
get allocation detail (hours, roles) for these workers from external system
get leave data for workers from external system
get workpack (contract) for this project
TimeBased Engagement
workpackPotentialWorkers = Potential workers to fetch from workpack.
parameters coming from BillingInfoForWorkers
- workerId, workerRole, netBillableHours.
worker list to update
- if (BillingInfoForWorkers.get(workerId) != null) {
if (workpackPotentialWorkers. get Role (BillingInfoForWorkers.get(workerId).role) == null) {
// return error -> role is not defined at workpack level
}
worker. set role (BillingInfoForWorkers.get(workerId).role);
worker.netBillableHours = BillingInfoForWorkers.get(workerId).billableHours;
}
worker.set leave days;
// handle some of the constraint cases for worker. // refer workpack aggregate - TBD - possible to reuse workpack aggregate logic for projected amount calculation?
// as of now, constraints are not considered.
// set Total amount for worker
worker.set Total amount (workpackPotentialWorkers. get (worker.role). rate * worker. getBillableHours() )
// handle constraint cases based on amount. // refer workpack aggregate
return worker list.
// get projected value from workpack.
-------------
sync
create List<TimeBasedBillingInfo> BillingInfoForWorkers from read model
- mostly same logic of save/submit should work.
--------------
get milestone detail.
create List<TimeBasedBillingInfo> BillingInfoForWorkers from read model
get Info from external system and calcualte - before timesheet submission
Directly use read model -> after timesheet submission
- mostly same logic of save/submit should work
This was the first version of the pseudo-code we generated. Though, it was missing a lot detail. Also, it looks shabby against the actual code standards, right? But it served multiple purposes.
We started to see the requirements from an abstract perspective. We could see that most of the logic can be reused across – save/submit/sync and get features.
We could focus on business steps to implement. In fact, while writing this, we realized that the scenario for constraint was not covered in the old code. And we also thought that it would be possible to use that logic from workpack (basically a contract).
We also gave thought to using the right type of collections (based on usage patterns).
For many places, we didn’t have a clue what to consider. But that kind of uncertainty is fine. Also deliberately, we didn’t consider all the scenarios so that we don’t spend too much time on the first version, and can improve things iteratively.
Pseudo code version 1.1:
Next steps:
Check logic for assignment driven engagement types for save and submit timesheet. Enhance pseudo code based on that.
For TimeBased Engagement:
Check logic of how projected amount was calculated for milestones of workpack. It might be possible to simplify and reuse the logic.
check if we can remove BillableHours, NonBillableHours and ResponseHours classes. and can convert “abstract class Hours” into “class BillableNonBillableHours”.
Check if using maps with worker Id as key can be helpful. it can remove iterations.
Now, this was interesting. instead of adding logic straight away, we thought about pending points (from pseudo-code perspective). And also, ideas such as using maps at some places instead of list types or removing unnecessary created classes.
In the next iteration of pseudo-code, we added pseudo code for “Assignment based” engagement. We also found out what logic can be common among both engagement types and what can be specific one.
Workpack Calculation Pseudo Code Version 1:
save/submit timesheet API
common method for Projected amount and Actual amount calculation for Time based engagement type
-> use existing method with some refactoring
getProjectedAmountForTimebased(ZonedDateTime milestoneStartDate,
Map<Duration, Integer> effectiveDaysMap, List<TimeBasedProjectedWorker> projectedWorkers, BillingConstraints billingConstraint)
method from workpackDomainService (relocate this method from TimebasedBillingConfiguration class to workpackDomainService)
- after relocating, call this method from workpackDomainService into TMBillingConfiguration class
-> Method refactring :
- return a valueObject which contains (Double amount, List<WorkerBillingInfo>)
- WorkerBillingInfo contains : workerId, totalCost, netBillableHours(as per billingConstraint)
-> For Milestone Projected Amount: the same method will be called from
: TMBillingConfiguration -> getProjectedAmount(
ZonedDateTime milestoneStartDate,
Map<Duration, Integer> effectiveDaysMap) method
- from the returned valueObject,
amount can be used as value of ProjectedAmount
-> For Milestone Actual Amount: call this method from milestoneAggregate -> Save/Submit Command
- Create effectiveDaysMap using values for workers based on leave and allcoated data
Because for amount calculation, we thought that workpack logic of projected amount calculation can be reused with refactoring, we started to look into that. That’s why a separate version was created.
This is not the final version and we iterate further to make things better. and eventually, we could use the common logic in both places.
Once we found that the pseudo-code is mature enough (not 100% completed but enough to start with code updation), we started to make changes to the actual code and as and when required, we updated the pseudo-code too.