Calendar Cycle Calculation Logic closes #276 #284
2 changed files with 127 additions and 237 deletions
|
|
@ -2,10 +2,16 @@ defmodule Mv.MembershipFees.CalendarCycles do
|
|||
@moduledoc """
|
||||
Calendar-based cycle calculation functions for membership fees.
|
||||
|
||||
This module provides pure functions for calculating cycle boundaries
|
||||
This module provides functions for calculating cycle boundaries
|
||||
based on interval types (monthly, quarterly, half-yearly, yearly).
|
||||
|
||||
All functions are pure (no side effects) and work with Elixir's `Date` type.
|
||||
The calculation functions (`calculate_cycle_start/3`, `calculate_cycle_end/2`,
|
||||
`next_cycle_start/2`) are pure functions with no side effects.
|
||||
|
||||
The time-dependent functions (`current_cycle?/3`, `last_completed_cycle?/3`)
|
||||
depend on a date parameter for testability. Their 2-argument variants
|
||||
(`current_cycle?/2`, `last_completed_cycle?/2`) use `Date.utc_today()` and
|
||||
are not referentially transparent.
|
||||
|
||||
## Interval Types
|
||||
|
||||
|
|
@ -29,47 +35,61 @@ defmodule Mv.MembershipFees.CalendarCycles do
|
|||
~D[2025-01-01]
|
||||
"""
|
||||
|
||||
@typedoc """
|
||||
Interval type for membership fee cycles.
|
||||
|
||||
- `:monthly` - Monthly cycles (1st to last day of month)
|
||||
- `:quarterly` - Quarterly cycles (1st of quarter to last day of quarter)
|
||||
- `:half_yearly` - Half-yearly cycles (1st of half-year to last day of half-year)
|
||||
- `:yearly` - Yearly cycles (Jan 1st to Dec 31st)
|
||||
"""
|
||||
@type interval :: :monthly | :quarterly | :half_yearly | :yearly
|
||||
|
||||
@doc """
|
||||
Calculates the start date of the cycle that contains the given date.
|
||||
Calculates the start date of the cycle that contains the reference date.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `date` - The date for which to find the cycle start
|
||||
- `date` - The date for which to find the cycle start (used as default if `reference_date` not provided)
|
||||
|
moritz marked this conversation as resolved
|
||||
- `interval` - The interval type (`:monthly`, `:quarterly`, `:half_yearly`, `:yearly`)
|
||||
- `reference_date` - Optional reference date (defaults to `date`)
|
||||
- `reference_date` - The reference date to use for calculation (defaults to `date`)
|
||||
|
||||
## Returns
|
||||
|
||||
The start date of the cycle containing the given date.
|
||||
The start date of the cycle containing the reference date.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> calculate_cycle_start(~D[2024-03-15], :monthly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-03-15], :monthly)
|
||||
~D[2024-03-01]
|
||||
|
||||
iex> calculate_cycle_start(~D[2024-05-15], :quarterly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-05-15], :quarterly)
|
||||
~D[2024-04-01]
|
||||
|
||||
iex> calculate_cycle_start(~D[2024-09-15], :half_yearly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-09-15], :half_yearly)
|
||||
~D[2024-07-01]
|
||||
|
||||
iex> calculate_cycle_start(~D[2024-12-15], :yearly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-12-15], :yearly)
|
||||
~D[2024-01-01]
|
||||
"""
|
||||
@spec calculate_cycle_start(Date.t(), interval(), Date.t() | nil) :: Date.t()
|
||||
def calculate_cycle_start(date, interval, reference_date \\ nil) do
|
||||
reference = reference_date || date
|
||||
|
||||
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)
|
||||
:quarterly -> quarterly_cycle_start(reference)
|
||||
:half_yearly -> half_yearly_cycle_start(reference)
|
||||
:yearly -> yearly_cycle_start(reference)
|
||||
: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)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the end date of a cycle based on its start date and interval.
|
||||
|
||||
|
|
@ -84,19 +104,19 @@ defmodule Mv.MembershipFees.CalendarCycles do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> calculate_cycle_end(~D[2024-03-01], :monthly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-03-01], :monthly)
|
||||
~D[2024-03-31]
|
||||
|
||||
iex> calculate_cycle_end(~D[2024-02-01], :monthly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-02-01], :monthly)
|
||||
~D[2024-02-29]
|
||||
|
||||
iex> calculate_cycle_end(~D[2024-01-01], :quarterly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-01-01], :quarterly)
|
||||
~D[2024-03-31]
|
||||
|
||||
iex> calculate_cycle_end(~D[2024-01-01], :half_yearly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-01-01], :half_yearly)
|
||||
~D[2024-06-30]
|
||||
|
||||
iex> calculate_cycle_end(~D[2024-01-01], :yearly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-01-01], :yearly)
|
||||
~D[2024-12-31]
|
||||
"""
|
||||
@spec calculate_cycle_end(Date.t(), interval()) :: Date.t()
|
||||
|
|
@ -123,16 +143,16 @@ defmodule Mv.MembershipFees.CalendarCycles do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> next_cycle_start(~D[2024-01-01], :monthly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(~D[2024-01-01], :monthly)
|
||||
~D[2024-02-01]
|
||||
|
||||
iex> next_cycle_start(~D[2024-01-01], :quarterly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(~D[2024-01-01], :quarterly)
|
||||
~D[2024-04-01]
|
||||
|
||||
iex> next_cycle_start(~D[2024-01-01], :half_yearly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(~D[2024-01-01], :half_yearly)
|
||||
~D[2024-07-01]
|
||||
|
||||
iex> next_cycle_start(~D[2024-01-01], :yearly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(~D[2024-01-01], :yearly)
|
||||
~D[2025-01-01]
|
||||
"""
|
||||
@spec next_cycle_start(Date.t(), interval()) :: Date.t()
|
||||
|
|
@ -143,63 +163,74 @@ defmodule Mv.MembershipFees.CalendarCycles do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Checks if the cycle contains today's date.
|
||||
Checks if the cycle contains the given date.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `cycle_start` - The start date of the cycle
|
||||
- `interval` - The interval type
|
||||
- `today` - The date to check (defaults to today's date)
|
||||
|
||||
## Returns
|
||||
|
||||
`true` if today is within the cycle, `false` otherwise.
|
||||
`true` if the given date is within the cycle, `false` otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
# Assuming today is 2024-03-15
|
||||
iex> current_cycle?(~D[2024-03-01], :monthly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.current_cycle?(~D[2024-03-01], :monthly, ~D[2024-03-15])
|
||||
true
|
||||
|
||||
iex> current_cycle?(~D[2024-02-01], :monthly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.current_cycle?(~D[2024-02-01], :monthly, ~D[2024-03-15])
|
||||
false
|
||||
|
||||
iex> Mv.MembershipFees.CalendarCycles.current_cycle?(~D[2024-03-01], :monthly, ~D[2024-03-01])
|
||||
true
|
||||
|
||||
iex> Mv.MembershipFees.CalendarCycles.current_cycle?(~D[2024-03-01], :monthly, ~D[2024-03-31])
|
||||
true
|
||||
"""
|
||||
@spec current_cycle?(Date.t(), interval()) :: boolean()
|
||||
def current_cycle?(cycle_start, interval) do
|
||||
today = Date.utc_today()
|
||||
@spec current_cycle?(Date.t(), interval(), Date.t()) :: boolean()
|
||||
def current_cycle?(cycle_start, interval, today) do
|
||||
cycle_end = calculate_cycle_end(cycle_start, interval)
|
||||
|
||||
Date.compare(cycle_start, today) in [:lt, :eq] and
|
||||
Date.compare(today, cycle_end) in [:lt, :eq]
|
||||
end
|
||||
|
||||
@spec current_cycle?(Date.t(), interval()) :: boolean()
|
||||
def current_cycle?(cycle_start, interval) do
|
||||
current_cycle?(cycle_start, interval, Date.utc_today())
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the cycle was just completed (ended yesterday or earlier, but is the most recent completed cycle).
|
||||
Checks if the cycle was just completed (ended before or on the given date, but is the most recent completed cycle).
|
||||
|
||||
## Parameters
|
||||
|
||||
- `cycle_start` - The start date of the cycle
|
||||
- `interval` - The interval type
|
||||
- `today` - The date to check against (defaults to today's date)
|
||||
|
||||
## Returns
|
||||
|
||||
`true` if the cycle ended yesterday or earlier and is the last completed cycle, `false` otherwise.
|
||||
`true` if the cycle ended before or on the given date and is the last completed cycle, `false` otherwise.
|
||||
|
||||
## Examples
|
||||
|
||||
# Assuming today is 2024-04-01 (cycle ended yesterday)
|
||||
iex> last_completed_cycle?(~D[2024-03-01], :monthly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.last_completed_cycle?(~D[2024-03-01], :monthly, ~D[2024-04-01])
|
||||
true
|
||||
|
||||
# Assuming today is 2024-03-15 (cycle is still current)
|
||||
iex> last_completed_cycle?(~D[2024-03-01], :monthly)
|
||||
iex> Mv.MembershipFees.CalendarCycles.last_completed_cycle?(~D[2024-03-01], :monthly, ~D[2024-03-15])
|
||||
false
|
||||
|
||||
iex> Mv.MembershipFees.CalendarCycles.last_completed_cycle?(~D[2024-02-01], :monthly, ~D[2024-04-15])
|
||||
false
|
||||
"""
|
||||
@spec last_completed_cycle?(Date.t(), interval()) :: boolean()
|
||||
def last_completed_cycle?(cycle_start, interval) do
|
||||
today = Date.utc_today()
|
||||
@spec last_completed_cycle?(Date.t(), interval(), Date.t()) :: boolean()
|
||||
def last_completed_cycle?(cycle_start, interval, today) do
|
||||
cycle_end = calculate_cycle_end(cycle_start, interval)
|
||||
|
||||
# Cycle must have ended (yesterday or earlier)
|
||||
# Cycle must have ended (before or on the given date)
|
||||
case Date.compare(today, cycle_end) do
|
||||
:gt ->
|
||||
# Check if this is the most recent completed cycle
|
||||
|
|
@ -214,6 +245,11 @@ defmodule Mv.MembershipFees.CalendarCycles do
|
|||
end
|
||||
end
|
||||
|
||||
@spec last_completed_cycle?(Date.t(), interval()) :: boolean()
|
||||
def last_completed_cycle?(cycle_start, interval) do
|
||||
last_completed_cycle?(cycle_start, interval, Date.utc_today())
|
||||
end
|
||||
|
||||
# Private helper functions
|
||||
|
||||
defp monthly_cycle_start(date) do
|
||||
|
|
|
|||
|
|
@ -6,53 +6,9 @@ defmodule Mv.MembershipFees.CalendarCyclesTest do
|
|||
|
||||
alias Mv.MembershipFees.CalendarCycles
|
||||
|
||||
doctest Mv.MembershipFees.CalendarCycles
|
||||
|
||||
describe "calculate_cycle_start/3" do
|
||||
test "monthly: returns 1st of month for any date" do
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-03-15], :monthly) == ~D[2024-03-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-03-31], :monthly) == ~D[2024-03-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-03-01], :monthly) == ~D[2024-03-01]
|
||||
end
|
||||
|
||||
test "quarterly: returns 1st of quarter (Jan/Apr/Jul/Oct)" do
|
||||
# Q1 (Jan-Mar)
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-01-15], :quarterly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-02-15], :quarterly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-03-15], :quarterly) == ~D[2024-01-01]
|
||||
|
||||
# Q2 (Apr-Jun)
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-04-15], :quarterly) == ~D[2024-04-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-05-15], :quarterly) == ~D[2024-04-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-06-15], :quarterly) == ~D[2024-04-01]
|
||||
|
||||
# Q3 (Jul-Sep)
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-07-15], :quarterly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-08-15], :quarterly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-09-15], :quarterly) == ~D[2024-07-01]
|
||||
|
||||
# Q4 (Oct-Dec)
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-10-15], :quarterly) == ~D[2024-10-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-11-15], :quarterly) == ~D[2024-10-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-12-15], :quarterly) == ~D[2024-10-01]
|
||||
end
|
||||
|
||||
test "half_yearly: returns 1st of half (Jan/Jul)" do
|
||||
# First half (Jan-Jun)
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-01-15], :half_yearly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-03-15], :half_yearly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-06-15], :half_yearly) == ~D[2024-01-01]
|
||||
|
||||
# Second half (Jul-Dec)
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-07-15], :half_yearly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-09-15], :half_yearly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-12-15], :half_yearly) == ~D[2024-07-01]
|
||||
end
|
||||
|
||||
test "yearly: returns 1st of January" do
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-01-15], :yearly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-06-15], :yearly) == ~D[2024-01-01]
|
||||
assert CalendarCycles.calculate_cycle_start(~D[2024-12-15], :yearly) == ~D[2024-01-01]
|
||||
end
|
||||
|
||||
test "uses reference_date when provided" do
|
||||
date = ~D[2024-03-15]
|
||||
reference = ~D[2024-05-20]
|
||||
|
|
@ -62,178 +18,76 @@ defmodule Mv.MembershipFees.CalendarCyclesTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "calculate_cycle_end/2" do
|
||||
test "monthly: returns last day of month" do
|
||||
# 31-day month
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-03-01], :monthly) == ~D[2024-03-31]
|
||||
|
||||
# 30-day month
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-04-01], :monthly) == ~D[2024-04-30]
|
||||
|
||||
# February in leap year
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-02-01], :monthly) == ~D[2024-02-29]
|
||||
|
||||
# February in non-leap year
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2023-02-01], :monthly) == ~D[2023-02-28]
|
||||
end
|
||||
|
||||
test "quarterly: returns last day of quarter" do
|
||||
# Q1: Jan-Mar -> Mar 31
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :quarterly) == ~D[2024-03-31]
|
||||
|
||||
# Q2: Apr-Jun -> Jun 30
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-04-01], :quarterly) == ~D[2024-06-30]
|
||||
|
||||
# Q3: Jul-Sep -> Sep 30
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-07-01], :quarterly) == ~D[2024-09-30]
|
||||
|
||||
# Q4: Oct-Dec -> Dec 31
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-10-01], :quarterly) == ~D[2024-12-31]
|
||||
end
|
||||
|
||||
test "half_yearly: returns last day of half-year" do
|
||||
# First half: Jan-Jun -> Jun 30
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :half_yearly) == ~D[2024-06-30]
|
||||
|
||||
# Second half: Jul-Dec -> Dec 31
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-07-01], :half_yearly) == ~D[2024-12-31]
|
||||
end
|
||||
|
||||
test "yearly: returns Dec 31" do
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :yearly) == ~D[2024-12-31]
|
||||
assert CalendarCycles.calculate_cycle_end(~D[2023-01-01], :yearly) == ~D[2023-12-31]
|
||||
end
|
||||
end
|
||||
|
||||
describe "next_cycle_start/2" do
|
||||
test "monthly: adds one month" do
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-01-01], :monthly) == ~D[2024-02-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-02-01], :monthly) == ~D[2024-03-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-12-01], :monthly) == ~D[2025-01-01]
|
||||
end
|
||||
|
||||
test "quarterly: adds three months" do
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-01-01], :quarterly) == ~D[2024-04-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-04-01], :quarterly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-07-01], :quarterly) == ~D[2024-10-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-10-01], :quarterly) == ~D[2025-01-01]
|
||||
end
|
||||
|
||||
test "half_yearly: adds six months" do
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-01-01], :half_yearly) == ~D[2024-07-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-07-01], :half_yearly) == ~D[2025-01-01]
|
||||
end
|
||||
|
||||
test "yearly: adds one year" do
|
||||
assert CalendarCycles.next_cycle_start(~D[2024-01-01], :yearly) == ~D[2025-01-01]
|
||||
assert CalendarCycles.next_cycle_start(~D[2023-01-01], :yearly) == ~D[2024-01-01]
|
||||
end
|
||||
end
|
||||
|
||||
describe "current_cycle?/2" do
|
||||
test "returns true when today is within cycle" do
|
||||
today = Date.utc_today()
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
||||
|
||||
assert CalendarCycles.current_cycle?(cycle_start, :monthly) == true
|
||||
end
|
||||
|
||||
test "returns true when today equals cycle start" do
|
||||
today = Date.utc_today()
|
||||
cycle_start = today
|
||||
|
||||
# For monthly, if today is the 1st, it's the cycle start
|
||||
if today.day == 1 do
|
||||
assert CalendarCycles.current_cycle?(cycle_start, :monthly) == true
|
||||
end
|
||||
end
|
||||
|
||||
test "returns true when today equals cycle end" do
|
||||
today = Date.utc_today()
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
||||
cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, :monthly)
|
||||
|
||||
# If today is the last day of the month, it's the cycle end
|
||||
if today == cycle_end do
|
||||
assert CalendarCycles.current_cycle?(cycle_start, :monthly) == true
|
||||
end
|
||||
end
|
||||
|
||||
test "returns false when today is before cycle start" do
|
||||
future_date = Date.add(Date.utc_today(), 35)
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(future_date, :monthly)
|
||||
|
||||
assert CalendarCycles.current_cycle?(cycle_start, :monthly) == false
|
||||
end
|
||||
|
||||
test "returns false when today is after cycle end" do
|
||||
past_date = Date.add(Date.utc_today(), -35)
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(past_date, :monthly)
|
||||
|
||||
assert CalendarCycles.current_cycle?(cycle_start, :monthly) == false
|
||||
end
|
||||
describe "current_cycle?/3" do
|
||||
# Basic examples are covered by doctests
|
||||
|
||||
test "works for all interval types" do
|
||||
today = Date.utc_today()
|
||||
today = ~D[2024-03-15]
|
||||
|
||||
for interval <- [:monthly, :quarterly, :half_yearly, :yearly] do
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, interval)
|
||||
result = CalendarCycles.current_cycle?(cycle_start, interval)
|
||||
result = CalendarCycles.current_cycle?(cycle_start, interval, today)
|
||||
|
||||
assert result == true, "Expected current cycle for #{interval} with start #{cycle_start}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "last_completed_cycle?/2" do
|
||||
test "returns true when cycle ended yesterday" do
|
||||
yesterday = Date.add(Date.utc_today(), -1)
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(yesterday, :monthly)
|
||||
cycle_end = CalendarCycles.calculate_cycle_end(cycle_start, :monthly)
|
||||
|
||||
# Only test if yesterday was actually the cycle end
|
||||
if yesterday == cycle_end do
|
||||
assert CalendarCycles.last_completed_cycle?(cycle_start, :monthly) == true
|
||||
end
|
||||
end
|
||||
|
||||
test "returns false when cycle is still current" do
|
||||
describe "current_cycle?/2 wrapper" do
|
||||
test "calls current_cycle?/3 with Date.utc_today()" do
|
||||
today = Date.utc_today()
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
||||
|
||||
assert CalendarCycles.last_completed_cycle?(cycle_start, :monthly) == false
|
||||
# This test verifies the wrapper works, but uses actual today
|
||||
# The real testing happens in current_cycle?/3 tests above
|
||||
result = CalendarCycles.current_cycle?(cycle_start, :monthly)
|
||||
|
||||
assert result == true
|
||||
end
|
||||
end
|
||||
|
||||
test "returns false when cycle is in the future" do
|
||||
future_date = Date.add(Date.utc_today(), 35)
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(future_date, :monthly)
|
||||
|
||||
assert CalendarCycles.last_completed_cycle?(cycle_start, :monthly) == false
|
||||
end
|
||||
describe "last_completed_cycle?/3" do
|
||||
# Basic examples are covered by doctests
|
||||
|
||||
test "returns false when next cycle has also ended" do
|
||||
# Use a date from two cycles ago
|
||||
past_date = Date.add(Date.utc_today(), -65)
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(past_date, :monthly)
|
||||
# Two cycles ago: cycle ended, but next cycle also ended
|
||||
today = ~D[2024-05-15]
|
||||
cycle_start = ~D[2024-03-01]
|
||||
# Cycle ended 2024-03-31, next cycle ended 2024-04-30, today is 2024-05-15
|
||||
|
||||
assert CalendarCycles.last_completed_cycle?(cycle_start, :monthly) == false
|
||||
assert CalendarCycles.last_completed_cycle?(cycle_start, :monthly, today) == false
|
||||
end
|
||||
|
||||
test "works correctly for quarterly intervals" do
|
||||
# Test with a known past quarter
|
||||
# Q1 2024 ended on 2024-03-31
|
||||
# Q2 2024 ends on 2024-06-30
|
||||
# Today is 2024-04-15 (after Q1 ended, before Q2 ended)
|
||||
today = ~D[2024-04-15]
|
||||
past_quarter_start = ~D[2024-01-01]
|
||||
|
||||
assert CalendarCycles.last_completed_cycle?(past_quarter_start, :quarterly, today) == true
|
||||
end
|
||||
|
||||
test "returns false when cycle ended on the given date" do
|
||||
# Cycle ends on today, so it's still current, not completed
|
||||
today = ~D[2024-03-31]
|
||||
cycle_start = ~D[2024-03-01]
|
||||
|
||||
assert CalendarCycles.last_completed_cycle?(cycle_start, :monthly, today) == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "last_completed_cycle?/2 wrapper" do
|
||||
test "calls last_completed_cycle?/3 with Date.utc_today()" do
|
||||
today = Date.utc_today()
|
||||
cycle_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
||||
|
||||
if Date.compare(today, CalendarCycles.calculate_cycle_end(past_quarter_start, :quarterly)) ==
|
||||
:gt do
|
||||
# Check if next quarter hasn't ended yet
|
||||
next_quarter_start = CalendarCycles.next_cycle_start(past_quarter_start, :quarterly)
|
||||
next_quarter_end = CalendarCycles.calculate_cycle_end(next_quarter_start, :quarterly)
|
||||
# This test verifies the wrapper works, but uses actual today
|
||||
# The real testing happens in last_completed_cycle?/3 tests above
|
||||
result = CalendarCycles.last_completed_cycle?(cycle_start, :monthly)
|
||||
|
||||
if Date.compare(today, next_quarter_end) in [:lt, :eq] do
|
||||
assert CalendarCycles.last_completed_cycle?(past_quarter_start, :quarterly) == true
|
||||
end
|
||||
end
|
||||
# Result depends on actual today, so we just verify it's a boolean
|
||||
assert is_boolean(result)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue
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.