diff --git a/lib/mv/constants.ex b/lib/mv/constants.ex
index f60f77ad..a0607078 100644
--- a/lib/mv/constants.ex
+++ b/lib/mv/constants.ex
@@ -50,7 +50,7 @@ defmodule Mv.Constants do
@suspended_param "suspended"
- @stichtag_param "stichtag"
+ @as_of_date_param "as_of_date"
@max_boolean_filters 50
@@ -240,16 +240,16 @@ defmodule Mv.Constants do
def suspended_param, do: @suspended_param
@doc """
- Returns the URL parameter name for the point-in-time membership ("Stichtag")
+ Returns the URL parameter name for the point-in-time "active on X" membership
filter: keeps members who were active on the given ISO-8601 date X, i.e.
`join_date <= X and (exit_date is nil or exit_date > X)`.
## Examples
- iex> Mv.Constants.stichtag_param()
- "stichtag"
+ iex> Mv.Constants.as_of_date_param()
+ "as_of_date"
"""
- def stichtag_param, do: @stichtag_param
+ def as_of_date_param, do: @as_of_date_param
@doc """
Returns the maximum number of boolean custom field filters allowed per request.
diff --git a/lib/mv_web/live/components/add_filter_builder_component.ex b/lib/mv_web/live/components/add_filter_builder_component.ex
index fc1ee34b..4cc2c619 100644
--- a/lib/mv_web/live/components/add_filter_builder_component.ex
+++ b/lib/mv_web/live/components/add_filter_builder_component.ex
@@ -35,7 +35,7 @@ defmodule MvWeb.Components.AddFilterBuilderComponent do
* `{:payment_filter_changed, nil | :fully_paid | {:has_unpaid, 1..3} | {:unpaid_range, pos_integer(), pos_integer() | nil}}`
* `{:payment_period_changed, %{from: Date.t() | nil, to: Date.t() | nil}}`
* `{:suspended_changed, boolean()}`
- * `{:stichtag_changed, Date.t() | nil}`
+ * `{:as_of_date_changed, Date.t() | nil}`
and `{:clear_all_filters}` for Clear all.
"""
@@ -91,7 +91,7 @@ defmodule MvWeb.Components.AddFilterBuilderComponent do
|> assign(:payment_filter, assigns[:payment_filter])
|> assign(:payment_period, assigns[:payment_period] || PaymentAging.default_period())
|> assign(:suspended, assigns[:suspended] || false)
- |> assign(:stichtag, assigns[:stichtag])
+ |> assign(:as_of_date, assigns[:as_of_date])
end
# --------------------------------------------------------------------------
@@ -461,23 +461,23 @@ defmodule MvWeb.Components.AddFilterBuilderComponent do
<%!-- Separate paragraph (not part of the Active/Former button-join): the
- point-in-time "active on X" Stichtag filter (§1.24). --%>
-
+ point-in-time "active on X" as-of-date filter (§1.24). --%>
+
<.input
type="date"
- name="stichtag"
+ name="as_of_date"
label={gettext("Active on reference date")}
class="input input-sm input-bordered"
- value={date_iso(@stichtag)}
- data-testid="stichtag-input"
+ value={date_iso(@as_of_date)}
+ data-testid="as-of-date-input"
/>
- <%!-- Third paragraph "Eigener Zeitraum": the same native From/To range the
+ <%!-- Third paragraph, a custom range: the same native From/To range the
join-date builder uses, wired to the exit_date custom mode — members whose
exit date falls within [from, to] (§1.24). A sibling form (not the
- quick/Stichtag one) so committing a bound only touches the exit_date
+ quick/as-of-date one) so committing a bound only touches the exit_date
slice and never re-reads the quick radios. --%>
diff --git a/lib/mv_web/live/member_live/index/as_of_date.ex b/lib/mv_web/live/member_live/index/as_of_date.ex
new file mode 100644
index 00000000..6ec9d9a1
--- /dev/null
+++ b/lib/mv_web/live/member_live/index/as_of_date.ex
@@ -0,0 +1,37 @@
+defmodule MvWeb.MemberLive.Index.AsOfDate do
+ @moduledoc """
+ URL codec for the point-in-time "active on X" membership filter (§1.24/§2.6).
+
+ The as-of date is a single ISO-8601 date X carried in the flat URL contract
+ under the `as_of_date` key. When set, the overview keeps members who were
+ active on X (`join_date <= X and (exit_date is nil or exit_date > X)`); the
+ predicate itself lives in `OverviewQuery`. Absent or malformed values decode
+ to nil (no filter), and nil encodes to the empty map so a fresh URL stays
+ canonical.
+ """
+
+ @as_of_date_param Mv.Constants.as_of_date_param()
+
+ @doc """
+ Decodes the as-of date from a params map. Missing or malformed ISO-8601
+ values yield nil.
+ """
+ @spec parse(map()) :: Date.t() | nil
+ def parse(params) when is_map(params), do: parse_date(Map.get(params, @as_of_date_param))
+
+ @doc """
+ Encodes an as-of date into URL params. nil yields the empty map.
+ """
+ @spec to_params(Date.t() | nil) :: %{optional(String.t()) => String.t()}
+ def to_params(%Date{} = date), do: %{@as_of_date_param => Date.to_iso8601(date)}
+ def to_params(_), do: %{}
+
+ defp parse_date(value) when is_binary(value) do
+ case Date.from_iso8601(String.trim(value)) do
+ {:ok, date} -> date
+ _ -> nil
+ end
+ end
+
+ defp parse_date(_), do: nil
+end
diff --git a/lib/mv_web/live/member_live/index/overview_query.ex b/lib/mv_web/live/member_live/index/overview_query.ex
index dc125d65..fa5559ca 100644
--- a/lib/mv_web/live/member_live/index/overview_query.ex
+++ b/lib/mv_web/live/member_live/index/overview_query.ex
@@ -46,7 +46,7 @@ defmodule MvWeb.MemberLive.Index.OverviewQuery do
opts[:boolean_custom_fields]
)
|> apply_date_filters(opts[:date_filters])
- |> apply_stichtag_filter(opts[:stichtag])
+ |> apply_as_of_date_filter(opts[:as_of_date])
|> apply_custom_date_filters(opts[:date_filters], opts[:date_custom_fields])
|> apply_payment_filter(opts[:payment_filter], payment_period(opts))
|> apply_suspended_filter(opts[:suspended], payment_period(opts))
@@ -285,20 +285,20 @@ defmodule MvWeb.MemberLive.Index.OverviewQuery do
defp apply_custom_date_filters(query, _filters, _custom_fields), do: query
# ---------------------------------------------------------------------------
- # Point-in-time membership ("Stichtag"): a member counts as a member on the
+ # Point-in-time membership (as-of date): a member counts as a member on the
# reference date X iff it had joined by X and had not yet left as of X
# (§1.24/§2.6). "Left as of X" means a non-nil exit_date at or before X, so an
# exit_date strictly after X (or nil) still counts as a member on X.
# ---------------------------------------------------------------------------
- defp apply_stichtag_filter(query, %Date{} = x) do
+ defp apply_as_of_date_filter(query, %Date{} = x) do
Ash.Query.filter(
query,
expr(join_date <= ^x and (is_nil(exit_date) or exit_date > ^x))
)
end
- defp apply_stichtag_filter(query, _), do: query
+ defp apply_as_of_date_filter(query, _), do: query
# ---------------------------------------------------------------------------
# Sort (always append the unique id tie-breaker for keyset stability)
@@ -327,7 +327,7 @@ defmodule MvWeb.MemberLive.Index.OverviewQuery do
])
end
- # "Beiträge" (payment) column: sort DB-side by the period-scoped unpaid-cycle
+ # Fees (payment) column: sort DB-side by the period-scoped unpaid-cycle
# count (§1.27). The count calculation takes the active period as arguments so
# the sort key matches the displayed value; the id tie-breaker keeps keyset
# pages stable across equal counts.
diff --git a/lib/mv_web/live/member_live/index/stichtag.ex b/lib/mv_web/live/member_live/index/stichtag.ex
deleted file mode 100644
index 647d609d..00000000
--- a/lib/mv_web/live/member_live/index/stichtag.ex
+++ /dev/null
@@ -1,36 +0,0 @@
-defmodule MvWeb.MemberLive.Index.Stichtag do
- @moduledoc """
- URL codec for the point-in-time membership ("Stichtag") filter (§1.24/§2.6).
-
- The Stichtag is a single ISO-8601 date X carried in the flat URL contract
- under the `stichtag` key. When set, the overview keeps members who were active
- on X (`join_date <= X and (exit_date is nil or exit_date > X)`); the predicate
- itself lives in `OverviewQuery`. Absent or malformed values decode to nil (no
- filter), and nil encodes to the empty map so a fresh URL stays canonical.
- """
-
- @stichtag_param Mv.Constants.stichtag_param()
-
- @doc """
- Decodes the Stichtag date from a params map. Missing or malformed ISO-8601
- values yield nil.
- """
- @spec parse(map()) :: Date.t() | nil
- def parse(params) when is_map(params), do: parse_date(Map.get(params, @stichtag_param))
-
- @doc """
- Encodes a Stichtag date into URL params. nil yields the empty map.
- """
- @spec to_params(Date.t() | nil) :: %{optional(String.t()) => String.t()}
- def to_params(%Date{} = date), do: %{@stichtag_param => Date.to_iso8601(date)}
- def to_params(_), do: %{}
-
- defp parse_date(value) when is_binary(value) do
- case Date.from_iso8601(String.trim(value)) do
- {:ok, date} -> date
- _ -> nil
- end
- end
-
- defp parse_date(_), do: nil
-end
diff --git a/test/mv_web/member_live/index_active_former_test.exs b/test/mv_web/member_live/index_active_former_test.exs
index 4bb2c731..e959643d 100644
--- a/test/mv_web/member_live/index_active_former_test.exs
+++ b/test/mv_web/member_live/index_active_former_test.exs
@@ -62,16 +62,16 @@ defmodule MvWeb.MemberLive.IndexActiveFormerTest do
assert has_element?(view, "#row-#{ctx.former.id}")
end
- describe "Stichtag paragraph (§1.24)" do
+ describe "as-of-date paragraph (§1.24)" do
test "the active/former control carries a separate 'active on reference date' input", ctx do
{:ok, view, _html} = live(ctx.conn, ~p"/members")
pick_field(view, "active_former")
- # The Stichtag input sits in its own paragraph, not inside the
+ # The as-of-date input sits in its own paragraph, not inside the
# Active/Former button-join.
- assert has_element?(view, "[data-testid='stichtag-paragraph'] input[name='stichtag']")
- refute has_element?(view, ".join input[name='stichtag']")
+ assert has_element?(view, "[data-testid='as-of-date-paragraph'] input[name='as_of_date']")
+ refute has_element?(view, ".join input[name='as_of_date']")
end
test "setting a reference date activates the point-in-time membership filter", ctx do
@@ -81,22 +81,22 @@ defmodule MvWeb.MemberLive.IndexActiveFormerTest do
view
|> element("[data-testid='value-control-active-former']")
- |> render_change(%{"quick" => "active", "stichtag" => "2024-06-15"})
+ |> render_change(%{"quick" => "active", "as_of_date" => "2024-06-15"})
_ = render(view)
path = assert_patch(view)
- assert path =~ "stichtag=2024-06-15"
+ assert path =~ "as_of_date=2024-06-15"
end
end
- describe "Eigener Zeitraum exit-date range (§1.24)" do
+ describe "custom exit-date range (§1.24)" do
test "the exit-date control carries a separate native From/To range paragraph", ctx do
{:ok, view, _html} = live(ctx.conn, ~p"/members")
pick_field(view, "active_former")
# Third paragraph, same native two-input range control the join-date
- # builder uses — separate from the Active/Former join and the Stichtag box.
+ # builder uses — separate from the Active/Former join and the as-of-date box.
assert has_element?(view, "[data-testid='value-control-exit-range']")
assert has_element?(
diff --git a/test/mv_web/member_live/index_stichtag_property_test.exs b/test/mv_web/member_live/index_as_of_date_property_test.exs
similarity index 92%
rename from test/mv_web/member_live/index_stichtag_property_test.exs
rename to test/mv_web/member_live/index_as_of_date_property_test.exs
index 017d08fb..a30fef52 100644
--- a/test/mv_web/member_live/index_stichtag_property_test.exs
+++ b/test/mv_web/member_live/index_as_of_date_property_test.exs
@@ -1,8 +1,8 @@
-defmodule MvWeb.MemberLive.IndexStichtagPropertyTest do
+defmodule MvWeb.MemberLive.IndexAsOfDatePropertyTest do
@moduledoc """
§2.6 — Point-in-time membership ("member on the reference date X").
- For arbitrary (join_date, exit_date) pairs and reference dates X, the Stichtag
+ For arbitrary (join_date, exit_date) pairs and reference dates X, the as-of-date
filter keeps a member iff `join_date <= X and (exit_date is nil or exit_date > X)`.
Verified at the `OverviewQuery` layer against an in-memory oracle.
"""
@@ -42,7 +42,7 @@ defmodule MvWeb.MemberLive.IndexStichtagPropertyTest do
end)
end
- property "Stichtag filter keeps exactly the members active on X" do
+ property "as-of-date filter keeps exactly the members active on X" do
actor = Mv.Helpers.SystemActor.get_system_actor()
check all(
@@ -67,7 +67,7 @@ defmodule MvWeb.MemberLive.IndexStichtagPropertyTest do
do: id
actual =
- %{stichtag: x}
+ %{as_of_date: x}
|> OverviewQuery.build()
|> Ash.read!(actor: actor)
|> MapSet.new(& &1.id)
diff --git a/test/mv_web/member_live/index_filter_url_roundtrip_test.exs b/test/mv_web/member_live/index_filter_url_roundtrip_test.exs
index 8fe3405a..b01a9705 100644
--- a/test/mv_web/member_live/index_filter_url_roundtrip_test.exs
+++ b/test/mv_web/member_live/index_filter_url_roundtrip_test.exs
@@ -1,7 +1,7 @@
defmodule MvWeb.MemberLive.IndexFilterUrlRoundtripTest do
@moduledoc """
§2.1 (extended) — the v2 filter keys survive the LiveView decode→encode round
- trip. Loading `/members` with a Stichtag date, the suspended flag, and a
+ trip. Loading `/members` with an as-of date, the suspended flag, and a
two-sided payment count range, then triggering an unrelated re-patch (a sort
click), preserves every one of those keys in the pushed URL.
"""
@@ -21,15 +21,15 @@ defmodule MvWeb.MemberLive.IndexFilterUrlRoundtripTest do
%{conn: conn_with_oidc_user(conn)}
end
- test "stichtag, suspended and payment count range persist through a re-patch", %{conn: conn} do
- url = "/members?stichtag=2026-03-15&suspended=1&pay_filter=has_unpaid&pay_min=2&pay_max=4"
+ test "as-of date, suspended and payment count range persist through a re-patch", %{conn: conn} do
+ url = "/members?as_of_date=2026-03-15&suspended=1&pay_filter=has_unpaid&pay_min=2&pay_max=4"
{:ok, view, _html} = live(conn, url)
# Trigger an unrelated URL re-patch by sorting a stable column.
view |> element("[data-testid='payment']") |> render_click()
path = assert_patch(view)
- assert path =~ "stichtag=2026-03-15"
+ assert path =~ "as_of_date=2026-03-15"
assert path =~ "suspended=1"
assert path =~ "pay_filter=has_unpaid"
assert path =~ "pay_min=2"
diff --git a/test/mv_web/member_live/index_status_params_codec_test.exs b/test/mv_web/member_live/index_status_params_codec_test.exs
index b1438969..bf32962e 100644
--- a/test/mv_web/member_live/index_status_params_codec_test.exs
+++ b/test/mv_web/member_live/index_status_params_codec_test.exs
@@ -1,28 +1,28 @@
defmodule MvWeb.MemberLive.IndexStatusParamsCodecTest do
@moduledoc """
§2.1 (extended) — URL codec round-trip for the new v2 filter keys: the
- point-in-time Stichtag date and the suspended-status flag. Encoding a state to
+ point-in-time as-of date and the suspended-status flag. Encoding a state to
params then decoding yields the canonical state, and the default (no filter)
encodes to the empty map so a fresh URL is canonical.
"""
use ExUnit.Case, async: true
+ alias MvWeb.MemberLive.Index.AsOfDate
alias MvWeb.MemberLive.Index.PaymentAging
- alias MvWeb.MemberLive.Index.Stichtag
- describe "Stichtag date" do
+ describe "as-of date" do
test "a set date round-trips" do
date = ~D[2026-03-15]
- assert date |> Stichtag.to_params() |> Stichtag.parse() == date
+ assert date |> AsOfDate.to_params() |> AsOfDate.parse() == date
end
test "nil encodes to the empty map and decodes to nil" do
- assert Stichtag.to_params(nil) == %{}
- assert Stichtag.parse(%{}) == nil
+ assert AsOfDate.to_params(nil) == %{}
+ assert AsOfDate.parse(%{}) == nil
end
test "a malformed date param decodes to nil" do
- assert Stichtag.parse(%{"stichtag" => "not-a-date"}) == nil
+ assert AsOfDate.parse(%{"as_of_date" => "not-a-date"}) == nil
end
end