refactor: reduce nesting depth and improve code readability
All checks were successful
continuous-integration/drone/push Build is passing

- Replace Enum.map |> Enum.join with Enum.map_join for efficiency
- Extract helper functions to reduce nesting depth from 4 to 2
- Rename is_current_cycle? to current_cycle? following Elixir conventions
This commit is contained in:
Moritz 2025-12-15 11:50:08 +01:00
parent 06324d77c5
commit e9c53cc520
3 changed files with 146 additions and 114 deletions

View file

@ -607,7 +607,14 @@ defmodule Mv.Membership.Member do
cycles = Map.get(member, :membership_fee_cycles) cycles = Map.get(member, :membership_fee_cycles)
if is_list(cycles) and cycles != [] do if is_list(cycles) and cycles != [] do
Enum.find(cycles, fn cycle -> Enum.find(cycles, &current_cycle?(&1, today))
else
nil
end
end
# Checks if a cycle is the current cycle (active today)
defp current_cycle?(cycle, today) do
case Map.get(cycle, :membership_fee_type) do case Map.get(cycle, :membership_fee_type) do
%{interval: interval} -> %{interval: interval} ->
cycle_end = cycle_end =
@ -619,10 +626,6 @@ defmodule Mv.Membership.Member do
_ -> _ ->
false false
end end
end)
else
nil
end
end end
@doc false @doc false
@ -634,30 +637,40 @@ defmodule Mv.Membership.Member do
if is_list(cycles) and cycles != [] do if is_list(cycles) and cycles != [] do
cycles cycles
|> Enum.filter(fn cycle -> |> filter_completed_cycles(today)
|> sort_cycles_by_end_date()
|> List.first()
else
nil
end
end
# Filters cycles that have ended (cycle_end < today)
defp filter_completed_cycles(cycles, today) do
Enum.filter(cycles, fn cycle ->
case Map.get(cycle, :membership_fee_type) do case Map.get(cycle, :membership_fee_type) do
%{interval: interval} -> %{interval: interval} ->
cycle_end = cycle_end =
Mv.MembershipFees.CalendarCycles.calculate_cycle_end(cycle.cycle_start, interval) Mv.MembershipFees.CalendarCycles.calculate_cycle_end(cycle.cycle_start, interval)
# Cycle must have ended (cycle_end < today)
Date.compare(today, cycle_end) == :gt Date.compare(today, cycle_end) == :gt
_ -> _ ->
false false
end end
end) end)
|> Enum.sort_by( end
# Sorts cycles by end date in descending order
defp sort_cycles_by_end_date(cycles) do
Enum.sort_by(
cycles,
fn cycle -> fn cycle ->
interval = Map.get(cycle, :membership_fee_type).interval interval = Map.get(cycle, :membership_fee_type).interval
Mv.MembershipFees.CalendarCycles.calculate_cycle_end(cycle.cycle_start, interval) Mv.MembershipFees.CalendarCycles.calculate_cycle_end(cycle.cycle_start, interval)
end, end,
{:desc, Date} {:desc, Date}
) )
|> List.first()
else
nil
end
end end
@doc false @doc false
@ -668,6 +681,14 @@ defmodule Mv.Membership.Member do
cycles = Map.get(member, :membership_fee_cycles) cycles = Map.get(member, :membership_fee_cycles)
if is_list(cycles) and cycles != [] do if is_list(cycles) and cycles != [] do
filter_overdue_cycles(cycles, today)
else
[]
end
end
# Filters cycles that are unpaid and have ended (cycle_end < today)
defp filter_overdue_cycles(cycles, today) do
Enum.filter(cycles, fn cycle -> Enum.filter(cycles, fn cycle ->
case Map.get(cycle, :membership_fee_type) do case Map.get(cycle, :membership_fee_type) do
%{interval: interval} -> %{interval: interval} ->
@ -680,18 +701,11 @@ defmodule Mv.Membership.Member do
false false
end end
end) end)
else
[]
end
end end
# Regenerates cycles when membership fee type changes # Regenerates cycles when membership fee type changes
# Deletes future unpaid cycles and regenerates them with the new type/amount # Deletes future unpaid cycles and regenerates them with the new type/amount
defp regenerate_cycles_on_type_change(member) do defp regenerate_cycles_on_type_change(member) do
alias Mv.MembershipFees.MembershipFeeCycle
alias Mv.MembershipFees.CycleGenerator
alias Mv.MembershipFees.CalendarCycles
require Ash.Query require Ash.Query
today = Date.utc_today() today = Date.utc_today()
@ -699,58 +713,71 @@ defmodule Mv.Membership.Member do
# Find all unpaid cycles for this member # Find all unpaid cycles for this member
# We need to check cycle_end for each cycle using its own interval # We need to check cycle_end for each cycle using its own interval
all_unpaid_cycles_query = all_unpaid_cycles_query =
MembershipFeeCycle Mv.MembershipFees.MembershipFeeCycle
|> Ash.Query.filter(member_id == ^member.id) |> Ash.Query.filter(member_id == ^member.id)
|> Ash.Query.filter(status == :unpaid) |> Ash.Query.filter(status == :unpaid)
|> Ash.Query.load([:membership_fee_type]) |> Ash.Query.load([:membership_fee_type])
case Ash.read(all_unpaid_cycles_query) do case Ash.read(all_unpaid_cycles_query) do
{:ok, all_unpaid_cycles} -> {:ok, all_unpaid_cycles} ->
# Filter cycles that haven't ended yet (cycle_end >= today) cycles_to_delete = filter_future_cycles(all_unpaid_cycles, today)
delete_and_regenerate_cycles(cycles_to_delete, member.id)
{:error, reason} ->
{:error, reason}
end
end
# Filters cycles that haven't ended yet (cycle_end >= today)
# These are the "future" cycles that should be regenerated # These are the "future" cycles that should be regenerated
# Use each cycle's own interval to calculate cycle_end defp filter_future_cycles(all_unpaid_cycles, today) do
cycles_to_delete =
Enum.filter(all_unpaid_cycles, fn cycle -> Enum.filter(all_unpaid_cycles, fn cycle ->
case cycle.membership_fee_type do case cycle.membership_fee_type do
%{interval: interval} -> %{interval: interval} ->
cycle_end = CalendarCycles.calculate_cycle_end(cycle.cycle_start, interval) cycle_end =
Mv.MembershipFees.CalendarCycles.calculate_cycle_end(cycle.cycle_start, interval)
Date.compare(today, cycle_end) in [:lt, :eq] Date.compare(today, cycle_end) in [:lt, :eq]
_ -> _ ->
false false
end end
end) end)
end
# Delete future unpaid cycles # Deletes future cycles and regenerates them with the new type/amount
defp delete_and_regenerate_cycles(cycles_to_delete, member_id) do
if Enum.empty?(cycles_to_delete) do if Enum.empty?(cycles_to_delete) do
# No cycles to delete, just regenerate # No cycles to delete, just regenerate
case CycleGenerator.generate_cycles_for_member(member.id) do regenerate_cycles(member_id)
{:ok, _cycles} -> :ok else
case delete_cycles(cycles_to_delete) do
:ok -> regenerate_cycles(member_id)
{:error, reason} -> {:error, reason} {:error, reason} -> {:error, reason}
end end
else end
end
# Deletes cycles and returns :ok if all succeeded, {:error, reason} otherwise
defp delete_cycles(cycles_to_delete) do
delete_results = delete_results =
Enum.map(cycles_to_delete, fn cycle -> Enum.map(cycles_to_delete, fn cycle ->
Ash.destroy(cycle) Ash.destroy(cycle)
end) end)
# Check if any deletions failed
if Enum.any?(delete_results, &match?({:error, _}, &1)) do if Enum.any?(delete_results, &match?({:error, _}, &1)) do
{:error, :deletion_failed} {:error, :deletion_failed}
else else
# Regenerate cycles with new type/amount :ok
# CycleGenerator uses its own transaction with advisory lock
# It will reload the member, so it will see the deleted cycles are gone
# and the new membership_fee_type_id
case CycleGenerator.generate_cycles_for_member(member.id) do
{:ok, _cycles} -> :ok
{:error, reason} -> {:error, reason}
end
end end
end end
{:error, reason} -> # Regenerates cycles with new type/amount
{:error, reason} # CycleGenerator uses its own transaction with advisory lock
defp regenerate_cycles(member_id) do
case Mv.MembershipFees.CycleGenerator.generate_cycles_for_member(member_id) do
{:ok, _cycles} -> :ok
{:error, reason} -> {:error, reason}
end end
end end

View file

@ -37,15 +37,23 @@ defmodule Mv.MembershipFees.Changes.ValidateSameInterval do
current_type_id = get_current_type_id(changeset) current_type_id = get_current_type_id(changeset)
new_type_id = get_new_type_id(changeset) new_type_id = get_new_type_id(changeset)
cond do
# If no current type, allow any change (first assignment) # If no current type, allow any change (first assignment)
if is_nil(current_type_id) do is_nil(current_type_id) ->
changeset changeset
else
# If new type is nil, that's allowed (removing type) # If new type is nil, that's allowed (removing type)
if is_nil(new_type_id) do is_nil(new_type_id) ->
changeset changeset
else
# Both types exist - validate intervals match # Both types exist - validate intervals match
true ->
validate_intervals_match(changeset, current_type_id, new_type_id)
end
end
# Validates that intervals match when both types exist
defp validate_intervals_match(changeset, current_type_id, new_type_id) do
case get_intervals(current_type_id, new_type_id) do case get_intervals(current_type_id, new_type_id) do
{:ok, current_interval, new_interval} -> {:ok, current_interval, new_interval} ->
if current_interval == new_interval do if current_interval == new_interval do
@ -60,8 +68,6 @@ defmodule Mv.MembershipFees.Changes.ValidateSameInterval do
changeset changeset
end end
end end
end
end
# Get current type ID from changeset data # Get current type ID from changeset data
defp get_current_type_id(changeset) do defp get_current_type_id(changeset) do

View file

@ -215,8 +215,7 @@ defmodule Mv.MembershipFees.Changes.ValidateSameIntervalTest do
defp extract_error_message(%Ash.Error.Invalid{errors: errors}) do defp extract_error_message(%Ash.Error.Invalid{errors: errors}) do
errors errors
|> Enum.filter(&(&1.field == :membership_fee_type_id)) |> Enum.filter(&(&1.field == :membership_fee_type_id))
|> Enum.map(& &1.message) |> Enum.map_join(" ", & &1.message)
|> Enum.join(" ")
end end
end end
end end