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