Compare commits
4 commits
feature/27
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 894b9b9d5c | |||
| a7285915e6 | |||
| da6c495d04 | |||
| 3fc4440bce |
2 changed files with 510 additions and 0 deletions
329
lib/mv/membership_fees/calendar_cycles.ex
Normal file
329
lib/mv/membership_fees/calendar_cycles.ex
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
defmodule Mv.MembershipFees.CalendarCycles do
|
||||||
|
@moduledoc """
|
||||||
|
Calendar-based cycle calculation functions for membership fees.
|
||||||
|
|
||||||
|
This module provides functions for calculating cycle boundaries
|
||||||
|
based on interval types (monthly, quarterly, half-yearly, yearly).
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- `:monthly` - Cycles from 1st to last day of each month
|
||||||
|
- `:quarterly` - Cycles from 1st of Jan/Apr/Jul/Oct to last day of quarter
|
||||||
|
- `:half_yearly` - Cycles from 1st of Jan/Jul to last day of half-year
|
||||||
|
- `:yearly` - Cycles from Jan 1st to Dec 31st
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> date = ~D[2024-03-15]
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(date, :monthly)
|
||||||
|
~D[2024-03-01]
|
||||||
|
|
||||||
|
iex> cycle_start = ~D[2024-01-01]
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(cycle_start, :yearly)
|
||||||
|
~D[2024-12-31]
|
||||||
|
|
||||||
|
iex> cycle_start = ~D[2024-01-01]
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(cycle_start, :yearly)
|
||||||
|
~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 reference date.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
- `date` - Ignored in this 3-argument version (kept for API consistency)
|
||||||
|
- `interval` - The interval type (`:monthly`, `:quarterly`, `:half_yearly`, `:yearly`)
|
||||||
|
- `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)
|
||||||
|
~D[2024-03-01]
|
||||||
|
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-05-15], :quarterly)
|
||||||
|
~D[2024-04-01]
|
||||||
|
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_start(~D[2024-09-15], :half_yearly)
|
||||||
|
~D[2024-07-01]
|
||||||
|
|
||||||
|
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()
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
- `cycle_start` - The start date of the cycle
|
||||||
|
- `interval` - The interval type
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
The end date of the cycle.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-03-01], :monthly)
|
||||||
|
~D[2024-03-31]
|
||||||
|
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-02-01], :monthly)
|
||||||
|
~D[2024-02-29]
|
||||||
|
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-01-01], :quarterly)
|
||||||
|
~D[2024-03-31]
|
||||||
|
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.calculate_cycle_end(~D[2024-01-01], :half_yearly)
|
||||||
|
~D[2024-06-30]
|
||||||
|
|
||||||
|
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()
|
||||||
|
def calculate_cycle_end(cycle_start, interval) do
|
||||||
|
case interval do
|
||||||
|
:monthly -> monthly_cycle_end(cycle_start)
|
||||||
|
:quarterly -> quarterly_cycle_end(cycle_start)
|
||||||
|
:half_yearly -> half_yearly_cycle_end(cycle_start)
|
||||||
|
:yearly -> yearly_cycle_end(cycle_start)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Calculates the start date of the next cycle.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
- `cycle_start` - The start date of the current cycle
|
||||||
|
- `interval` - The interval type
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
The start date of the next cycle.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(~D[2024-01-01], :monthly)
|
||||||
|
~D[2024-02-01]
|
||||||
|
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(~D[2024-01-01], :quarterly)
|
||||||
|
~D[2024-04-01]
|
||||||
|
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.next_cycle_start(~D[2024-01-01], :half_yearly)
|
||||||
|
~D[2024-07-01]
|
||||||
|
|
||||||
|
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()
|
||||||
|
def next_cycle_start(cycle_start, interval) do
|
||||||
|
cycle_end = calculate_cycle_end(cycle_start, interval)
|
||||||
|
next_date = Date.add(cycle_end, 1)
|
||||||
|
calculate_cycle_start(next_date, interval)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
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 the given date is within the cycle, `false` otherwise.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.current_cycle?(~D[2024-03-01], :monthly, ~D[2024-03-15])
|
||||||
|
true
|
||||||
|
|
||||||
|
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(), 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 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
|
||||||
|
|
||||||
|
- `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 is the last completed cycle, `false` otherwise.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Mv.MembershipFees.CalendarCycles.last_completed_cycle?(~D[2024-03-01], :monthly, ~D[2024-04-01])
|
||||||
|
true
|
||||||
|
|
||||||
|
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(), Date.t()) :: boolean()
|
||||||
|
def last_completed_cycle?(cycle_start, interval, today) do
|
||||||
|
cycle_end = calculate_cycle_end(cycle_start, interval)
|
||||||
|
|
||||||
|
# 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 (today <= next_end)
|
||||||
|
next_start = next_cycle_start(cycle_start, interval)
|
||||||
|
next_end = calculate_cycle_end(next_start, interval)
|
||||||
|
|
||||||
|
Date.compare(today, next_end) in [:lt, :eq]
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
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
|
||||||
|
Date.new!(date.year, date.month, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp monthly_cycle_end(cycle_start) do
|
||||||
|
Date.end_of_month(cycle_start)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp quarterly_cycle_start(date) do
|
||||||
|
quarter_start_month =
|
||||||
|
case date.month do
|
||||||
|
m when m in [1, 2, 3] -> 1
|
||||||
|
m when m in [4, 5, 6] -> 4
|
||||||
|
m when m in [7, 8, 9] -> 7
|
||||||
|
m when m in [10, 11, 12] -> 10
|
||||||
|
end
|
||||||
|
|
||||||
|
Date.new!(date.year, quarter_start_month, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp quarterly_cycle_end(cycle_start) do
|
||||||
|
case cycle_start.month do
|
||||||
|
1 -> Date.new!(cycle_start.year, 3, 31)
|
||||||
|
4 -> Date.new!(cycle_start.year, 6, 30)
|
||||||
|
7 -> Date.new!(cycle_start.year, 9, 30)
|
||||||
|
10 -> Date.new!(cycle_start.year, 12, 31)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp half_yearly_cycle_start(date) do
|
||||||
|
half_start_month = if date.month in 1..6, do: 1, else: 7
|
||||||
|
Date.new!(date.year, half_start_month, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp half_yearly_cycle_end(cycle_start) do
|
||||||
|
case cycle_start.month do
|
||||||
|
1 -> Date.new!(cycle_start.year, 6, 30)
|
||||||
|
7 -> Date.new!(cycle_start.year, 12, 31)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp yearly_cycle_start(date) do
|
||||||
|
Date.new!(date.year, 1, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp yearly_cycle_end(cycle_start) do
|
||||||
|
Date.new!(cycle_start.year, 12, 31)
|
||||||
|
end
|
||||||
|
end
|
||||||
181
test/mv/membership_fees/calendar_cycles_test.exs
Normal file
181
test/mv/membership_fees/calendar_cycles_test.exs
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
defmodule Mv.MembershipFees.CalendarCyclesTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for CalendarCycles module.
|
||||||
|
"""
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Mv.MembershipFees.CalendarCycles
|
||||||
|
|
||||||
|
doctest Mv.MembershipFees.CalendarCycles
|
||||||
|
|
||||||
|
describe "calculate_cycle_start/3" do
|
||||||
|
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 "current_cycle?/3" do
|
||||||
|
# Basic examples are covered by doctests
|
||||||
|
|
||||||
|
test "works for all interval types" do
|
||||||
|
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, today)
|
||||||
|
|
||||||
|
assert result == true, "Expected current cycle for #{interval} with start #{cycle_start}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
describe "last_completed_cycle?/3" do
|
||||||
|
# Basic examples are covered by doctests
|
||||||
|
|
||||||
|
test "returns false when next cycle has also ended" do
|
||||||
|
# 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, today) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "works correctly for quarterly intervals" do
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Result depends on actual today, so we just verify it's a boolean
|
||||||
|
assert is_boolean(result)
|
||||||
|
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