feat(settings): admin-editable currency via settings table - #30
Merged
Conversation
Replaces the hardcoded `$CURRENCY_CODE = 'USD'` in includes/load.php
with a DB-backed value an admin can change from a Settings page. One
currency per deployment (single-tenant); per-org/per-user currency
remains future work and requires a tenancy model the codebase
doesn't have today.
Pieces
------
- migrations/004_settings_table.{up,down}.sql — new `settings`
table with `setting_key` PK + `setting_value` + `updated_at`,
seeded with `currency_code = 'USD'`. schema.sql also gains the
table so a fresh install (and CI) has it from minute one.
- includes/settings.php — `Settings::get($key, $default)` /
`Settings::set($key, $value)` with per-request cache.
Settings::load() swallows a missing-table error and uses defaults,
so the brief window between a deploy and applying migration 004
doesn't 500 the whole site.
- includes/database.php — new `connection()` getter so Settings
can run a query that handles its own errors instead of dying
through `query()`/`prepare_query()`.
- includes/formatcurrency.php — extracted the ISO 4217 table into
a memoized `currency_table()` helper and added
`supported_currency_codes()` (sorted list of keys), so the
Settings page can validate POSTed codes against the exact same
list `formatcurrency()` knows how to render. `formatcurrency()`
also gains a safety fallback to USD when handed an unknown code,
instead of throwing on undefined index.
- includes/load.php — `$CURRENCY_CODE = Settings::get('currency_code', 'USD')`.
- users/settings.php — admin-only (`page_require_level(1)`), CSRF-
protected, dropdown of supported codes + live "1234.56" sample
rendered through formatcurrency() at the chosen code so the
admin sees exactly what the system will produce.
- layouts/admin_menu.php — link under User Management → Settings.
Tests
-----
- tests/SettingsTest.php (integration): supported list shape, get()
default fallback, set() upsert + cache-invalidation round-trip,
currency_code → formatcurrency() round-trip, unknown-code
fallback. Skips with a clear hint when the settings table is
absent locally (migration not yet applied) so a dev clone without
the migration still runs the rest of the suite cleanly.
- tests/run.sh wired in the new suite. tests/bootstrap.php now
requires settings.php + formatcurrency.php.
Deploy
------
After merge, on each existing deployment:
sudo mysqldump --single-transaction inventory > inventory-pre-004.sql
sudo mysql inventory < migrations/004_settings_table.up.sql
The fallback in Settings::load() means traffic between the deploy
and the migration apply continues to render as USD instead of
erroring; the migration window is non-fatal.
gap-analysis.md item 2 from the post-PR-27 list is now resolved
(single-tenant scope). migrations/README.md index updated to cover
002/003/004 (rows for the new migration and to backfill the prior
two that were never indexed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
formatcurrency() emits the euro sign as `€` (HTML entity) so that the value is safe to drop directly into an HTML page. CI's SettingsTest checked for the literal `€` character and failed against `€1.234,56`. Decode through html_entity_decode() before substring-matching. No production behaviour changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
bitsandbots
added a commit
to jleog/inventory
that referenced
this pull request
May 22, 2026
First of three deferred sub-projects from next_steps_inventory.md
("Deferred work" item 1). Scope:
- 5 in-scope tables: users, customers, sales, orders, stock
- New helpers in includes/sql.php: soft_delete_by_id, restore_by_id,
purge_by_id, find_with_deleted, find_by_id_with_deleted,
table_has_soft_delete (cached schema introspection).
- find_all/find_by_id default-filter deleted_at IS NULL; raw-SQL
helpers hand-edited per the in-scope table list.
- Migrations 005-009 add deleted_at + deleted_by columns + FK to
users(id) ON DELETE SET NULL (mirrors PR bitsandbots#27's fk_log_user).
- Admin-only users/trash.php + restore.php + purge.php; row-local
cascade (a deleted customer's existing sales still show).
- New tests/SoftDeleteTest.php (10 cases) wired into tests/run.sh.
Deploy follows PR bitsandbots#30's defensive pattern: table_has_soft_delete()
returns false until the column lands, so traffic between code-deploy
and migration-apply still serves correct unfiltered reads.
Awaiting user review before writing-plans is invoked.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the hardcoded
$CURRENCY_CODE = 'USD'inincludes/load.phpwith a DB-backed value an admin can change from a new Settings page. Single-tenant — one currency per deployment.settings(setting_key, setting_value, updated_at)table (migration 004 + schema.sql).Settings::get/setwith per-request cache (includes/settings.php); gracefully degrades to defaults if the table is missing (e.g. brief window between deploy and migration apply)./users/settings.phppage: CSRF-protected dropdown of the ~91 ISO 4217 codesformatcurrency()already supports, with a live sample render of1234.56at the chosen code.formatcurrency.phprefactor: the currency table is now exposed through a memoizedcurrency_table()helper +supported_currency_codes()so the form validates against the exact list the renderer knows. Unknown codes fall back to USD instead of throwing on undefined-index.SettingsTest.phpintegration suite; skips cleanly when the local DB doesn't have the table yet.After merge, on each existing deployment:
CI doesn't need this —
schema.sqlis the canonical fresh state and already contains the table.bash tests/run.sh— 5/5 suites pass locally (SettingsTest skipped because migration not applied here)php -lclean across all modified PHP (caught by the new pre-commit hook from PR chore(hooks): add pre-commit php -l hook (opt-in) #29)formatcurrency()regression: unknown code returns$100.00USD instead of warningstyle-src 'self', login page has 0 inline styles (pre-existing behaviour unchanged)SettingsTestagainst the schema.sql-importedsettingstable (i.e. it does NOT skip in CI)/users/settings.phpis redirected bypage_require_level(1)Per-organization / per-user currency requires a tenancy model the codebase doesn't have. Captured in
docs/gap-analysis.mdas a follow-up.🤖 Generated with Claude Code