Skip to content

refactor: classifier config from env vars to DB-only per-company#173

Merged
emilwojtaszek merged 6 commits into
mainfrom
refactor/classifier-db-config
Apr 24, 2026
Merged

refactor: classifier config from env vars to DB-only per-company#173
emilwojtaszek merged 6 commits into
mainfrom
refactor/classifier-db-config

Conversation

@emilwojtaszek

@emilwojtaszek emilwojtaszek commented Apr 24, 2026

Copy link
Copy Markdown
Member

Summary

  • Move classifier configuration from environment variables to DB-only per-company config
  • Classification is OFF by default — must be enabled per company with URL, token, and thresholds
  • Remove all classifier env vars (INVOICE_CLASSIFIER_URL, INVOICE_CLASSIFIER_API_TOKEN, CATEGORY_CONFIDENCE_THRESHOLD, TAG_CONFIDENCE_THRESHOLD)
  • Worker gates on ClassifierConfig.enabled before calling the classifier service

Changes

Core plumbing

  • Behaviour callbacks accept config map (%{url, api_token}) as 2nd arg
  • Client reads URL/token from config param, not Application.get_env
  • InvoiceClassifier.predict_and_apply/2 accepts ClassifierConfig struct
  • Default thresholds extracted to ClassifierConfig schema constants (single source of truth)

Worker

  • Fetches ClassifierConfig by company_id, cancels if not enabled
  • No classification runs unless company has explicitly enabled it

Config cleanup

  • Removed classifier blocks from config.exs, runtime.exs, .env.example
  • Removed ServiceConfig.env_defaults/0
  • Removed classifier from global /healthz/services health checks (per-company now)

UI

  • "Enable invoice classification" toggle (was "Override environment variables")
  • Static placeholders from ClassifierConfig defaults (was env var values)
  • "Classification is disabled" message when not enabled

Seeds

  • seeds.exs creates disabled ClassifierConfig for all companies without one

Test plan

  • mix test — 2155 tests pass, 0 failures
  • Settings > Classifier: toggle enabled, enter localhost URL, save
  • Create expense invoice with classifier enabled — classification runs
  • Create expense invoice with classifier disabled — no classification job
  • /healthz/services returns only pdf_renderer + invoice_extractor
  • Run mix run priv/repo/seeds.exs — seeds classifier configs

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Configuration Changes

    • Invoice classifier moved to per-company DB configs; global env defaults and example vars for the classifier removed. Classification disabled by default; URL/token/thresholds must be set per company.
  • UI

    • Settings page reflects disabled-state copy, uses per-company values/placeholders, and requires a URL for pre-checks.
  • Health & Operations

    • Global health/warm-up no longer includes classifier; checked per-company.
  • Data

    • Seed script populates missing per-company classifier configs.
  • Tests

    • Tests updated to use per-company configs and adjusted client call signatures.

Classification is now OFF by default per company. When enabled via
Settings > Classifier, the company's URL, token, and thresholds are
used. No more env var fallback for classifier config.

- Behaviour callbacks accept config map (url + api_token)
- Client reads URL/token from config param, not Application env
- Worker gates on ClassifierConfig.enabled before calling classifier
- Remove INVOICE_CLASSIFIER_URL, INVOICE_CLASSIFIER_API_TOKEN,
  CATEGORY_CONFIDENCE_THRESHOLD, TAG_CONFIDENCE_THRESHOLD env vars
- Remove env_defaults/0 from ServiceConfig
- Remove classifier from global health checks (per-company now)
- Extract default thresholds/URL to ClassifierConfig schema constants
- Add thresholds_for_company/1 for LiveView threshold display
- UI: "Enable invoice classification" toggle, no env override language
- Seeds: create disabled ClassifierConfig for all companies
- Add classifier_config factory for tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bb599897-7299-4b6a-9a38-bfad5d8b79ba

📥 Commits

Reviewing files that changed from the base of the PR and between e808ba1 and 66e08fd.

📒 Files selected for processing (2)
  • lib/ksef_hub/invoice_classifier.ex
  • test/ksef_hub/invoice_classifier_test.exs

📝 Walkthrough

Walkthrough

Moves invoice-classifier from global env/runtime config to per-company DB-backed ClassifierConfig; client/behaviour arities and worker/liveviews/tests updated to accept per-call config; removes global thresholds and global health/warm-up checks; adds DB seeding for missing per-company configs.

Changes

Cohort / File(s) Summary
Env & Runtime Config
\.env.example, config/config.exs, config/runtime.exs
Removed invoice-classifier env vars and default threshold keys from examples and runtime; runtime no longer reads/validates invoice-classifier env vars.
Core Classifier API
lib/ksef_hub/invoice_classifier.ex, lib/ksef_hub/invoice_classifier/behaviour.ex, lib/ksef_hub/invoice_classifier/client.ex
Switched to per-call config: public predict_and_apply/1predict_and_apply/2; behaviour and client callbacks gain config arg; client request building/auth driven by passed config; thresholds resolved from ClassifierConfig; added thresholds_for_company/1.
Worker & Background
lib/ksef_hub/invoice_classifier/worker.ex
Jobs now fetch per-company ClassifierConfig and cancel when classifier not enabled; classification delegated with explicit config.
Service Config & Defaults
lib/ksef_hub/service_config.ex, lib/ksef_hub/service_config/classifier_config.ex
Replaced env-default accessor with DB-backed classifier defaults; added default_* accessors and classifier_defaults/0; docs updated for per-company model.
Health & Warm-up
lib/ksef_hub/service_health.ex
Removed invoice classifier from global health aggregation and upload warm-up; classifier health now handled per-company; removed global client resolver.
Web UI / LiveViews
lib/ksef_hub_web/live/invoice_live/classify.ex, lib/ksef_hub_web/live/invoice_live/show.ex, lib/ksef_hub_web/live/settings_live/services.ex
LiveViews now compute thresholds per-company via InvoiceClassifier.thresholds_for_company/1; settings UI treats classifier as disabled unless enabled, removes env-default fallbacks, and simplifies health-check/save flows.
Seeding & Factories
priv/repo/seeds.exs, test/support/factory.ex
Adds seed logic to create missing ClassifierConfig rows and a classifier_config_factory/0 for tests with default values.
Tests & Stubs
test/.../client_test.exs, .../worker_test.exs, .../invoice_classifier_test.exs, .../controllers/health_controller_test.exs, test/support/classifier_stub.ex
Tests updated to use per-company configs, pass config into classifier calls, update Mox/stub arities, remove global env mutation, and drop classifier from global health assertions.
Misc (docs/assigns)
lib/ksef_hub_web/live/...
UI text and assigns updated to reflect enabled/disabled semantics and new default accessor functions.

Sequence Diagram

sequenceDiagram
    participant Worker as Worker
    participant SC as ServiceConfig
    participant IC as InvoiceClassifier
    participant Client as ClassifierClient
    participant API as External API

    Worker->>SC: get_classifier_config(company_id)
    SC-->>Worker: ClassifierConfig (enabled?, url, api_token, thresholds)
    alt disabled
        Worker->>Worker: cancel job (classification not enabled)
    else enabled
        Worker->>IC: predict_and_apply(invoice, config)
        IC->>IC: decrypt api_token (if present)
        IC->>Client: predict_category(input, config)
        Client->>Client: fetch_url(config) / build_req
        Client->>API: POST /predict_category (auth from config)
        API-->>Client: category + confidence
        Client-->>IC: category result
        IC->>IC: apply category if >= config.category_threshold
        IC->>Client: predict_tag(input, config)
        Client->>API: POST /predict_tag
        API-->>Client: tags + confidences
        Client-->>IC: tag results
        IC->>IC: filter tags by config.tag_threshold
        IC-->>Worker: updated invoice / result
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly Related PRs

  • #170: Implements per-company ClassifierConfig and wiring — directly overlaps with this per-company refactor.
  • #120: Modifies invoice_classifier prediction/threshold logic — touches the same core module changes.
  • #86: Related to threshold handling and InvoiceClassifier API adjustments — similar area of change.

Poem

🐇 I hopped from env into the DB,
Per-company configs now follow me.
Tokens tucked and thresholds set,
Each invoice finds its proper bet.
Thump-thump, nibble — hooray, glee! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 main refactoring objective: moving invoice classifier configuration from environment variables to database-only, per-company configuration.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 refactor/classifier-db-config

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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/ksef_hub/service_config.ex (1)

87-89: ⚠️ Potential issue | 🟡 Minor

Fix the formatter failure before merge.

CI reports mix format --check-formatted failing in this file near EOF. Please run mix format and commit the formatting-only fix.

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

In `@lib/ksef_hub/service_config.ex` around lines 87 - 89, Run mix format on the
affected file and commit the formatting-only change: reformat
lib/ksef_hub/service_config.ex (ensure the file is formatted per project
.formatter.exs, that trailing whitespace is removed and the file ends with a
single newline at EOF) and push the commit; this will resolve the mix format
--check-formatted failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ksef_hub/invoice_classifier.ex`:
- Around line 136-138: The assignment to cat_threshold and the following
variable start (confident_category?) is misformatted; run mix format and adjust
the wrapping so the assignment uses parentheses or line breaks that satisfy
formatter rules — specifically update the lines around cat_threshold =
config.category_confidence_threshold ||
ClassifierConfig.default_category_threshold() and the subsequent
confident_category? expression in module InvoiceClassifier (look for the
variables cat_threshold, config.category_confidence_threshold,
ClassifierConfig.default_category_threshold, and confident_category?) so they
are wrapped/indented per mix format (e.g., keep the full fallback call on one
line or break before the ||) to resolve the formatter check.
- Around line 30-40: thresholds_for_company/1 currently insists both
category_confidence_threshold and tag_confidence_threshold are floats and
otherwise returns both defaults; change it to preserve per-field overrides by
fetching the config via ServiceConfig.get_classifier_config(company_id), then
for each field (category_confidence_threshold and tag_confidence_threshold) use
the configured value if it is a float and fallback to
ClassifierConfig.default_category_threshold() or
ClassifierConfig.default_tag_threshold() respectively if not; update the
function to perform individual is_float checks (so behavior matches the
per-field fallback in resolve_predictions/4) while keeping references to
ClassifierConfig.default_category_threshold/ default_tag_threshold and
ServiceConfig.get_classifier_config.

In `@test/ksef_hub/invoice_classifier_test.exs`:
- Line 225: The test headers/assertions are failing mix format; run mix format
(or manually wrap long lines) to fix formatting in the tests, specifically in
the test "sets predicted when only tag is above tag threshold" and the tests
covering the assertions between the later block (around the
invoice_classifier_test cases), ensuring long assertion lines and string
concatenations are wrapped to conform to Elixir formatter conventions so that
mix format --check-formatted passes; locate the tests by their names in
invoice_classifier_test.exs and re-run mix format to apply the changes.

In `@test/ksef_hub/invoice_classifier/client_test.exs`:
- Around line 85-89: The long chained assertions in the test for
Client.predict_tag are exceeding line length; break them into multiple
assertions or wrap the map access onto separate lines so each assert is short:
keep the call assert {:ok, result} = Client.predict_tag(%{invoice_title:
"Monthly subscription"}, `@test_config`) on its own line, then assert
result["predicted_label"] == "benefit-books-formula", assert
result["confidence"] == 0.88, and assert result["model_version"] == "1.0.0" each
on their own lines (or split any long expressions across lines) to satisfy the
formatter and CI.
- Around line 29-34: The long single-line assertion causes CI formatting
failures; refactor the final assertion in the test for Client.predict_category
to wrap across multiple lines by extracting nested values into temporaries and
asserting them separately (e.g., bind result["probabilities"] to a variable and
then assert that variable["operations:infrastructure"] == 0.95), or split the
assertion into two shorter asserts so that the line length is reduced while
keeping existing checks for predicted_label, confidence, model_version and
probabilities.

---

Outside diff comments:
In `@lib/ksef_hub/service_config.ex`:
- Around line 87-89: Run mix format on the affected file and commit the
formatting-only change: reformat lib/ksef_hub/service_config.ex (ensure the file
is formatted per project .formatter.exs, that trailing whitespace is removed and
the file ends with a single newline at EOF) and push the commit; this will
resolve the mix format --check-formatted failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 17fa7d7f-23a3-44ed-b920-bb82876d1237

📥 Commits

Reviewing files that changed from the base of the PR and between 7fccee6 and 32a5dd6.

📒 Files selected for processing (22)
  • .env.example
  • config/config.exs
  • config/runtime.exs
  • lib/ksef_hub/invoice_classifier.ex
  • lib/ksef_hub/invoice_classifier/behaviour.ex
  • lib/ksef_hub/invoice_classifier/client.ex
  • lib/ksef_hub/invoice_classifier/worker.ex
  • lib/ksef_hub/service_config.ex
  • lib/ksef_hub/service_config/classifier_config.ex
  • lib/ksef_hub/service_health.ex
  • lib/ksef_hub_web/live/invoice_live/classify.ex
  • lib/ksef_hub_web/live/invoice_live/show.ex
  • lib/ksef_hub_web/live/settings_live/services.ex
  • priv/repo/seeds.exs
  • test/ksef_hub/invoice_classifier/client_test.exs
  • test/ksef_hub/invoice_classifier/worker_test.exs
  • test/ksef_hub/invoice_classifier_test.exs
  • test/ksef_hub_web/controllers/health_controller_test.exs
  • test/ksef_hub_web/live/invoice_live/upload_test.exs
  • test/ksef_hub_web/live/settings_live/services_test.exs
  • test/support/classifier_stub.ex
  • test/support/factory.ex
💤 Files with no reviewable changes (4)
  • config/runtime.exs
  • .env.example
  • config/config.exs
  • test/ksef_hub_web/live/invoice_live/upload_test.exs

Comment on lines +30 to +40
case ServiceConfig.get_classifier_config(company_id) do
%ClassifierConfig{
category_confidence_threshold: cat,
tag_confidence_threshold: tag
}
when is_float(cat) and is_float(tag) ->
{cat, tag}

_ ->
{ClassifierConfig.default_category_threshold(), ClassifierConfig.default_tag_threshold()}
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve partial threshold overrides in thresholds_for_company/1.

Current logic requires both thresholds to be floats; if only one is configured, both fall back to defaults. That can show incorrect values compared to actual prediction behavior (which falls back per field in resolve_predictions/4).

Suggested fix
   def thresholds_for_company(company_id) do
     case ServiceConfig.get_classifier_config(company_id) do
-      %ClassifierConfig{
-        category_confidence_threshold: cat,
-        tag_confidence_threshold: tag
-      }
-      when is_float(cat) and is_float(tag) ->
-        {cat, tag}
+      %ClassifierConfig{} = config ->
+        {
+          config.category_confidence_threshold ||
+            ClassifierConfig.default_category_threshold(),
+          config.tag_confidence_threshold ||
+            ClassifierConfig.default_tag_threshold()
+        }

       _ ->
         {ClassifierConfig.default_category_threshold(), ClassifierConfig.default_tag_threshold()}
     end
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ksef_hub/invoice_classifier.ex` around lines 30 - 40,
thresholds_for_company/1 currently insists both category_confidence_threshold
and tag_confidence_threshold are floats and otherwise returns both defaults;
change it to preserve per-field overrides by fetching the config via
ServiceConfig.get_classifier_config(company_id), then for each field
(category_confidence_threshold and tag_confidence_threshold) use the configured
value if it is a float and fallback to
ClassifierConfig.default_category_threshold() or
ClassifierConfig.default_tag_threshold() respectively if not; update the
function to perform individual is_float checks (so behavior matches the
per-field fallback in resolve_predictions/4) while keeping references to
ClassifierConfig.default_category_threshold/ default_tag_threshold and
ServiceConfig.get_classifier_config.

Comment thread lib/ksef_hub/invoice_classifier.ex Outdated
Comment thread test/ksef_hub/invoice_classifier_test.exs Outdated
Comment thread test/ksef_hub/invoice_classifier/client_test.exs Outdated
Comment thread test/ksef_hub/invoice_classifier/client_test.exs Outdated
emilwojtaszek and others added 2 commits April 24, 2026 16:47
The SSRF private-IP block prevented health checks from reaching
localhost/private-network classifiers. Since the URL is configured
by admins (manage_services permission), not untrusted users, the
SSRF check is unnecessary and counterproductive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Run mix format across all changed files
- thresholds_for_company/1 now falls back per-field instead of
  returning both defaults when either is nil (matches resolve_predictions)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/ksef_hub_web/live/settings_live/services.ex (1)

307-319: ⚠️ Potential issue | 🟡 Minor

Probe only validated submitted URLs.

resolve_check_url/2 currently accepts any non-empty string, and Line 458 falls back to the persisted URL when the submitted one is blank. That means the save/check-health flow can probe a value different from what the user is trying to save, and malformed URLs still send the user through the Save anyway? path even though lib/ksef_hub/service_config/classifier_config.ex:97-107 will reject them afterward.

Suggested fix
   defp resolve_check_url(params, socket) do
     enabled = params["enabled"] in ["true", true]
     url = params["url"]

     cond do
-      enabled && is_binary(url) && url != "" -> url
-      enabled -> socket.assigns.config.url
+      enabled && valid_http_url?(url) -> url
       true -> nil
     end
   end

   `@spec` check_health_async(ServiceConfig.ClassifierConfig.t()) :: reference()
   defp check_health_async(config) do
-    url = if config.enabled, do: config.url
+    url = if(config.enabled && valid_http_url?(config.url), do: config.url)

     if url do
       %{ref: ref} =
         Task.Supervisor.async_nolink(KsefHub.TaskSupervisor, fn ->
           {:health_result, check_url_health(url)}
@@
       ref
     end
   end
+
+  `@spec` valid_http_url?(term()) :: boolean()
+  defp valid_http_url?(url) when is_binary(url) do
+    case URI.parse(url) do
+      %URI{scheme: scheme, host: host}
+      when scheme in ["http", "https"] and is_binary(host) and host != "" ->
+        true
+
+      _ ->
+        false
+    end
+  end
+
+  defp valid_http_url?(_), do: false

Also applies to: 335-336, 451-489

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

In `@lib/ksef_hub_web/live/settings_live/services.ex` around lines 307 - 319,
resolve_check_url/2 is allowing fallback to the persisted URL when the submitted
URL is blank, causing health checks to probe a different value than the user is
saving; change the flow so the health probe only runs for a submitted URL that
is non-blank and minimally validated (e.g., well-formed scheme/host) and does
not fall back to the persisted URL. Specifically, update resolve_check_url/2 to
return nil when the submitted URL is blank or fails basic validation, ensure the
Task.Supervisor async call to check_url_health only happens when
resolve_check_url/2 returns a validated submitted URL, and keep do_save/2
unchanged so classifier validation (classifier_config) still runs on save
errors. Reference: resolve_check_url/2, check_url_health/1, do_save/2, and the
classifier validation path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ksef_hub_web/live/settings_live/services.ex`:
- Line 12: The LiveView currently aliases and calls
KsefHub.ServiceConfig.ClassifierConfig directly for placeholder/default values;
instead, add and call a context function on KsefHub.ServiceConfig (e.g.,
get_classifier_defaults/0 or classifier_defaults/0) that returns those defaults,
and update this module to call KsefHub.ServiceConfig.get_classifier_defaults()
(or the chosen name) everywhere you previously referenced ClassifierConfig
(including the occurrences around lines 87-122) and remove the direct alias to
KsefHub.ServiceConfig.ClassifierConfig so callers use the ServiceConfig context
API only.
- Around line 40-43: The code currently starts a probe on every connected mount
by calling check_health_async(config) and assigning active_health_ref and
health, which causes disabled configs (where check_health_async/1 returns
:not_configured) to display as "Unreachable"; change the logic in the socket
setup so you only call check_health_async/1 and assign active_health_ref/health
when the config is enabled (e.g., check a flag on config or use a helper like
config_enabled?(config)); for disabled configs skip the probe and set health to
a neutral/disabled value (or leave it as nil) instead of :checking so the UI
does not mark a disabled config as unhealthy (refer to connected?(socket),
check_health_async/1, active_health_ref and health assigns).

---

Outside diff comments:
In `@lib/ksef_hub_web/live/settings_live/services.ex`:
- Around line 307-319: resolve_check_url/2 is allowing fallback to the persisted
URL when the submitted URL is blank, causing health checks to probe a different
value than the user is saving; change the flow so the health probe only runs for
a submitted URL that is non-blank and minimally validated (e.g., well-formed
scheme/host) and does not fall back to the persisted URL. Specifically, update
resolve_check_url/2 to return nil when the submitted URL is blank or fails basic
validation, ensure the Task.Supervisor async call to check_url_health only
happens when resolve_check_url/2 returns a validated submitted URL, and keep
do_save/2 unchanged so classifier validation (classifier_config) still runs on
save errors. Reference: resolve_check_url/2, check_url_health/1, do_save/2, and
the classifier validation path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a778d062-9bb1-4a6e-83dd-f169e4c0fbca

📥 Commits

Reviewing files that changed from the base of the PR and between 32a5dd6 and 82442f3.

📒 Files selected for processing (2)
  • lib/ksef_hub_web/live/settings_live/services.ex
  • test/ksef_hub_web/live/settings_live/services_test.exs
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/ksef_hub_web/live/settings_live/services_test.exs

Comment thread lib/ksef_hub_web/live/settings_live/services.ex Outdated
Comment thread lib/ksef_hub_web/live/settings_live/services.ex
emilwojtaszek and others added 2 commits April 24, 2026 17:22
- Add ServiceConfig.classifier_defaults/0 context function and remove
  direct ClassifierConfig alias from the LiveView
- Skip health probe on mount when config is disabled (avoids showing
  "Unreachable" for disabled classifiers)
- resolve_check_url no longer falls back to persisted URL — only probes
  the submitted URL the user is actually saving

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The async health check to an unreachable URL takes variable time
depending on CI load. Replace Process.sleep(200) with a polling
loop that waits up to 2s for the "Save anyway" dialog to appear.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@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)
lib/ksef_hub_web/live/settings_live/services.ex (1)

435-448: Consider exposing a type alias from the context module.

The @spec references ServiceConfig.ClassifierConfig.t() directly. While this is acceptable for type specs (the guideline targets function calls, not types), exposing a type alias like ServiceConfig.classifier_config() would maintain cleaner module boundaries.

💡 Optional: Add type alias to ServiceConfig

In lib/ksef_hub/service_config.ex:

`@type` classifier_config :: ClassifierConfig.t()

Then update the spec:

-  `@spec` build_form(ServiceConfig.ClassifierConfig.t()) :: Phoenix.HTML.Form.t()
+  `@spec` build_form(ServiceConfig.classifier_config()) :: Phoenix.HTML.Form.t()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ksef_hub_web/live/settings_live/services.ex` around lines 435 - 448, Add
a public type alias in the ServiceConfig module (e.g., `@type` classifier_config
:: ClassifierConfig.t()) and then update the `@spec` on build_form to reference
the alias (change ServiceConfig.ClassifierConfig.t() to
ServiceConfig.classifier_config()). This keeps type references cleaner while
leaving the function implementation (build_form/1) and its use of
ServiceConfig.change_classifier_config/2 unchanged.
lib/ksef_hub/invoice_classifier.ex (1)

99-107: Consider logging decryption failures for observability.

When Encryption.decrypt/1 fails, the function silently returns nil. While this allows classification to proceed (perhaps the service doesn't require auth), it could mask configuration issues. A warning-level log would help operators diagnose misconfigured tokens without blocking classification.

💡 Optional: Add warning log on decryption failure
   defp decrypt_token(encrypted) do
     case Encryption.decrypt(encrypted) do
       {:ok, token} -> token
-      {:error, _} -> nil
+      {:error, reason} ->
+        Logger.warning("Failed to decrypt classifier API token: #{inspect(reason)}")
+        nil
     end
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ksef_hub/invoice_classifier.ex` around lines 99 - 107, In decrypt_token/1
in the InvoiceClassifier module, keep the existing behavior but log a warning
when Encryption.decrypt(encrypted) returns {:error, reason}; update the clause
handling {:error, _} to call Logger.warn/1 (or your app logger) with a clear
message including the encrypted input identifier and the inspected reason
(masked or truncated if sensitive) before returning nil so decryption failures
are observable without changing return semantics.
test/ksef_hub/invoice_classifier_test.exs (1)

440-460: Consider using distinct values in first test to verify config is actually read.

The test at line 442 asserts {0.71, 0.95} which are both the configured values and the defaults. This test would pass even if thresholds_for_company/1 always returned defaults. The test at lines 449-458 correctly uses distinct values (0.5, 0.8) to prove custom config is read.

Consider updating the setup thresholds to differ from defaults, or this specific test is essentially a duplicate of "returns defaults when no config exists".

💡 Optional: Use distinct thresholds in setup
   `@category_threshold` 0.71
   `@tag_threshold` 0.95
+  # Note: These match defaults. For clearer testing, consider using distinct values:
+  # `@category_threshold` 0.75
+  # `@tag_threshold` 0.90

Or update the first test to use the other_company pattern with distinct thresholds.

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

In `@test/ksef_hub/invoice_classifier_test.exs` around lines 440 - 460, The
"returns thresholds from classifier config" test is ineffective because it
asserts values identical to the defaults; update the test so it verifies actual
config lookup by configuring distinct thresholds for the existing setup company
(or follow the "other_company" pattern) and then assert
InvoiceClassifier.thresholds_for_company(company.id) returns those distinct
values; reference the test description "returns thresholds from classifier
config" and the function InvoiceClassifier.thresholds_for_company/1 when making
the change.
🤖 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/settings_live/services.ex`:
- Around line 435-448: Add a public type alias in the ServiceConfig module
(e.g., `@type` classifier_config :: ClassifierConfig.t()) and then update the
`@spec` on build_form to reference the alias (change
ServiceConfig.ClassifierConfig.t() to ServiceConfig.classifier_config()). This
keeps type references cleaner while leaving the function implementation
(build_form/1) and its use of ServiceConfig.change_classifier_config/2
unchanged.

In `@lib/ksef_hub/invoice_classifier.ex`:
- Around line 99-107: In decrypt_token/1 in the InvoiceClassifier module, keep
the existing behavior but log a warning when Encryption.decrypt(encrypted)
returns {:error, reason}; update the clause handling {:error, _} to call
Logger.warn/1 (or your app logger) with a clear message including the encrypted
input identifier and the inspected reason (masked or truncated if sensitive)
before returning nil so decryption failures are observable without changing
return semantics.

In `@test/ksef_hub/invoice_classifier_test.exs`:
- Around line 440-460: The "returns thresholds from classifier config" test is
ineffective because it asserts values identical to the defaults; update the test
so it verifies actual config lookup by configuring distinct thresholds for the
existing setup company (or follow the "other_company" pattern) and then assert
InvoiceClassifier.thresholds_for_company(company.id) returns those distinct
values; reference the test description "returns thresholds from classifier
config" and the function InvoiceClassifier.thresholds_for_company/1 when making
the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 90e2da22-e105-449b-a4aa-b9758f33bc0f

📥 Commits

Reviewing files that changed from the base of the PR and between 82442f3 and 66c7089.

📒 Files selected for processing (5)
  • lib/ksef_hub/invoice_classifier.ex
  • lib/ksef_hub/service_config.ex
  • lib/ksef_hub_web/live/settings_live/services.ex
  • test/ksef_hub/invoice_classifier/client_test.exs
  • test/ksef_hub/invoice_classifier_test.exs
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/ksef_hub/service_config.ex
  • test/ksef_hub/invoice_classifier/client_test.exs

- decrypt_token/1 now logs a warning when decryption fails instead
  of silently returning nil
- thresholds_for_company test uses distinct values (0.60/0.85)
  instead of matching defaults, proving actual config lookup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/ksef_hub_web/live/settings_live/services_test.exs`:
- Around line 145-164: The test relies on a real network failure; replace that
with a Mox-based mock: introduce or use an HTTP client behaviour (the calls in
check_url_health/1 that currently call Req.get) and update check_url_health/1 to
call the injected client module instead of Req directly, then in
test/ksef_hub_web/live/settings_live/services_test.exs set a Mox expectation for
the client to return a deterministic error response for "http://127.0.0.1:19999"
before rendering/submitting the form so the async health-check task immediately
sees the mocked failure and the test can assert the "Save anyway" UI without
real network timing. Ensure the test config sets the app to use the mock client
and verify the mock is verified after the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: efe4418b-7c36-4e6a-8386-b5bfe5dd85cb

📥 Commits

Reviewing files that changed from the base of the PR and between 66c7089 and e808ba1.

📒 Files selected for processing (1)
  • test/ksef_hub_web/live/settings_live/services_test.exs

Comment on lines +145 to +164
url: "http://127.0.0.1:19999",
category_confidence_threshold: "0.80",
tag_confidence_threshold: "0.90"
}

# Submit with a private IP — shows confirmation dialog
# Submit with an unreachable URL — shows confirmation dialog
view
|> form("form[phx-submit=save]", classifier: form_params)
|> render_submit()

# Wait for the async health-check task to deliver its result
Process.sleep(50)
html = render(view)
# Poll until the async health-check task completes and renders "Save anyway"
html =
Enum.reduce_while(1..20, nil, fn _, _acc ->
Process.sleep(100)
html = render(view)

if html =~ "Save anyway",
do: {:halt, html},
else: {:cont, html}
end)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify this test still relies on real network timing and does not use Mox in this flow.
rg -n -C3 --type=elixir '127\.0\.0\.1:19999|Process\.sleep|Enum\.reduce_while|Mox|expect\(' test/ksef_hub_web/live/settings_live/services_test.exs
rg -n -C3 --type=elixir 'check_url_health\(' lib/ksef_hub_web/live/settings_live/services.ex

Repository: appunite/appunite-ksef-ex

Length of output: 1573


Mock pre-save health checks instead of relying on real unreachable URLs.

This test violates the coding guideline "Use Mox for mocking external services in tests" by relying on real network failure timing. The check_url_health/1 function at line 477-480 in services.ex makes real HTTP requests via Req.get(), and this test expects that call to fail on 127.0.0.1:19999. Instead, mock the Req call to return a deterministic error response, eliminating flakiness and improving test speed.

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

In `@test/ksef_hub_web/live/settings_live/services_test.exs` around lines 145 -
164, The test relies on a real network failure; replace that with a Mox-based
mock: introduce or use an HTTP client behaviour (the calls in check_url_health/1
that currently call Req.get) and update check_url_health/1 to call the injected
client module instead of Req directly, then in
test/ksef_hub_web/live/settings_live/services_test.exs set a Mox expectation for
the client to return a deterministic error response for "http://127.0.0.1:19999"
before rendering/submitting the form so the async health-check task immediately
sees the mocked failure and the test can assert the "Save anyway" UI without
real network timing. Ensure the test config sets the app to use the mock client
and verify the mock is verified after the test.

@emilwojtaszek emilwojtaszek merged commit ccad3cd into main Apr 24, 2026
3 checks passed
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