feat(overview): add period-scoped payment filter and field-picker descriptor

This commit is contained in:
Simon 2026-07-10 16:27:15 +02:00
parent 95ef3177ac
commit 7d1a71b1fd
12 changed files with 654 additions and 0 deletions

View file

@ -113,6 +113,74 @@ defmodule MvWeb.MemberLive.Index.DateFilterPropertyTest do
end
end
# §2.4 — single-source exit-date state (quick ↔ detail) ----------------
#
# The active/former quick filter is a shortcut onto the very same
# `exit_date` state the detailed control writes: there is no separate
# quick-filter field. This property drives interleaved sequences of quick
# and detailed changes and asserts that `quick_state/1` is always a pure
# function of the single `exit_date` source — i.e. the two views can never
# diverge.
defp quick_choice_gen, do: member_of([:active, :former, :all])
defp command_gen do
one_of([
gen(all(s <- quick_choice_gen()), do: {:quick, s}),
gen all(
mode <- exit_date_mode_gen(),
from <- optional_date_gen(),
to <- optional_date_gen()
) do
{:detail, %{mode: mode, from: from, to: to}}
end
])
end
defp apply_command(filters, {:quick, s}), do: DateFilter.set_quick_state(filters, s)
defp apply_command(filters, {:detail, exit_date}), do: Map.put(filters, :exit_date, exit_date)
defp expected_quick_for(:active_only), do: :active
defp expected_quick_for(:inactive_only), do: :former
defp expected_quick_for(:all), do: :all
defp expected_quick_for(:custom), do: :custom
property "quick filter is a single-source shortcut onto exit_date, never divergent" do
check all(commands <- list_of(command_gen(), max_length: 12)) do
final =
Enum.reduce(commands, DateFilter.default(), fn command, filters ->
next = apply_command(filters, command)
# Invariant at every step: quick_state is derived solely from the
# exit_date mode — one consistent value, no second source.
assert DateFilter.quick_state(next) == expected_quick_for(next.exit_date.mode)
# A quick command resolves to exactly the chosen state and clears bounds.
case command do
{:quick, s} ->
assert DateFilter.quick_state(next) == s
assert next.exit_date.from == nil
assert next.exit_date.to == nil
_ ->
:ok
end
next
end)
# join_date is untouched by any exit_date command (no cross-contamination).
assert final.join_date == DateFilter.default().join_date
end
end
property "set_quick_state ∘ quick_state round-trips for the three quick states" do
check all(s <- quick_choice_gen(), exit_date <- exit_date_state_gen()) do
filters = Map.put(DateFilter.default(), :exit_date, exit_date)
assert filters |> DateFilter.set_quick_state(s) |> DateFilter.quick_state() == s
end
end
property "encoding then decoding built-in date filter state is identity" do
check all(
join_date <- join_date_state_gen(),