nixos-module: work around upstream language-list caching bug

Migration 2025_08_31_224356_additional_languages_af_ca_id.php runs
during `migrate`, before db:seed's LanguageSeeder has populated the
other 42 rows of the languages table, and does
Cache::forever('languages', Language::all()) - permanently caching
just the 3 languages (Afrikaans, Catalan, Indonesian) it sees at that
moment. StaticServiceProvider checks this cache key before ever
querying the DB again, so every fresh install's language dropdown
(e.g. the "Welcome to Invoice Ninja" company setup wizard) was stuck
showing only those 3 entries, in their own native-language spelling
(ctrans() translates each name using the active locale).

Adds an unconditional `invoiceninja-manage cache:forget languages` to
invoiceninja-data-setup.service, and a regression test asserting
/api/v1/statics returns more than 40 languages. Verified directly:
before the fix, a fresh account's statics call returned exactly 3
languages; after, it returns all 45.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 15:17:22 -04:00
co-authored by Claude Sonnet 5
parent d7fea37e25
commit 7d7d717745
3 changed files with 30 additions and 1 deletions
+7 -1
View File
@@ -97,7 +97,13 @@ expect Nix to report the correct hash on a failed build (or use `nix-prefetch`/`
clears bootstrap/opcode cache, rebuilds `.env`, rsyncs static storage into `dataDir`, symlinks the runtime clears bootstrap/opcode cache, rebuilds `.env`, rsyncs static storage into `dataDir`, symlinks the runtime
`app.php`, and guards one-time actions (`migrate`, `db:seed`, admin account creation) behind marker files `app.php`, and guards one-time actions (`migrate`, `db:seed`, admin account creation) behind marker files
(`.initial-migration`, `.db-seeded`, `.admin-created`) in `dataDir`. `phpfpm-invoiceninja.service` and (`.initial-migration`, `.db-seeded`, `.admin-created`) in `dataDir`. `phpfpm-invoiceninja.service` and
`invoiceninja-queue-worker.service` both depend on it. `invoiceninja-queue-worker.service` both depend on it. It also unconditionally runs
`invoiceninja-manage cache:forget languages` on every activation, working around an upstream bug: migration
`2025_08_31_224356_additional_languages_af_ca_id.php` runs during `migrate` (before `db:seed`'s
`LanguageSeeder` has populated the other 42 rows) and does `Cache::forever('languages', Language::all())`,
permanently caching just the 3 languages it sees at that moment — without this, the language dropdown in
the app stays stuck at 3 entries forever, since `StaticServiceProvider` checks that cache key before ever
querying the DB again.
- `invoiceninja-scheduler.timer`/`.service` run `artisan schedule:run` every minute — this is what drives - `invoiceninja-scheduler.timer`/`.service` run `artisan schedule:run` every minute — this is what drives
Invoice Ninja's own internal cron-scheduled jobs (recurring invoices, scheduled reports, etc.); without it the Invoice Ninja's own internal cron-scheduled jobs (recurring invoices, scheduled reports, etc.); without it the
app's dashboard reports "cron jobs are not enabled". Laravel's scheduler is just a dispatcher, so this one app's dashboard reports "cron jobs are not enabled". Laravel's scheduler is just a dispatcher, so this one
+8
View File
@@ -696,6 +696,14 @@ in
# Necessary for languages, currencies, countries, etc. # Necessary for languages, currencies, countries, etc.
[[ ! -f ${cfg.dataDir}/.db-seeded ]] && invoiceninja-manage db:seed --force && touch ${cfg.dataDir}/.db-seeded [[ ! -f ${cfg.dataDir}/.db-seeded ]] && invoiceninja-manage db:seed --force && touch ${cfg.dataDir}/.db-seeded
# Work around an upstream bug: migration 2025_08_31_224356_additional_languages_af_ca_id.php
# runs during `migrate` (before db:seed's LanguageSeeder has populated the other 42 rows) and
# does `Cache::forever('languages', Language::all())`, permanently caching just the 3 languages
# it sees at that moment. Without this, the language list stays stuck at those 3 forever,
# since StaticServiceProvider checks this cache key before ever querying the DB again.
# Unconditional (not marker-gated) so it also self-heals installs that hit this before the fix.
invoiceninja-manage cache:forget languages
# Create Invoice Ninja admin account # Create Invoice Ninja admin account
[[ (! -f ${cfg.dataDir}/.admin-created) && (${ [[ (! -f ${cfg.dataDir}/.admin-created) && (${
if cfg.adminAccount.enable then "true" else "false" if cfg.adminAccount.enable then "true" else "false"
+15
View File
@@ -21,6 +21,8 @@ pkgs.testers.nixosTest {
}; };
testScript = '' testScript = ''
import json
def check_invoiceninja(machine, proxy_unit): def check_invoiceninja(machine, proxy_unit):
machine.wait_for_unit("mysql.service") machine.wait_for_unit("mysql.service")
machine.wait_for_unit("redis-invoiceninja.service") machine.wait_for_unit("redis-invoiceninja.service")
@@ -65,6 +67,19 @@ pkgs.testers.nixosTest {
) )
assert "${adminEmail}" in login_response, f"login did not succeed: {login_response}" assert "${adminEmail}" in login_response, f"login did not succeed: {login_response}"
with subtest("full language list is available, not just the af/ca/id migration's 3"):
# Regression check: migration 2025_08_31_224356_additional_languages_af_ca_id.php runs
# during `migrate`, before db:seed's LanguageSeeder has populated the other 42 rows, and
# does Cache::forever('languages', Language::all()) - permanently caching just the 3
# languages it sees at that moment. invoiceninja-data-setup works around this with
# `invoiceninja-manage cache:forget languages`; this confirms the workaround is in place.
token = json.loads(login_response)["data"][0]["token"]["token"]
statics = machine.succeed(
f"curl -sf http://localhost/api/v1/statics -H 'X-API-TOKEN: {token}'"
)
language_count = len(json.loads(statics)["languages"])
assert language_count > 40, f"expected ~45 languages, got {language_count}"
nginx.start() nginx.start()
caddy.start() caddy.start()