refactor: reduce nesting depth and improve code readability
All checks were successful
continuous-integration/drone/push Build is passing
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:
parent
06324d77c5
commit
e9c53cc520
3 changed files with 146 additions and 114 deletions
|
|
@ -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, ¤t_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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue