Conventions and hard rules
These aren’t style preferences. Each rule below exists because breaking it once already cost real time or real data. The full source of truth is each repo’s own CLAUDE.md; this page is the onboarding-friendly version.
No em-dashes, anywhere
Section titled “No em-dashes, anywhere”Never use the em-dash character (the long dash punctuation mark, U+2014) in any output for this project: chat replies, code comments, commit messages, PR descriptions, changelogs, docs (including this site). Use a period, comma, or parenthesis instead. This has been asked for and violated repeatedly enough that it’s now a hard rule, not something to weigh against other considerations.
Database safety {#database-safety}
Section titled “Database safety {#database-safety}”Incident (2026-08-31): a PHPUnit test using RefreshDatabase ran migrate:fresh against the real local central database instead of the isolated SQLite test database, wiping ~34 central tables (tenants, websites, domains, packages, features, admins, subscriptions). Root cause: composer install/composer require unconditionally caches config (artisan config:cache), which bakes the real .env database values into bootstrap/cache/config.php. A cached config() call ignores phpunit.xml’s env overrides entirely, since those only affect raw env()/getenv() reads. Tenant business databases were untouched; the central registry was gone, with no way to reconstruct rows that were never in the binlog’s change window.
Structural fix in place: dg_smart_pos_api/tests/bootstrap.php deletes any cached config/route files before every test run, unconditionally. Do not remove that block or add a “skip if already cleared” shortcut to it.
Rules that apply to every session in dg_smart_pos_api, not just automated tests:
- After any
composer install/update/require, the next command must bephp artisan config:clear, before touching a database in any way. - Before any schema/data-mutating command (
migrate,migrate:fresh,migrate:refresh,db:seed, a new test usingRefreshDatabase, a writingtinkerone-liner), verify the resolved connection first, in the same shell session:For a test run this must printTerminal window php artisan tinker --execute="echo config('database.default').':'.config('database.connections.'.config('database.default').'.database');"sqlite::memory:. For a deliberate write against a real DB, it must print exactly the connection you intend. phpunit.xml’sforce="true"env vars are not a safety net against a cached config. Treattests/bootstrap.php’s cache purge as the actual mechanism.- Nothing in this repo runs
migrate:fresh/db:seed --forcein CI or a deploy pipeline. Never add a schema-destructive command to any hook, script, or pipeline without an explicit, human-reviewed gate.
Git workflow: one branch per task, strict
Section titled “Git workflow: one branch per task, strict”Incident: a single long-lived branch accumulated four unrelated features plus an unrelated bug fix, 100+ commits, never pushed. Splitting it back apart took hours of manual reconstruction.
Rule: before starting a new, distinct task, create a new branch for it first, every time.
- A “task” is whatever the current request is about. If it’s not a direct continuation of what the current branch was created for, it gets its own branch.
- Branch from
main(or the relevantdevelop/stagingbranch). - Name branches
feature/<short-name>,fix/<short-name>,chore/<short-name>. - This applies per repo, independently:
dg_smart_pos_api,dg_smart_pos_frontend,dg_smart_pos_admin,dg_smart_pos_storefronteach have their own git history. - A bug fix discovered incidentally while doing the actual task can ride in the same branch, with a commit message explaining why. It must never become an excuse to bundle in a second, unrelated feature.
- If unsure whether something is a continuation or a new case: default to branching. A branch is cheap; unwinding a mixed one is not.
Core architecture change freeze (active)
Section titled “Core architecture change freeze (active)”The platform is being restructured so a business type becomes a configuration of capabilities rather than a pile of per-client special cases. Full direction: docs/PLATFORM_STRATEGY_AND_ROADMAP.md (supersedes docs/ERP_MIGRATION_PLAN.md). See Roadmap for the summary.
Continues as normal: bug fixes, performance and security work, UX/polish, report and query changes, client configuration and data fixes, storefront content and copy: anything that doesn’t change what the platform can do.
Paused: new features touching Sales, Inventory/Stock, Costing, Items, Accounting, or Pricing; new modules; new system_config booleans; new business_type conditionals; new sidebar gate keys; any new dependency on a model currently under migration.
A genuine client need that requires core work is not refused, it’s classified first (existing capability? configuration? work for an existing module? new module? genuinely core?) and built as a capability rather than a conditional wherever possible. If a conditional is truly unavoidable, that’s a decision recorded explicitly in the commit message, never a silent default.
Adding things
Section titled “Adding things”New Laravel module: create modules/{ModuleName}/ with Providers/{ModuleName}ServiceProvider.php, Routes/, Database/migrations/; add to config/modules.php’s enabled array; if tenant-scoped, add to tenantRouteModules in App\Providers\ModulesServiceProvider and require the route file in routes/tenant.php. (Currently paused by the core freeze unless the work is classified as genuinely needing a new module.)
New frontend module: create src/modules/{ModuleName}/ with index.js, routes/, store.js, pages/, services/; register in src/modules/index.js.
New system config toggle (config/system_config.php, 38+ store-level booleans): add the entry (default, label, optional scope: 'store'); add a tenant migration for the system_configs column; add a getter in dg_smart_pos_frontend/src/store/systemConfig.js via readSystemSetting; SystemConfigService::getForStore() merges and caches (5 min TTL, Redis). Call forgetStoreCache($storeId) after writes.
New sidebar gate: add a requiresXxx: true flag in src/Data/Sidebar.json; add the matching logic in src/utils/sidebarFilters.js → filterMenuChildren; pass the value into filterOpts in Drawer.vue.
Frontend UI patterns worth knowing before you build a page
Section titled “Frontend UI patterns worth knowing before you build a page”- Page height:
meta.fillMainHeighton a route controls whether the page host is edge-to-edge/no-scroll (POS terminal screens) or normally padded and scrolling (everything else, usingcalc(100vh - 100px)on the page’s own root, perInventoryPage.vue). Don’t reach forposition: absolute; inset: 0. See the frontendCLAUDE.mdfor the full rationale. - Modal headers: every
v-dialog > v-carduses av-toolbarwithcolor="primary"and a title that is never silently truncated. Vuetify’s title truncation lives on an inner.v-toolbar-title__placeholderelement you have to target with:deep()directly; reference implementation isBranchPricingModal.vueorStockPriceModal.vue.
Storefront marketing guide must stay in sync
Section titled “Storefront marketing guide must stay in sync”dg_smart_pos_frontend/src/modules/Website/components/MarketingGuideModal.vue is the single user-facing explanation of how storefront marketing/tracking/SEO behaves, and store owners act on exactly what it says. Any change to storefront marketing, tracking, analytics, or SEO updates that modal in the same change, not later. Write it for a shop owner, not an engineer, and never document a screen that doesn’t exist.
Reporting is single-source-of-truth
Section titled “Reporting is single-source-of-truth”daily_summaries is the SSOT per (store, date); transactions is derived via CashFlowService::syncFinanceTransactions. Don’t add parallel ledger writers. ReportingSaleQuery::retailPaidForStore() is the preferred composed query entrypoint over ad hoc Sale scope chains.