Skip to content

Commit

Permalink
Hanlde edge case
Browse files Browse the repository at this point in the history
There was a bug in the previous payroll logic that couldn't handle the
following scenario

* Claimant paid for a claim (Claim A) in Jan payroll
* Claimant paid a topup on Claim A in Feb payroll
* Claimant paid for a claim (Claim B) in Feb payroll

In that scenario the claimant would receive the topup payment twice as
both Claim A and Claim B have the same payment and so when we joined
claims to payments and left joined payments to topup both rows would
have a topup, causing the coalesce to pick the topup amount.
  • Loading branch information
rjlynch committed Jan 23, 2025
1 parent 0ae3ea6 commit e7986ec
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 16 deletions.
43 changes: 27 additions & 16 deletions app/models/payroll_run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,31 +56,42 @@ def payments_count
class LineItem < Struct.new(:id, :award_amount, keyword_init: true); end

def line_items(policy, filter: :all)
scope = Claim
.select(
"
DISTINCT(claims.id),
COALESCE(topups.award_amount, eligibilities.award_amount) AS award_amount
"
)
.with_award_amounts
.left_joins(payments: :topups)
.joins(payments: :payroll_run)
.where(payroll_runs: {id: id})
policies = if policy == :all
Policies::POLICIES
else
[policy]
end

scope = scope.by_policy(policy) unless policy == :all
claims_topped_up_in_payroll_run = Claim
.select("claims.id AS id, topups.award_amount AS award_amount")
.joins(topups: :payment)
.where(payments: {payroll_run_id: id})
.by_policies(policies)

claims_without_topups_in_payroll_run = Claim
.select("claims.id AS id, eligibilities.award_amount AS award_amount")
.with_award_amounts
.joins(:payments)
.where(payments: {payroll_run_id: id})
.where.not(id: claims_topped_up_in_payroll_run.reselect(:id))
.by_policies(policies)

case filter
sql = case filter
when :claims
scope = scope.where(topups: {id: nil})
claims_without_topups_in_payroll_run.to_sql
when :topups
scope = scope.where.not(topups: {id: nil})
claims_topped_up_in_payroll_run.to_sql
else
[
claims_without_topups_in_payroll_run.to_sql,
claims_topped_up_in_payroll_run.to_sql
].join("\nUNION\n")
end

# Claim delegates it's award amount to eligibility, so we want to return
# a non claim object ensuring the award amount is from the topup if there
# is one
ActiveRecord::Base.connection.execute(scope.to_sql).map(&LineItem.method(:new))
ActiveRecord::Base.connection.execute(sql).map(&LineItem.method(:new))
end

def ensure_no_payroll_run_this_month
Expand Down
132 changes: 132 additions & 0 deletions spec/models/payroll_run_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,138 @@
).to eq(204) # 103 + 101
end
end

it "handles claims that have topups in multiple payroll runs" do
claim_1 = create(
:claim,
:approved,
policy: Policies::EarlyCareerPayments,
eligibility_attributes: {
award_amount: 1
}
)

claim_2 = create(
:claim,
:approved,
policy: Policies::EarlyCareerPayments,
eligibility_attributes: {
award_amount: 7
},
date_of_birth: claim_1.date_of_birth,
student_loan_plan: claim_1.student_loan_plan,
bank_sort_code: claim_1.bank_sort_code,
bank_account_number: claim_1.bank_account_number,
building_society_roll_number: claim_1.building_society_roll_number,
national_insurance_number: claim_1.national_insurance_number
)

first_payroll_run = nil

travel_to 2.month.ago do
first_payroll_run = create(:payroll_run)

create(
:payment,
claims: [claim_1],
payroll_run: first_payroll_run
)
end

second_payroll_run = nil

travel_to 1.month.ago do
second_payroll_run = create(:payroll_run)

create(
:payment,
claims: [claim_1, claim_2],
topups: [create(:topup, claim: claim_1, award_amount: 3)],
payroll_run: second_payroll_run
)
end

third_payroll_run = create(:payroll_run)

create(
:payment,
claims: [claim_1],
topups: [create(:topup, claim: claim_1, award_amount: 5)],
payroll_run: third_payroll_run
)

expect(
first_payroll_run.total_claim_amount_for_policy(Policies::EarlyCareerPayments)
).to eq(1)

expect(
first_payroll_run.total_claim_amount_for_policy(Policies::EarlyCareerPayments, filter: :claims)
).to eq(1)

expect(
first_payroll_run.total_claim_amount_for_policy(Policies::EarlyCareerPayments, filter: :topups)
).to eq(0)

expect(
first_payroll_run.number_of_claims_for_policy(Policies::EarlyCareerPayments)
).to eq(1)

expect(
first_payroll_run.number_of_claims_for_policy(Policies::EarlyCareerPayments, filter: :claims)
).to eq(1)

expect(
first_payroll_run.number_of_claims_for_policy(Policies::EarlyCareerPayments, filter: :topups)
).to eq(0)

expect(
second_payroll_run.total_claim_amount_for_policy(Policies::EarlyCareerPayments)
).to eq(10) # topup 3 + claim 7

expect(
second_payroll_run.total_claim_amount_for_policy(Policies::EarlyCareerPayments, filter: :claims)
).to eq(7)

expect(
second_payroll_run.total_claim_amount_for_policy(Policies::EarlyCareerPayments, filter: :topups)
).to eq(3)

expect(
second_payroll_run.number_of_claims_for_policy(Policies::EarlyCareerPayments)
).to eq(2) # topup for claim 1 + claim 2

expect(
second_payroll_run.number_of_claims_for_policy(Policies::EarlyCareerPayments, filter: :claims)
).to eq(1)

expect(
second_payroll_run.number_of_claims_for_policy(Policies::EarlyCareerPayments, filter: :topups)
).to eq(1)

expect(
third_payroll_run.total_claim_amount_for_policy(Policies::EarlyCareerPayments)
).to eq(5)

expect(
third_payroll_run.total_claim_amount_for_policy(Policies::EarlyCareerPayments, filter: :claims)
).to eq(0)

expect(
third_payroll_run.total_claim_amount_for_policy(Policies::EarlyCareerPayments, filter: :topups)
).to eq(5)

expect(
third_payroll_run.number_of_claims_for_policy(Policies::EarlyCareerPayments)
).to eq(1)

expect(
third_payroll_run.number_of_claims_for_policy(Policies::EarlyCareerPayments, filter: :claims)
).to eq(0)

expect(
third_payroll_run.number_of_claims_for_policy(Policies::EarlyCareerPayments, filter: :topups)
).to eq(1)
end
end

describe ".this_month" do
Expand Down

0 comments on commit e7986ec

Please sign in to comment.