Files
opensourcepos/AGENTS.md
T

9.9 KiB

Agent Instructions

This document is the single source of truth for all AI agents working on the Open Source Point of Sale (OSPOS) codebase. Read it fully before making any changes.

Project Overview

OpenSourcePOS is a web-based Point of Sale system built on CodeIgniter 4 (PHP 8.2+) with MySQL/MariaDB. Frontend uses Bootstrap 3 (Bootstrap 5 migration in progress) and jQuery, with assets built via Gulp.

Common Commands

# PHP dependencies
composer install

# Frontend dependencies and asset build
npm install
npm run build          # Runs Gulp: compiles and copies all CSS/JS to public/resources/

# Run full test suite
composer test

# Run a single test file
vendor/bin/phpunit tests/unit/AppTest.php

# Lint / code style check
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php --dry-run

# Apply code style fixes
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php

Tests require a MariaDB/MySQL database (see CI config in .github/workflows/phpunit.yml).

Architecture

Framework & Entry Point

  • Framework: CodeIgniter 4 — MVC with QueryBuilder ORM, no Eloquent
  • Web root: public/public/index.php is the only entry point
  • Routes: app/Config/Routes.php
  • App config: app/Config/App.php (version, session, security settings)
  • Environment: .env file (copy from .env.example); CI_ENVIRONMENT controls dev/prod/test mode

Directory Layout

app/
├── Config/         # CI4 config classes
├── Controllers/    # ~27 controllers (Sales, Items, Reports, Customers, etc.)
├── Models/         # ~28 models (Sale, Item, Customer, Supplier, etc.)
├── Views/          # PHP view templates
├── Libraries/      # Business logic (Sale_lib, Tax_lib, Receiving_lib, etc.)
├── Plugins/        # Plugin system — each plugin is a subdirectory here
├── Database/       # Migrations (ospos_ prefix) and seeds
├── Language/       # i18n files (IETF BCP 47 locale names)
├── Filters/        # Request/response filters (auth, HTTPS, etc.)
└── Events/         # CI4 event subscribers
public/
└── resources/      # Built CSS/JS (do not edit directly — generated by npm run build)
tests/              # PHPUnit test suite

Key Libraries

app/Libraries/ holds core business logic:

  • Sale_lib.php — sale cart state, pricing, discounts, tax calculation
  • Tax_lib.php — multi-tier tax engine
  • Receiving_lib.php — purchase orders / receivings
  • Barcode_lib.php — barcode generation
  • Email_lib.php — email delivery
  • Token_lib.php — CSRF/session token management

Database

  • Table prefix: ospos_ (defined in app/Config/Database.php)
  • Migrations live in app/Database/Migrations/ and run automatically on first access
  • CodeIgniter QueryBuilder throughout — no raw SQL unless necessary

Plugin System

MANDATORY: Read app/Plugins/README.md before doing any work related to plugins. It is the authoritative reference for plugin architecture, event contracts, view hook points, the $pluginData pass-through mechanism, language file conventions, migrations, and LICENSE requirements.

Plugins live in app/Plugins/<PluginName>/ and are auto-discovered by PluginManager. Each plugin:

  • Extends BasePlugin or implements PluginInterface
  • Registers event hooks in registerEvents() using Events::on()
  • Can include its own Views/, Models/, Controllers/, Language/, and Migrations/ subdirectories
  • Configuration stored in ospos_plugin_config table
  • Plugin-specific routes go in Config/Routes.php within the plugin directory

View Hook Points

Core views inject plugin UI using pluginContent('hook_name', $data). All currently defined hook points are listed in app/Plugins/README.md. When adding a new hook point to a core view:

  1. Call pluginContent('your_hook_name', $data) at the injection point
  2. If the hook is inside or adjacent to a form whose controller fires Events::trigger(), add data-plugin-form="true" to that form tag so plugin_data_helper.js serializes any [data-plugin-field] inputs into the POST

Plugin Data Pass-through ($pluginData)

All Events::trigger() calls in core controllers pass a final array $pluginData argument decoded from the POST field plugin_data. Plugin listeners declare array $pluginData = [] as the last parameter. Plugin-injected inputs use data-plugin-field="pluginid_varname" (namespaced to the plugin ID) — the core JS collects and serializes them automatically. Never add plugin-specific logic to core controllers to read individual keys; the entire opaque array is forwarded as-is.

Frontend Build

gulpfile.js (Gulp 5) copies vendor CSS/JS from node_modules/ into public/resources/. Run npm run build after installing npm packages or changing gulp tasks. Do not manually edit files under public/resources/.

Code Style

  • PSR-12 enforced via PHP-CS-Fixer (config: .php-cs-fixer.no-header.php)
  • Follow PHP CodeIgniter 4 coding standards
  • camelCase for variables and methods; PascalCase for classes; UPPER_CASE for constants
  • When editing existing code containing non-PSR-compliant local variable names, refactor those variable names to camelCase as 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 use statement at the top of the file instead of referencing them inline via fully-qualified name (e.g. use Config\Database; then Database::connect(), not \Config\Database::connect())
  • Do not add comments or docblocks that merely restate what the code already makes clear — 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 const for variables that are never reassigned, let for variables that are. Never use var.

Development Workflow

  • 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
  • Tests must pass before submitting changes (composer test)
  • Minimum PHPUnit version: 10.5.16+. Default config: phpunit.xml.dist

Plugin Tests

Each plugin may ship its own Tests/ subdirectory. PHPUnit discovers them automatically via the Plugins testsuite in phpunit.xml.dist.

Convention:

app/Plugins/{PluginName}/
└── Tests/
    └── {PluginName}Test.php    # namespace App\Plugins\{PluginName}\Tests

Base class: Extend Tests\Support\PluginTestCase (registers plugin namespaces in setUp). Pure-PHP tests may extend CodeIgniter\Test\CIUnitTestCase directly.

  • 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.phptests/Controllers/Item_kitsTest.php (or Item_kitsControllerTest.php, matching this codebase's existing *ControllerTest.php suffix for controllers), Sale_lib.phptests/Libraries/Sale_libTest.php, etc. Do not create feature- or endpoint-scoped test files alongside a class's main test file (e.g. no Item_kitsBarcodeTest.php next to Item_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.
vendor/bin/phpunit --testsuite Plugins          # plugins only
vendor/bin/phpunit app/Plugins/CASPOSPlugin/Tests/  # single plugin

No changes to composer.json are needed — App\Plugins\{Name}\Tests\* resolves via the existing App\app/ PSR-4 mapping.

Conventions

  • Controllers → app/Controllers/
  • Models → app/Models/
  • Views → app/Views/
  • Migrations → app/Database/Migrations/
  • Plugins → app/Plugins/ (see app/Plugins/README.md for plugin structure, event hooks, and LICENSE requirements)
  • Use CodeIgniter 4 framework patterns and helpers
  • Sanitize user input; escape output using esc() helper

Localization

  • 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/ and app/Language/en-GB/ should contain English strings
  • Plugin language files (app/Plugins/*/Language/) follow the same localization rules as app/Language/
  • Use ' to encapsulate key and string values. If the value contains ' then it should be escaped as \'

Security

  • app.allowedHostnames must be set in production (host header injection protection)
  • HTMLPurifier for HTML sanitization; Laminas Escaper for output escaping
  • CSRF tokens managed via Token_lib — do not bypass CI4's CSRF filter
  • Session storage is database-backed (ospos_sessions table) for multi-instance support
  • Never commit secrets, credentials, or .env files
  • Use parameterized queries to prevent SQL injection
  • Validate and sanitize all user input