Calendar Cycle Calculation Logic closes #276 #284

Merged
moritz merged 3 commits from feature/276_cycle_calculation into main 2025-12-16 16:39:37 +01:00
Showing only changes of commit a7285915e6 - Show all commits

View file

@ -50,14 +50,47 @@ defmodule Mv.MembershipFees.CalendarCycles do
## Parameters
- `date` - The date for which to find the cycle start (used as default if `reference_date` not provided)
- `date` - Ignored in this 3-argument version (kept for API consistency)
moritz marked this conversation as resolved

why for API consistency?

why for API consistency?

Because all other cycle function use the current date as first argument for testing the functions. This is just kept to have consistency between the other functions.

Because all other cycle function use the current date as first argument for testing the functions. This is just kept to have consistency between the other functions.
- `interval` - The interval type (`:monthly`, `:quarterly`, `:half_yearly`, `:yearly`)
- `reference_date` - The reference date to use for calculation (defaults to `date`)
- `reference_date` - The date used to determine which cycle to calculate
## Returns
The start date of the cycle containing the reference date.
## Examples
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-03-15], :monthly, ~D[2024-05-20])
~D[2024-05-01]
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-03-15], :quarterly, ~D[2024-05-20])
~D[2024-04-01]
"""
@spec calculate_cycle_start(Date.t(), interval(), Date.t()) :: Date.t()
def calculate_cycle_start(_date, interval, reference_date) do
case interval do
:monthly -> monthly_cycle_start(reference_date)
:quarterly -> quarterly_cycle_start(reference_date)
:half_yearly -> half_yearly_cycle_start(reference_date)
:yearly -> yearly_cycle_start(reference_date)
end
end
@doc """
Calculates the start date of the cycle that contains the given date.
This is a convenience function that calls `calculate_cycle_start/3` with `date` as both
the input and reference date.
## Parameters
- `date` - The date used to determine which cycle to calculate
- `interval` - The interval type (`:monthly`, `:quarterly`, `:half_yearly`, `:yearly`)
## Returns
The start date of the cycle containing the given date.
## Examples
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-03-15], :monthly)
@ -71,20 +104,7 @@ defmodule Mv.MembershipFees.CalendarCycles do
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-12-15], :yearly)
~D[2024-01-01]
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-03-15], :monthly, ~D[2024-05-20])
~D[2024-05-01]
"""
@spec calculate_cycle_start(Date.t(), interval(), Date.t()) :: Date.t()
def calculate_cycle_start(_date, interval, reference_date) do
case interval do
:monthly -> monthly_cycle_start(reference_date)
:quarterly -> quarterly_cycle_start(reference_date)
:half_yearly -> half_yearly_cycle_start(reference_date)
:yearly -> yearly_cycle_start(reference_date)
end
end
@spec calculate_cycle_start(Date.t(), interval()) :: Date.t()
def calculate_cycle_start(date, interval) do
calculate_cycle_start(date, interval, date)
@ -203,7 +223,13 @@ defmodule Mv.MembershipFees.CalendarCycles do
end
@doc """
Checks if the cycle was just completed (ended before or on the given date, but is the most recent completed cycle).
Checks if the cycle is the last completed cycle.
A cycle is considered the last completed cycle if:
- The cycle has ended (cycle_end < today)
- The next cycle has not ended yet (today <= next_end)
In other words: `cycle_end < today <= next_end`
## Parameters
@ -213,7 +239,7 @@ defmodule Mv.MembershipFees.CalendarCycles do
## Returns
`true` if the cycle ended before or on the given date and is the last completed cycle, `false` otherwise.
`true` if the cycle is the last completed cycle, `false` otherwise.
## Examples
@ -230,11 +256,11 @@ defmodule Mv.MembershipFees.CalendarCycles do
def last_completed_cycle?(cycle_start, interval, today) do
cycle_end = calculate_cycle_end(cycle_start, interval)
# Cycle must have ended (before or on the given date)
# Cycle must have ended (cycle_end < today)
case Date.compare(today, cycle_end) do
:gt ->
# Check if this is the most recent completed cycle
# by verifying that the next cycle hasn't ended yet
# by verifying that the next cycle hasn't ended yet (today <= next_end)
next_start = next_cycle_start(cycle_start, interval)
next_end = calculate_cycle_end(next_start, interval)