This document provides guidance for AI agents working on the Open Source Point of Sale (OSPOS) codebase.
- PSR-12 enforced via PHP-CS-Fixer (config:
.php-cs-fixer.no-header.php) - Follow PHP CodeIgniter 4 coding standards
camelCasefor variables and methods;PascalCasefor classes;UPPER_CASEfor constants- When editing existing code containing non-PSR-compliant local variable names, refactor those variable names to
camelCaseas part of the edit - All newly written code (variables, classes, functions) must use PSR-compliant naming, regardless of surrounding code style
- PHP 8.2+ features acceptable (named arguments, enums, readonly properties)
- Write PHP 8.2+ compatible code with proper type declarations
- Always import classes, functions, and constants with a
usestatement at the top of the file instead of referencing them inline via fully-qualified name (e.g.use Config\Database;thenDatabase::connect(), not\Config\Database::connect()) - No useless comments or docblocks: if the code speaks for itself, explanatory comments are unnecessary. Do not add comments or docblocks that merely restate what the code already makes clear (e.g.
@param array $itemsnext toarray $items, or a comment repeating a method's name) — only comment on non-obvious rationale, constraints, or behavior - Views in
app/Views/errors/html/are excluded from the fixer - Run fixer before committing:
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php - JavaScript: use
constfor variables that are never reassigned,letfor variables that are. Never usevar.
- Create a new git worktree for each issue, based on the latest state of
origin/master - Commit fixes to the worktree and push to the remote
- Run PHPUnit tests:
composer test - Tests must pass before submitting changes
- One test file per class under test. A controller, model, library, or helper gets exactly one test file covering all of its behavior —
Item_kits.php→tests/Controllers/Item_kitsTest.php(orItem_kitsControllerTest.php, matching this codebase's existing*ControllerTest.phpsuffix for controllers),Sale_lib.php→tests/Libraries/Sale_libTest.php, etc. Do not create feature- or endpoint-scoped test files alongside a class's main test file (e.g. noItem_kitsBarcodeTest.phpnext toItem_kitsControllerTest.php) — add the new test methods to the existing file for that class instead. If no test file exists yet for the class, create the one canonical file rather than a narrowly-scoped one. - Name test methods as
test{MethodUnderTest}_{Behavior}.PascalCasethe method under test, then an underscore, then a short behavior phrase —testPostSave_RejectsNegativeTaxPercent,testSaveLocale_AcceptsValidReferenceCodeMinMax. Start the behavior with a verb:Accepts/Requiresfor valid or boundary cases,Rejectsfor invalid ones. The name should read as a self-documenting sentence and needs no docblock. - Resetting the test database (full migration reset, not just
$refresh). When a test class needs theospos_testsschema fully dropped and re-migrated from scratch (e.g. soospos_permissions/app_configget freshly seeded),use Tests\Support\DatabaseBootstrapTrait;and call$this->resetDatabaseOnce()as the first line ofsetUp(), beforeparent::setUp():use Tests\Support\DatabaseBootstrapTrait; class FooTest extends CIUnitTestCase { use DatabaseTestTrait; use DatabaseBootstrapTrait; protected $migrate = true; protected $migrateOnce = true; protected $seedOnce = true; protected $refresh = false; protected $namespace = null; protected function setUp(): void { $this->resetDatabaseOnce(); parent::setUp(); // ... } }
DatabaseBootstrapTrait(tests/Support/DatabaseBootstrapTrait.php) wrapsTestDatabaseBootstrapSeeder::reset()with a per-class static guard so it only runs once per test class. Do not reinvent this with a localprivate static bool $doneBootstrapflag,setUpBeforeClass(), orConfig::seeder($this->DBGroup)->call('App\Database\Seeds\TestDatabaseBootstrapSeeder')followed by a manualConfig::connect($this->DBGroup)->close()/resetDataCache()in the test file — those are the old patterns the trait replaces, and the manual version reintroduces the exact stale-connection bugTestDatabaseBootstrapSeeder::reset()already fixes internally. Only add this trait to test classes that actually touch the real test database (DatabaseTestTraitusers) — pure unit tests that mock everything don't need it. If a test class readsconfig(OSPOS::class)->settingsfor real (not mocking it out), also callconfig(OSPOS::class)->update_settings();right afterparent::setUp();to force a fresh settings read post-reset. Seetests/Controllers/ReportsControllerTest.phpfor that shape. - Mocking OSPOS config settings. When a test needs
config(OSPOS::class)->settingsto return fixed values instead of reading from the database,use Tests\Support\OsposConfigFixtureTrait;and call$this->injectOsposSettings([...])instead of manually constructingnew OSPOS()+Factories::injectMock('config', OSPOS::class, $ospos):use Tests\Support\OsposConfigFixtureTrait; class FooTest extends CIUnitTestCase { use OsposConfigFixtureTrait; protected function setUp(): void { parent::setUp(); $this->injectOsposSettings([ 'number_locale' => 'en_US', 'currency_decimals' => 2, ]); } }
OSPOS::getDefaultSettings()isprivateand only covers a handful of keys, so it is not a usable base to merge onto — each test must supply the exact settings it depends on. Use this trait regardless of whether the test also usesDatabaseBootstrapTrait.
- Install dependencies:
composer install && npm install - Build assets:
npm run buildorgulp
- Controllers go in
app/Controllers/ - Models go in
app/Models/ - Views go in
app/Views/ - Database migrations in
app/Database/Migrations/ - Use CodeIgniter 4 framework patterns and helpers
- Sanitize user input; escape output using
esc()helper
- When adding new keys to language files, add the key to all
app/Language/*/variants - New keys must be inserted in alphabetical order within the language array
- Non-English files must use an empty string (
'') as the value when no translation is provided — CodeIgniter automatically falls back to the default (en) language. This applies only when a translation genuinely isn't available yet. - When explicitly asked to translate a phrase for a non-English language file, always provide the actual translation — never leave the value as an empty string, and never leave source English text in a non-English language file
- Never copy English text from a neighboring key as a value for a non-English language file, even if that neighboring key is already untranslated — evaluate each key independently
- Only
app/Language/en/andapp/Language/en-GB/should contain English strings - When translating a string containing placeholders (e.g.
{filePath},{reason}) or literal filenames/keys (e.g.throttle.key,.env), keep that placeholder or filename text unchanged and untranslated — move it to whatever position is grammatically correct in the translated sentence - Interpolate user-facing strings with
lang()and named placeholders (e.g.{company},{count}) rather than string concatenation or positional placeholders ({0},{1}) — this lets a translation reorder the placeholder to fit grammar, and named placeholders are self-documenting for translators (seeapp/Helpers/security_helper.php:441). CodeIgniter 4.7+ formats these via ICUMessageFormatter(theintlextension is a hard dependency), so ICU MessageFormat syntax is also available where a string needs it, e.g.lang('...', ['count' => $n])with'{count, plural, =0 {None} one {# item} other {# items}}' - Because placeholders are formatted by ICU
MessageFormatter, a placeholder-bearing translation must be valid ICU MessageFormat: double any literal braces ({{/}}) and any apostrophe ('') to render them literally, orMessageFormatterfails and CodeIgniter appends a formatting warning to the output. Placeholder-free strings are never passed to ICU, so they are unaffected - Plugin language files (
app/Plugins/*/Language/) follow the same localization rules asapp/Language/ - Use
'to encapsulate key and string values. If the value contains'then it should be escaped as\' - Align the
=>of a newly inserted key with the=>column already used by the rest of the file, if that file pads keys to a fixed column (not all do — some files have no padding at all). If the new key is longer than the widest existing key and would push the alignment column further right, reformat the whole file to the new wider column rather than leaving only the new line at a different width
- Never commit secrets, credentials, or
.envfiles - Use parameterized queries to prevent SQL injection
- Validate and sanitize all user input
- Never reference security advisory IDs (CVE, GHSA, etc.) in code, comments, commit messages, docblocks, documentation, or URLs — treat them the same as secrets. They act as a roadmap for attackers researching the exact exploit a fix addresses.