All checks were successful
continuous-integration/drone/push Build is passing
Add CalendarCycles module with functions for all interval types. Includes comprehensive tests for edge cases.
327 lines
14 KiB
Elixir
327 lines
14 KiB
Elixir
defmodule Mv.MembershipFees.CalendarCyclesTest do
|
|
@moduledoc """
|
|
Tests for CalendarCycles module.
|
|
"""
|
|
use ExUnit.Case, async: true
|
|
|
|
alias 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]
|
|
|
|
assert CalendarCycles.calculate_cycle_start(date, :monthly, reference) == ~D[2024-05-01]
|
|
assert CalendarCycles.calculate_cycle_start(date, :quarterly, reference) == ~D[2024-04-01]
|
|
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
|
|
|
|
test "works for all interval types" do
|
|
today = Date.utc_today()
|
|
|
|
for interval <- [:monthly, :quarterly, :half_yearly, :yearly] do
|
|
cycle_start = CalendarCycles.calculate_cycle_start(today, interval)
|
|
result = CalendarCycles.current_cycle?(cycle_start, interval)
|
|
|
|
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
|
|
today = Date.utc_today()
|
|
cycle_start = CalendarCycles.calculate_cycle_start(today, :monthly)
|
|
|
|
assert CalendarCycles.last_completed_cycle?(cycle_start, :monthly) == false
|
|
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
|
|
|
|
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)
|
|
|
|
assert CalendarCycles.last_completed_cycle?(cycle_start, :monthly) == false
|
|
end
|
|
|
|
test "works correctly for quarterly intervals" do
|
|
# Test with a known past quarter
|
|
past_quarter_start = ~D[2024-01-01]
|
|
today = Date.utc_today()
|
|
|
|
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)
|
|
|
|
if Date.compare(today, next_quarter_end) in [:lt, :eq] do
|
|
assert CalendarCycles.last_completed_cycle?(past_quarter_start, :quarterly) == true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "edge cases" do
|
|
test "leap year: February has 29 days" do
|
|
# 2024 is a leap year
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-02-01], :monthly) == ~D[2024-02-29]
|
|
|
|
# 2023 is not a leap year
|
|
assert CalendarCycles.calculate_cycle_end(~D[2023-02-01], :monthly) == ~D[2023-02-28]
|
|
end
|
|
|
|
test "year boundary: December 31 to January 1" do
|
|
# Yearly cycle
|
|
assert CalendarCycles.next_cycle_start(~D[2024-01-01], :yearly) == ~D[2025-01-01]
|
|
|
|
# Monthly cycle across year boundary
|
|
assert CalendarCycles.next_cycle_start(~D[2024-12-01], :monthly) == ~D[2025-01-01]
|
|
|
|
# Half-yearly cycle across year boundary
|
|
assert CalendarCycles.next_cycle_start(~D[2024-07-01], :half_yearly) == ~D[2025-01-01]
|
|
|
|
# Quarterly cycle across year boundary
|
|
assert CalendarCycles.next_cycle_start(~D[2024-10-01], :quarterly) == ~D[2025-01-01]
|
|
end
|
|
|
|
test "month boundary: different month lengths" do
|
|
# 31-day months
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :monthly) == ~D[2024-01-31]
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-03-01], :monthly) == ~D[2024-03-31]
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-05-01], :monthly) == ~D[2024-05-31]
|
|
|
|
# 30-day months
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-04-01], :monthly) == ~D[2024-04-30]
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-06-01], :monthly) == ~D[2024-06-30]
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-09-01], :monthly) == ~D[2024-09-30]
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-11-01], :monthly) == ~D[2024-11-30]
|
|
end
|
|
|
|
test "date in middle of cycle: all functions work correctly" do
|
|
middle_date = ~D[2024-03-15]
|
|
|
|
# calculate_cycle_start
|
|
assert CalendarCycles.calculate_cycle_start(middle_date, :monthly) == ~D[2024-03-01]
|
|
assert CalendarCycles.calculate_cycle_start(middle_date, :quarterly) == ~D[2024-01-01]
|
|
assert CalendarCycles.calculate_cycle_start(middle_date, :half_yearly) == ~D[2024-01-01]
|
|
assert CalendarCycles.calculate_cycle_start(middle_date, :yearly) == ~D[2024-01-01]
|
|
|
|
# calculate_cycle_end
|
|
monthly_start = CalendarCycles.calculate_cycle_start(middle_date, :monthly)
|
|
assert CalendarCycles.calculate_cycle_end(monthly_start, :monthly) == ~D[2024-03-31]
|
|
|
|
# next_cycle_start
|
|
assert CalendarCycles.next_cycle_start(monthly_start, :monthly) == ~D[2024-04-01]
|
|
end
|
|
|
|
test "quarterly: all quarter boundaries correct" do
|
|
# Q1 boundaries
|
|
assert CalendarCycles.calculate_cycle_start(~D[2024-01-15], :quarterly) == ~D[2024-01-01]
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :quarterly) == ~D[2024-03-31]
|
|
|
|
# Q2 boundaries
|
|
assert CalendarCycles.calculate_cycle_start(~D[2024-05-15], :quarterly) == ~D[2024-04-01]
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-04-01], :quarterly) == ~D[2024-06-30]
|
|
|
|
# Q3 boundaries
|
|
assert CalendarCycles.calculate_cycle_start(~D[2024-08-15], :quarterly) == ~D[2024-07-01]
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-07-01], :quarterly) == ~D[2024-09-30]
|
|
|
|
# Q4 boundaries
|
|
assert CalendarCycles.calculate_cycle_start(~D[2024-11-15], :quarterly) == ~D[2024-10-01]
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-10-01], :quarterly) == ~D[2024-12-31]
|
|
end
|
|
|
|
test "half_yearly: both half boundaries correct" do
|
|
# First half boundaries
|
|
assert CalendarCycles.calculate_cycle_start(~D[2024-03-15], :half_yearly) == ~D[2024-01-01]
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :half_yearly) == ~D[2024-06-30]
|
|
|
|
# Second half boundaries
|
|
assert CalendarCycles.calculate_cycle_start(~D[2024-09-15], :half_yearly) == ~D[2024-07-01]
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-07-01], :half_yearly) == ~D[2024-12-31]
|
|
end
|
|
|
|
test "yearly: full year boundaries" do
|
|
assert CalendarCycles.calculate_cycle_start(~D[2024-06-15], :yearly) == ~D[2024-01-01]
|
|
assert CalendarCycles.calculate_cycle_end(~D[2024-01-01], :yearly) == ~D[2024-12-31]
|
|
assert CalendarCycles.next_cycle_start(~D[2024-01-01], :yearly) == ~D[2025-01-01]
|
|
end
|
|
end
|
|
end
|