feat: implement calendar-based cycle calculation functions
All checks were successful
continuous-integration/drone/push Build is passing
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.
This commit is contained in:
parent
ebbf347e42
commit
822d06ed54
2 changed files with 594 additions and 0 deletions
327
test/mv/membership_fees/calendar_cycles_test.exs
Normal file
327
test/mv/membership_fees/calendar_cycles_test.exs
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue