Skip to content

feat: add duplicate filter to invoice status dropdown#135

Merged
emilwojtaszek merged 2 commits into
mainfrom
feat/duplicate-status-filter
Apr 7, 2026
Merged

feat: add duplicate filter to invoice status dropdown#135
emilwojtaszek merged 2 commits into
mainfrom
feat/duplicate-status-filter

Conversation

@emilwojtaszek

@emilwojtaszek emilwojtaszek commented Apr 7, 2026

Copy link
Copy Markdown
Member

Summary

  • Add "Duplicate" option to the invoice list status filter dropdown
  • Confirmed duplicates are now excluded from query results by default (even if their underlying status is pending/approved)
  • Default filter pre-selects Pending + Approved for a cleaner initial view
  • Deselecting all statuses shows all non-duplicate invoices

Test plan

  • Fresh page load: Pending + Approved pre-selected, no duplicates visible
  • Check "Duplicate": confirmed duplicates appear in the list
  • Check "Duplicate" + "Approved": both regular approved and duplicates show
  • Uncheck all: all invoices shown except confirmed duplicates
  • Filter count badge: no count shown for default selection (Pending + Approved)
  • mix test passes (4 new tests for duplicate filtering)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added "Duplicate" as a selectable status in the invoice status filter.
    • Default status filter now pre-populates to Pending and Approved.
  • Bug Fixes

    • Confirmed duplicate invoices are excluded from results unless "Duplicate" is explicitly selected.
    • Filter changes preserve default statuses in URL parameters; empty status selection is serialized as an empty value and pagination resets include statuses.
  • Tests

    • Added tests covering duplicate inclusion/exclusion and URL/filter behavior.

Add "Duplicate" option to the status filter so users can explicitly
toggle visibility of confirmed-duplicate invoices. Confirmed duplicates
are now excluded by default from query results, and the default filter
pre-selects Pending + Approved for a cleaner list view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2fa77052-c135-4e7c-8462-eda3b8337f98

📥 Commits

Reviewing files that changed from the base of the PR and between 585d170 and 0e268fb.

📒 Files selected for processing (3)
  • lib/ksef_hub/invoices.ex
  • lib/ksef_hub_web/live/invoice_live/index.ex
  • test/ksef_hub/invoices_test.exs

📝 Walkthrough

Walkthrough

Invoice filtering now treats :duplicate as a special status: confirmed duplicates are excluded unless :duplicate is requested. LiveView defaults statuses to [:pending, :approved], accepts "duplicate" in CSV parsing, preserves/serializes the statuses query param, and tests cover the new behaviors.

Changes

Cohort / File(s) Summary
Query filtering logic
lib/ksef_hub/invoices.ex
Introduced apply_status_and_duplicate_filters/2 and refactored apply_filters/2 to: treat :duplicate specially, exclude duplicate_status == :confirmed by default, derive real_statuses (requested statuses without :duplicate), and branch query construction for (only duplicate), (duplicate + normal), or (only normal) selections.
LiveView filters & params
lib/ksef_hub_web/live/invoice_live/index.ex
Ensure :statuses defaults to [:pending, :approved] in handle_params/3; add filter_count/1 and statuses_count/1; parse "statuses" to accept "duplicate" and treat "" as []; serialize an empty :statuses as ""; add "Duplicate" option to the multi-select.
Tests — invoices behavior
test/ksef_hub/invoices_test.exs
Added five tests for list_invoices/2 validating exclusion of confirmed duplicates by default, inclusion when :duplicate is requested (alone or with others), and behavior for absent or explicit empty statuses.
Tests — LiveView URL expectations
test/ksef_hub_web/live/invoice_live/index_test.exs
Updated two URL-assertion tests to expect statuses=pending%2Capproved to be preserved/added in push_patch/pagination scenarios.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,230,255,0.5)
    participant User
    end
    rect rgba(200,255,200,0.5)
    participant LiveView
    end
    rect rgba(255,230,200,0.5)
    participant InvoicesModule
    end
    rect rgba(255,200,200,0.5)
    participant DB
    end

    User->>LiveView: Change filters (statuses include/exclude "duplicate")
    LiveView->>InvoicesModule: build_query_params / list_invoices(filters)
    InvoicesModule->>InvoicesModule: apply_status_and_duplicate_filters(filters)
    alt only :duplicate requested
        InvoicesModule->>DB: WHERE duplicate_status == :confirmed
    else duplicate + normal requested
        InvoicesModule->>DB: WHERE (status IN real_statuses) OR (duplicate_status == :confirmed)
    else only normal requested
        InvoicesModule->>DB: WHERE status IN real_statuses AND duplicate_status != :confirmed
    end
    DB-->>InvoicesModule: rows
    InvoicesModule-->>LiveView: invoices list
    LiveView-->>User: render results / push_patch with serialized statuses
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A hop, a filter, duplicates stay tucked away,
Ask for :duplicate and they'll scamper out to play.
Pending and approved lead the default run,
Confirmed copies hide until you call them one—
Hooray! invoices hop to the right display. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: adding a 'Duplicate' option to the invoice status filter dropdown, which is the main user-facing feature introduced across the UI and filtering logic changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/duplicate-status-filter

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
test/ksef_hub/invoices_test.exs (1)

356-368: Add an explicit %{statuses: []} test case.

Current coverage checks %{} but not the explicit empty-statuses path produced by LiveView (statuses=""[]). Adding it would lock the “all deselected = all non-duplicates” behavior end-to-end.

✅ Suggested test addition
+    test "excludes confirmed duplicates when statuses is explicitly empty", %{company: company} do
+      insert(:invoice, type: :expense, company: company)
+      insert(:invoice, type: :expense, company: company, duplicate_status: :confirmed)
+
+      result = Invoices.list_invoices(company.id, %{statuses: []})
+      assert length(result) == 1
+      assert is_nil(hd(result).duplicate_status)
+    end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/ksef_hub/invoices_test.exs` around lines 356 - 368, Add an extra test
that calls Invoices.list_invoices(company.id, %{statuses: []}) to ensure the
explicit empty statuses path (produced by LiveView as statuses="") excludes
confirmed duplicates the same as the %{ } case; update the test suite by adding
a new test (e.g., "excludes confirmed duplicates when explicit statuses filter
is an empty list") that inserts one normal invoice and one with
duplicate_status: :confirmed for the same company and asserts the result length
is 1 and that hd(result).duplicate_status is nil, using the same setup as the
existing test that calls Invoices.list_invoices/2.
lib/ksef_hub/invoices.ex (1)

2445-2525: Refactor apply_filters/2 to reduce branching complexity.

This function is now doing too much (and CI is already flagging it). Extract duplicate/status handling into a dedicated helper to keep filter composition maintainable.

♻️ Proposed refactor outline
 defp apply_filters(query, filters) do
-  # Exclude confirmed duplicates unless "duplicate" is explicitly selected
-  include_duplicates = :duplicate in (filters[:statuses] || [])
-
-  query =
-    if include_duplicates do
-      query
-    else
-      where(query, [i], is_nil(i.duplicate_status) or i.duplicate_status != :confirmed)
-    end
+  query =
+    query
+    |> apply_duplicate_visibility(filters)

   Enum.reduce(filters, query, fn
@@
-      {:statuses, statuses}, q when is_list(statuses) and statuses != [] ->
-        real_statuses = Enum.reject(statuses, &(&1 == :duplicate))
-        show_duplicates = :duplicate in statuses
-
-        case {real_statuses, show_duplicates} do
-          {[], true} ->
-            where(q, [i], i.duplicate_status == :confirmed)
-          {_, true} ->
-            where(q, [i], i.status in ^real_statuses or i.duplicate_status == :confirmed)
-          {[_ | _], false} ->
-            where(q, [i], i.status in ^real_statuses)
-          _ ->
-            q
-        end
+      {:statuses, statuses}, q when is_list(statuses) and statuses != [] ->
+        apply_statuses_filter(q, statuses)
@@
   end)
 end
+
+defp apply_duplicate_visibility(query, filters) do
+  if :duplicate in (filters[:statuses] || []) do
+    query
+  else
+    where(query, [i], is_nil(i.duplicate_status) or i.duplicate_status != :confirmed)
+  end
+end
+
+defp apply_statuses_filter(query, statuses) do
+  real_statuses = Enum.reject(statuses, &(&1 == :duplicate))
+  show_duplicates = :duplicate in statuses
+
+  case {real_statuses, show_duplicates} do
+    {[], true} -> where(query, [i], i.duplicate_status == :confirmed)
+    {[_ | _], true} -> where(query, [i], i.status in ^real_statuses or i.duplicate_status == :confirmed)
+    {[_ | _], false} -> where(query, [i], i.status in ^real_statuses)
+    _ -> query
+  end
+end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ksef_hub/invoices.ex` around lines 2445 - 2525, The apply_filters/2
function has grown overly complex around the duplicate/status logic; extract
that whole block (the include_duplicates assignment and the {:statuses,
statuses} case plus the initial global exclude where(...) that checks
i.duplicate_status) into a focused helper (e.g.
apply_status_and_duplicate_filters(query, filters) or build_status_filter(query,
statuses, include_duplicates)) and call it from apply_filters/2, leaving
apply_filters to only enumerate simple per-filter clauses (keeping calls to
apply_payment_status_filter/2 and apply_text_search/2 intact); ensure the helper
preserves the current behavior: compute include_duplicates = :duplicate in
(filters[:statuses] || []) and then implement the four case branches ({[],
true}, {_, true}, {[_|_], false}, _) and the initial global exclusion when
include_duplicates is false.
lib/ksef_hub_web/live/invoice_live/index.ex (1)

84-118: Split filter_assigns/4 to reduce cyclomatic complexity.

The new status-count branching pushed this function over the CI complexity threshold. Extracting statuses_count/1 (and optionally filter_count/1) keeps this handler easier to evolve.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ksef_hub_web/live/invoice_live/index.ex` around lines 84 - 118, Extract
the status-count calculation out of filter_assigns/4 into a new private helper
statuses_count/1 (and optionally extract the whole filter_count computation into
filter_count/2 or filter_count/1) to reduce cyclomatic complexity: move the
logic that builds default_statuses, inspects filters[:statuses], compares to the
default MapSet and returns the integer count into statuses_count/1 and replace
the inline case in filter_assigns/4 with a call to statuses_count(filters); if
you extract filter_count as well, accept filters (and result if needed) and
return the final integer which filter_assigns/4 will assign to filter_count
while keeping all existing behavior and references (e.g., filter_assigns/4,
statuses_count/1, filter_count/1) identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/ksef_hub_web/live/invoice_live/index.ex`:
- Around line 84-118: Extract the status-count calculation out of
filter_assigns/4 into a new private helper statuses_count/1 (and optionally
extract the whole filter_count computation into filter_count/2 or
filter_count/1) to reduce cyclomatic complexity: move the logic that builds
default_statuses, inspects filters[:statuses], compares to the default MapSet
and returns the integer count into statuses_count/1 and replace the inline case
in filter_assigns/4 with a call to statuses_count(filters); if you extract
filter_count as well, accept filters (and result if needed) and return the final
integer which filter_assigns/4 will assign to filter_count while keeping all
existing behavior and references (e.g., filter_assigns/4, statuses_count/1,
filter_count/1) identical.

In `@lib/ksef_hub/invoices.ex`:
- Around line 2445-2525: The apply_filters/2 function has grown overly complex
around the duplicate/status logic; extract that whole block (the
include_duplicates assignment and the {:statuses, statuses} case plus the
initial global exclude where(...) that checks i.duplicate_status) into a focused
helper (e.g. apply_status_and_duplicate_filters(query, filters) or
build_status_filter(query, statuses, include_duplicates)) and call it from
apply_filters/2, leaving apply_filters to only enumerate simple per-filter
clauses (keeping calls to apply_payment_status_filter/2 and apply_text_search/2
intact); ensure the helper preserves the current behavior: compute
include_duplicates = :duplicate in (filters[:statuses] || []) and then implement
the four case branches ({[], true}, {_, true}, {[_|_], false}, _) and the
initial global exclusion when include_duplicates is false.

In `@test/ksef_hub/invoices_test.exs`:
- Around line 356-368: Add an extra test that calls
Invoices.list_invoices(company.id, %{statuses: []}) to ensure the explicit empty
statuses path (produced by LiveView as statuses="") excludes confirmed
duplicates the same as the %{ } case; update the test suite by adding a new test
(e.g., "excludes confirmed duplicates when explicit statuses filter is an empty
list") that inserts one normal invoice and one with duplicate_status: :confirmed
for the same company and asserts the result length is 1 and that
hd(result).duplicate_status is nil, using the same setup as the existing test
that calls Invoices.list_invoices/2.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fd7b0683-bc56-48c7-9704-82ccbd68a700

📥 Commits

Reviewing files that changed from the base of the PR and between 0e35eb1 and 585d170.

📒 Files selected for processing (4)
  • lib/ksef_hub/invoices.ex
  • lib/ksef_hub_web/live/invoice_live/index.ex
  • test/ksef_hub/invoices_test.exs
  • test/ksef_hub_web/live/invoice_live/index_test.exs

…ses test

Extract statuses_count/1 and filter_count/1 from filter_assigns/4 in
LiveView, and apply_status_and_duplicate_filters/2 from apply_filters/2
in the Invoices context to reduce cyclomatic complexity. Add test
covering explicit empty statuses list path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@emilwojtaszek emilwojtaszek merged commit e7efb20 into main Apr 7, 2026
1 of 2 checks passed
@emilwojtaszek

Copy link
Copy Markdown
Member Author

@coderabbitai, can you also run review even if i've just merged this PR?

@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown

@emilwojtaszek: Yes, absolutely! CodeRabbit can review a PR even after it has been merged. Triggering a review now! 🐇

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant