Last active
November 24, 2021 20:06
-
-
Save aaiezza/a0f110ec8e4bd0a928c3be363c7c9334 to your computer and use it in GitHub Desktop.
Calculate 401K employer match based on a tiered-contribution-matching system
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package challenge; | |
import static challenge.EmployerRetirementContributionCalculator.EmployeeContribution.employeeContribution; | |
import static challenge.EmployerRetirementContributionCalculator.EmployerContribution.Tier.Percentage.REMAINING_AMOUNT; | |
import challenge.EmployerRetirementContributionCalculator.EmployerContribution.Amount; | |
import challenge.EmployerRetirementContributionCalculator.EmployerContribution.EmployeeContributionMatched; | |
import java.util.List; | |
import java.util.concurrent.atomic.AtomicReference; | |
/* | |
# 401k contribution, employer would match. | |
# | |
# for the first 3% (of the salary) employee contribution percentage, employer match 100% of the contribution; | |
# for the next 2% (of the salary) contribution percentage, employer match 50% of the contribution; | |
# for the remaining contribution percentage, match 25% of the contribution; employer will cap the match to $4000. | |
contributing 10% of his salary -> the salary is $1000 | |
first 3% -> $30 | |
next 2% -> $10 | |
remaining 5% -> $12.5 | |
# 1 data model to capture this employer match data | |
# 2 for you to implement a generic method to calculate the match an employee will get, taking employee's salary, 401k contrinution percentage, and the match model as inputs. | |
*/ | |
class EmployerRetirementContributionCalculator { | |
@lombok.Value | |
public static class Salary { | |
private final double value; | |
} | |
@lombok.Value | |
public static class EmployeeContribution { | |
private final double value; | |
public static EmployeeContribution employeeContribution( | |
final Salary salary, final ContributionPercentage contributionPercentage) { | |
return new EmployeeContribution(salary.getValue() * contributionPercentage.getValue()); | |
} | |
} | |
@lombok.Value | |
public static class ContributionPercentage { | |
private final double value; | |
} | |
@lombok.Value | |
public static class EmployerContribution { | |
private final Amount amount; | |
private final EmployeeContributionMatched amountMatched; | |
public EmployerContribution applyTier( | |
final Tier tier, final Salary salary, final EmployeeContribution employeeContribution) { | |
final var tierAmountToMatch = salary.getValue() * tier.getPercentage().getValue(); | |
final var amountAvailableToMatch = | |
new EmployeeContributionMatched( | |
Math.min( | |
employeeContribution.getValue() - getAmountMatched().getValue(), | |
tierAmountToMatch)); | |
return new EmployerContribution( | |
new Amount( | |
getAmount().getValue() | |
+ (amountAvailableToMatch.getValue() * tier.getMatchPercentage().getValue())), | |
new EmployeeContributionMatched( | |
getAmountMatched().getValue() + amountAvailableToMatch.getValue())); | |
} | |
@lombok.Value | |
public static class Amount { | |
private final double value; | |
} | |
@lombok.Value | |
public static class EmployeeContributionMatched { | |
private final double value; | |
} | |
@lombok.Value | |
public static class Tier { | |
private final Percentage percentage; | |
private final MatchPercentage matchPercentage; | |
public Tier withPercentageRemainingFromTiers(final List<Tier> tiers) { | |
return new Tier( | |
new Percentage( | |
tiers.stream().map(Tier::getPercentage).mapToDouble(Percentage::getValue).sum()), | |
this.matchPercentage); | |
} | |
public boolean tierAppliesToRemainingAmountOfEmployeeContribution() { | |
return getPercentage() == REMAINING_AMOUNT; | |
} | |
@lombok.Value | |
public static class Percentage { | |
public static final Percentage REMAINING_AMOUNT = new Percentage(0); | |
private final double value; | |
} | |
@lombok.Value | |
public static class MatchPercentage { | |
private final double value; | |
} | |
} | |
} | |
public EmployerContribution calculateMatch( | |
final Salary salary, | |
final ContributionPercentage contributionPercentage, | |
final List<EmployerContribution.Tier> tiers) { | |
final var employeeContribution = employeeContribution(salary, contributionPercentage); | |
final AtomicReference<EmployerContribution> employerContribution = | |
new AtomicReference<>( | |
new EmployerContribution(new Amount(0), new EmployeeContributionMatched(0))); | |
tiers.stream() | |
.map( | |
tier -> | |
tier.tierAppliesToRemainingAmountOfEmployeeContribution() | |
? tier.withPercentageRemainingFromTiers(tiers) | |
: tier) | |
.forEach( | |
tier -> | |
employerContribution.set( | |
employerContribution.get().applyTier(tier, salary, employeeContribution))); | |
return employerContribution.get(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package challenge; | |
import static challenge.EmployerRetirementContributionCalculator.EmployerContribution.Tier.Percentage.REMAINING_AMOUNT; | |
import static java.util.Arrays.asList; | |
import static java.util.stream.Collectors.toList; | |
import static org.assertj.core.api.Assertions.assertThat; | |
import static org.junit.jupiter.params.provider.Arguments.arguments; | |
import challenge.EmployerRetirementContributionCalculator.ContributionPercentage; | |
import challenge.EmployerRetirementContributionCalculator.EmployerContribution; | |
import challenge.EmployerRetirementContributionCalculator.EmployerContribution.Tier; | |
import challenge.EmployerRetirementContributionCalculator.EmployerContribution.Tier.MatchPercentage; | |
import challenge.EmployerRetirementContributionCalculator.EmployerContribution.Tier.Percentage; | |
import challenge.EmployerRetirementContributionCalculator.Salary; | |
import java.util.List; | |
import java.util.stream.Stream; | |
import org.junit.jupiter.api.BeforeEach; | |
import org.junit.jupiter.params.ParameterizedTest; | |
import org.junit.jupiter.params.provider.Arguments; | |
import org.junit.jupiter.params.provider.MethodSource; | |
class EmployerRetirementContributionCalculatorTest { | |
private EmployerRetirementContributionCalculator subject; | |
@BeforeEach | |
void setUp() { | |
subject = new EmployerRetirementContributionCalculator(); | |
} | |
@MethodSource | |
@ParameterizedTest(name = "[{index}] {0} : Returns {4}") | |
void shouldCalculateExpectedEmployerContribution( | |
final String description, | |
final Salary salary, | |
final ContributionPercentage contributionPercentage, | |
final List<EmployerContribution.Tier> tiers, | |
final EmployerContribution.Amount expectedMatchAmount) { | |
assertThat(subject.calculateMatch(salary, contributionPercentage, tiers).getAmount()) | |
.isEqualTo(expectedMatchAmount); | |
} | |
static List<Arguments> shouldCalculateExpectedEmployerContribution() { | |
return Stream.of( | |
arguments( | |
"$1000 * 10% | tiers (3%→100%), (2%→50%), (x%→25%)", | |
new Salary(1000.00), | |
new ContributionPercentage(.1), | |
asList( | |
new Tier(new Percentage(.03), new MatchPercentage(1.0)), | |
new Tier(new Percentage(.02), new MatchPercentage(.5)), | |
new Tier(REMAINING_AMOUNT, new MatchPercentage(.25))), | |
new EmployerContribution.Amount(52.5)), | |
arguments( | |
"$1000 * 1% | tiers (3%→100%), (2%→50%), (x%→25%)", | |
new Salary(1000.00), | |
new ContributionPercentage(.01), | |
asList( | |
new Tier(new Percentage(.03), new MatchPercentage(1.0)), | |
new Tier(new Percentage(.02), new MatchPercentage(.5)), | |
new Tier(REMAINING_AMOUNT, new MatchPercentage(.25))), | |
new EmployerContribution.Amount(10.0)), | |
arguments( | |
"$152,000 * 15% | tier (4%→100%)", | |
new Salary(152000.00), | |
new ContributionPercentage(.15), | |
asList(new Tier(new Percentage(.04), new MatchPercentage(1.0))), | |
new EmployerContribution.Amount(6080.00)), | |
arguments( | |
"$1,000 * 1% | no employer match", | |
new Salary(1000.00), | |
new ContributionPercentage(.1), | |
asList(), | |
new EmployerContribution.Amount(0))) | |
.collect(toList()); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment