# enodastore audit

Audit scope: current repository state, including the Stitch references, README
and modification notes, configuration example, routes, schema/migrations,
controllers, services, models, core/middleware, views, and existing tests.
No features were added. The small fixes listed below address defects found
during this audit. Runtime validation is incomplete because this environment
does not have PHP, MySQL/MariaDB, or Docker installed.

## a. What exists

| Module | Status | Evidence / notes |
|---|---|---|
| Installer and schema bootstrap | Partial | Five-step installer, schema seeding, table-prefix support, and a lock file exist. Fresh-database installation was not executable here. |
| Authentication | Partial | Password login, registration, OTP verification, password reset, rate limiting, roles, and audit logs exist. OTP delivery is only a development file logger; production requires a real provider that is not implemented. |
| Plans and themes | Partial | `Plan`/`Theme` models, plan seeds, theme access metadata, storefront listing, and feature-gate helpers exist. There is no theme editor/publisher or `PlanService`. |
| Store onboarding | Implemented with fix | Slug checks, theme selection, Starter trial creation, and store creation exist. Duplicate store creation per owner is now serialized and rejected. |
| Storefront | Partial | Slug/custom-domain lookup and a store-scoped, plan-limited product listing exist. Product detail, cart, checkout, and order lookup routes all dispatch to the same home action. |
| Products/categories/inventory | Missing | Tables exist, but there are no product/category controllers, services, or models, and no create/update endpoints. |
| Orders/WhatsApp checkout | Missing | Order tables exist. No cart/session, order creation/status, customer lookup, or WhatsApp checkout workflow is implemented. |
| Coupons and Excel import | Missing | The plan feature keys and schema/UI references exist, but no coupon or import implementation is present. |
| Owner settings | Partial | Settings tabs and preview shell render. Inputs are not submitted; the “Save changes” button has no form/action. |
| Subscription/billing | Partial | Trial lifecycle, quotes, plan changes, manual submission, Razorpay operations, usage display, and cron logic exist. Manual approval is not implemented. |
| Razorpay | Partial | Server-side order creation, callback signature verification, encrypted credentials, webhook HMAC, replay classification, and audit logging exist. The Super Admin credential/toggle UI is absent. |
| Manual payments | Partial | Owner submission and private proof upload exist. `SubscriptionService` has approve/reject operations, but no Super Admin routes, controller actions, or screens expose them; pending payments cannot be reviewed through the app. |
| Invoices | Partial | Invoice records and Tamil-capable mPDF generation/download code exist. Invoice generation requires a settled payment, configured mPDF dependency, and database; none were exercised. |
| Super Admin | Missing | Admin routes currently render the generic dashboard. No store/user/plan/payment/settings management screens or handlers exist. |
| Cron/reminders | Implemented in code, unverified | `public/cron/daily.php` is CLI-only or requires a configured POST token and calls the daily subscription processor. No runtime execution was possible. |
| Internationalization | Partial | English/Tamil translation files and language switching exist, but substantial billing/settings/manual-payment text remains hard-coded in English. |
| Stitch designs | Reference only | The design tree has desktop, tablet, and 375px mobile exports plus design-system notes. These exports are not the runtime application views. |

## b. Bugs found and fixes

| File:line | Finding | Fix |
|---|---|---|
| `app/Services/SubscriptionService.php:47` | `currentForStore()` returned `plan_name`, while the billing view reads `name_en`; non-default current plans displayed the Starter fallback. | Added the `name_en` alias while retaining existing aliases used by invoices. |
| `app/Views/dashboard/billing.php:57,82` | Store currency was emitted without HTML escaping. | Escaped the currency in both billing price displays. |
| `app/Controllers/LanguageController.php:22-31` | Referrer paths containing backslashes/control characters were accepted as redirect targets, allowing browser URL normalization to turn a local-looking path into an external redirect. | Reject backslashes/control characters and discard control characters in the query. |
| `app/Controllers/OnboardingController.php:139-151` | The completion endpoint did not re-check whether the signed-in owner already had a store; concurrent/repeated requests could create multiple stores even though the onboarding page redirects existing owners. | Lock the owner row and re-check within the creation transaction before inserting. |
| `app/Controllers/AuthController.php:313-352` | Reset-token validity was checked before the transaction and consumed without locking; concurrent uses (including multiple outstanding tokens) could race. | Lock the user, revalidate/lock the token, update the password, and consume all outstanding reset tokens in one transaction. |
| `app/Services/LogOtpProvider.php:10-14` | The hard-coded development provider wrote plaintext OTPs regardless of `app_debug`. | Refuse file-based OTP delivery unless `app_debug` is enabled. A production SMS provider remains missing. |
| `app/Controllers/BillingController.php:74-82` | The manual-payment submit endpoint did not re-check the Manual toggle; a direct POST bypassed the check on the preceding page. | Reject submission server-side when Manual payments are disabled. |

The fixes were applied in this audit. They have not been runtime-tested in this
environment.

## c. Security check

| Check | Result |
|---|---|
| Prepared SQL statements | Pass for implemented request/database paths reviewed. User values use PDO prepared statements; fixed SQL queries are static, table names pass through the validated `Database::table()` helper, and the product-list `LIMIT` is integer-cast. No product/order mutation code exists to review. |
| CSRF on POST | Pass for application POST routes/forms. `CsrfGuard` checks POST requests globally. `/webhooks/razorpay` is the intentional exception and verifies the raw-body HMAC instead. |
| Password hashing/verification | Pass. Passwords and OTP codes use `password_hash()`/`password_verify()`. |
| Session fixation | Pass. Successful login regenerates the session ID; cookies are HttpOnly, SameSite=Lax, and secure when HTTPS is detected. |
| Output escaping | One unescaped billing currency was fixed. Dynamic values in reviewed views use `e()`/HTML escaping; layouts intentionally emit the already-rendered view buffer. Translation strings are static application resources. |
| File upload validation | Pass for implemented image/payment-proof uploads: MIME is checked with `finfo`, extensions are allowlisted and matched to MIME, filenames are random, and payment proof is kept under private storage. `public/uploads/.htaccess` disables CGI and denies PHP-like extensions. |
| Razorpay callback/webhook | Code verifies callback HMAC and webhook HMAC against the exact raw body; unique gateway IDs/replay logs support idempotency. Amount/order/currency are checked server-side. Runtime verification was unavailable. |
| Tenant isolation | Implemented owner billing/onboarding lookups derive the store from authenticated user ID; store-scoped billing and storefront product queries filter by `store_id`. Public store resolution by slug/domain is expected. Product/order mutation routes do not exist, so future tenant isolation cannot be certified. The schema alone permits a product category or order item to reference a row from another store; future write paths must enforce same-store ownership. |
| OTP secrecy | Development OTP logging is now gated by `app_debug`. No production SMS provider is present, so production OTP delivery cannot currently work. |

Webhook logs retain a bounded raw payload (up to 1 MiB per event); no retention
or cleanup policy is present. Treat the log table as containing payment/customer
data.

## d. Plan limits enforced server-side

| Limit/feature | Enforcement found | Actual status |
|---|---|---|
| Starter: 50 products | Seeded plan limit and `SubscriptionService::assertCanCreateProduct()` / `canCreateProductCount()` | Not enforced on product creation: no product mutation endpoint calls the helper. |
| Growth: 500 products | Seeded plan limit and the same helper | Not enforced on product creation for the same reason. |
| Pro: unlimited products | `productLimit()` returns `null`; helper allows null limits | Not testable against a create endpoint because none exists. |
| Storefront product visibility | `StorefrontController` uses `storefrontProductLimit()` and a store-filtered query | Enforced for listing; hides excess products rather than deleting them. |
| `custom_domain` | `canUseFeature()`; settings navigation sends a locked tab to billing | UI/navigation gate only; no custom-domain management endpoint exists. |
| `coupons` | `canUseFeature()` and dashboard navigation link | No coupon endpoint exists; not enforceable in a coupon action. |
| `excel_import` | `canUseFeature()` | No import endpoint exists. |
| `remove_branding` | `canUse()` in the storefront footer | Enforced at the current rendering point. |
| Theme access | `theme:` feature check and billing display; Starter onboarding limits theme choices | No theme editor/publish action exists to gate. |

The UI may show plan limits, but product creation limits are not presently
enforced in a write path. No product data is deleted during a plan change.

## e. Internationalization

English and Tamil language files and a language switch exist. Hard-coded
English remains in runtime billing/manual-payment/settings text, including
“Current plan”, “Compare plans”, “Payment method”, “Manual payment”, “UTR /
transaction reference”, settings labels, and payment status messages. These are
visible untranslated strings in `app/Views/dashboard/billing.php`,
`app/Views/dashboard/manual-payment.php`, `app/Views/dashboard/settings.php`,
and `app/Controllers/BillingController.php`; the auth and installer flows are
more consistently translation-key based.

## f. Responsive issues detectable from CSS/templates

- The runtime `public/assets/css/app.css` contains no media queries or responsive
  rules; layout responsiveness relies on Tailwind utility classes loaded from a
  CDN in `app/Views/partials/head.php`.
- The dashboard mobile bottom bar renders four translated labels with fixed
  horizontal padding. Long Tamil labels may become cramped at narrow widths;
  no browser/viewport test could be run.
- The Stitch exports include 375px/768px/1440px canvases, but those are static
  references and do not validate the current runtime templates.
- Settings tabs scroll horizontally and billing history is horizontally
  scrollable, reducing but not eliminating narrow-screen pressure.

## g. Database and code comparison

- `database/schema.sql` contains users, plans, themes, stores, subscriptions,
  payments, products, categories, variants, coupons, orders, order items,
  domains, visits, support tickets, OTP/reset/rate-limit, and audit tables.
  Migrations `005_billing_payments_invoices.sql` and
  `006_razorpay_webhooks.sql` add billing/invoice and webhook fields/tables.
- Foreign keys and unique keys exist for the main ownership/child relations.
  Product/category and order-item/product foreign keys are single-column, not
  composite same-store constraints. Because commerce write paths are missing,
  future code must check that linked rows belong to the same store.
- `payments.store_id` has a foreign-key index, but billing history filters by
  `store_id` and orders by `created_at`; a composite `(store_id, created_at)`
  index is absent and may be useful at scale. This is a performance
  recommendation, not an observed correctness defect.
- The store schema does not declare `owner_id` unique. The onboarding code now
  serializes store creation for the owner, but direct database writes can still
  violate the one-store assumption used by `findForOwner()`.
- Code references `products` only for storefront display/usage count and
  `categories`, `orders`, `coupons`, Excel, and theme-editor workflows have no
  corresponding controllers/services/models.
- Migration execution and prefix rewriting could not be exercised against a
  fresh database.

## Validation and smoke-test status

- VS Code diagnostics: no errors reported for the edited PHP files; this is not
  a substitute for `php -l`.
- `php -l` across 80 PHP files: blocked; PHP CLI is not installed.
- `php tests/subscription_service_test.php`: not run; PHP CLI is not installed.
  The existing file covers pure transition/proration/limit/pending-payload and
  signature/replay checks, but does not exercise database-backed workflows.
- VS Code test discovery found no test items for the existing PHP test.
- Fresh-database installer: blocked; PHP and MySQL/MariaDB are unavailable.
- `/tests/smoke.php`: added as an environment-driven HTTP workflow test. It
  requires a dedicated test installation, a verified store-owner with no
  existing store, and (for manual payment/PDF) an enabled method and fixture
  values. It checks the installer endpoint, login, onboarding, product form,
  storefront, cart/checkout content, a pre-generated invoice, and manual submit
  to pending. It reports unsupported/missing steps as failures. It could not
  be run here; it does not claim to install a fresh database or generate an
  invoice when admin approval is unavailable.
  Configure `SMOKE_BASE_URL`, `SMOKE_OWNER_EMAIL`,
  `SMOKE_OWNER_PASSWORD`, `SMOKE_STORE_SLUG`, `SMOKE_PLAN_ID`,
  `SMOKE_PAYMENT_SCREENSHOT`, and `SMOKE_INVOICE_ID` for a dedicated test
  instance before running it.

## Files changed in this audit

- `app/Controllers/AuthController.php`
- `app/Controllers/BillingController.php`
- `app/Controllers/LanguageController.php`
- `app/Controllers/OnboardingController.php`
- `app/Services/LogOtpProvider.php`
- `app/Services/SubscriptionService.php`
- `app/Views/dashboard/billing.php`
- `README.md`
- `docs/AUDIT.md`
- `tests/smoke.php`

## Remaining risks and missing scope for Parts 5–8

Concrete missing pieces found in this repository:

- Product and category CRUD, inventory enforcement in product writes, image
  management UI, and product detail pages.
- Cart, checkout, WhatsApp order creation, order status management, and
  customer order lookup.
- Coupon creation/validation and Excel import.
- Theme editor, preview/publish workflow, and owner customization settings.
- Custom-domain management and verification.
- Super Admin store/user/plan/settings management, payment-method/credential
  configuration, and routes/screens to expose the existing manual-payment
  approve/reject service operations.
- A production SMS/OTP provider.

Earlier task instructions identify Super Admin payment controls and manual
payment review as Part 5 work. No Part 6–8 specification is present in the
repository or available task history; this audit does not assign the other
missing features to specific future parts.
