# AGENTS.md

This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.

## Critical context — read first

- **This is a live production site** (kapitano.shop — a Syrian e-commerce marketplace, Arabic-first, prices in Syrian Pounds/SYP). Never run destructive artisan commands (`migrate:fresh`, `db:wipe`, seeding) or anything that mutates production data.
- **Origin: CodeCanyon (Active eCommerce CMS).** The codebase has many pre-existing bugs and almost **no reusable code — the same logic is copy-pasted in multiple places**. A fix in one file usually does NOT fix the whole project. Before declaring any fix complete, grep for sibling copies of the same logic across:
  - `app/Http/Controllers/` (web/customer)
  - `app/Http/Controllers/Admin/`
  - `app/Http/Controllers/Api/V2/` (mobile apps)
  - `app/Http/Helpers.php`
  - Blade views under `resources/views/frontend/`, `backend/`, `seller/`
  - Example: a pricing fix must be checked in `Helpers.php`, `CartController`, `Api/V2/CartController`, `CheckoutController`, and the checkout/cart views.
- `kapitano.json` contains Firebase credentials — never paste its contents into code, commits, or chat output.
- The `create-laravel-feature` skill prescribes a repository+service architecture that this codebase does not use. Do not impose it here — match the existing (fat-controller + global-helper) style unless the user explicitly asks for a refactor.

## Commands

```bash
composer install
php artisan serve
```

Frontend assets (old laravel-mix / webpack, Bootstrap 4 + jQuery + Vue 2):

```bash
npm run dev      # one-off build
npm run watch    # rebuild on change
npm run prod     # production build
```

Tests (safe to run anywhere: `phpunit.xml` forces `APP_ENV=testing` with in-memory SQLite, so they never touch the real database):

```bash
vendor/bin/phpunit
vendor/bin/phpunit --filter TestName
```

The suite is small (`tests/Feature/` — checkout OTP verification and a null-regression test that builds its own minimal schema). There is no linter/formatter configured.


CLI PHP on this dev machine works (PHP 8.4 with `mbstring`, `curl`, `pdo_mysql`), so `vendor/bin/phpunit` and `php artisan tinker <script.php>` run fine. The local MySQL database `kapitano` is a stale copy of production (useful for realistic data, but recent production rows are absent — check the newest `created_at` before drawing conclusions from it).

The CLI PHP on this Windows dev machine has `mbstring` and `curl` enabled and the suite runs. Two things are not automatic: `vendor/` may be absent (run `composer install`), and the local XAMPP MySQL may be stopped — the tests themselves use in-memory SQLite and don't need it, but running the app does.

One pre-existing failure is expected: `ProductionNullRegressionTest::test_search_works_with_preorder_disabled_and_quote_in_query` errors with `no such table: search_queries`, because that test builds its own minimal schema and never creates the search tables. It is unrelated to any pricing work.

## Git workflow

`main` is the production branch; `dev` is the integration branch. Never commit feature or fix work directly to either.

**Every new feature/fix gets its own branch — but check for an existing one first.** Before starting, list the branches:

```bash
git branch -a
```

If a branch for that work already exists (local or `remotes/origin/...`), check it out and continue on it. Only create a new branch when nothing matches:

```bash
git checkout -b feat/<short-slug>-<YYYY-MM-DD> dev
```

Naming follows what is already on the remote: `feat/…`, `fix/…`, `perf/…` with a short slug and the date (e.g. `fix/api-null-crashes-2026-08-17`, `perf/frontend-speed-2026-08-28`). Branch off `dev`. Commit and push only when the user asks.


## Architecture

Laravel 10, PHP 8.2. No repository/service abstraction as the primary pattern — most logic lives in controllers, Blade views, and global helpers.

### `app/Http/Helpers.php` is the architectural center

~215 global functions autoloaded via composer `files`. Almost everything routes through it:

- **Pricing**: `convert_price()`, `round_price()` / `round_system_price()`, `format_price()`, `cart_product_price()`, `home_discounted_price()`, `cart_product_tax()`, coupon/discount helpers. Pricing bugs almost always involve these plus their duplicated callers.

#### The price representation contract (read before touching any price)

Product prices are stored in the **system default currency (USD, `exchange_rate = 1`)** and displayed in **SYP**. Each product carries `products.exchange_type`, which picks *which* rate converts it:

- `market` → the currency's `real_rate`
- anything else — `exchange`, and the DB column default `kapitano`, which every existing product still has — → `exchange_rate`

There are three price forms, and mixing them is the source of nearly every pricing bug:

| Form | Meaning | Where it lives |
|---|---|---|
| `RAW` | as stored on the product/stock | `products.unit_price`, `product_stocks.price`, wholesale prices, bids |
| `BASE` | normalized to the **default** rate | `carts.price/tax`, `order_details.*`, `orders.*`, shipping/coupon settings |
| `SYP` | what the user sees | rendered output only |

Only two conversions are legal:

```
RAW  --round_system_price($raw, 5, $product)-->  BASE     (or product_price_to_syp() for RAW → SYP)
BASE --convert_price($base, 5)               -->  SYP     (single_price() = format_price(convert_price()))
```

`BASE` is the only form that may be **summed across products** — that is what makes a cart holding both a `market` and an `exchange` product total correctly. Rules that follow from this:

- Never pass a RAW price to `convert_price()` / `single_price()`; they deliberately ignore their legacy `$product` argument and always use the default rate.
- Never pass `$product` to `round_system_price()` for a value derived from an already-BASE amount (taxes computed off a BASE price, shipping, coupons, club points) — that double-converts.
- Reports that aggregate RAW columns in SQL (`product_stocks.price`, `products.unit_price/purchase_price`) must wrap them in `raw_to_base_sql()`, or they compare RAW cost against BASE revenue.
- `real_rate` is `0` in production today, so `market` currently falls back to the default rate. That fallback is deliberate — without it a market product prices at **0**.
- Rates are cached (`system_default_currency` for 86400s, plus a per-request memo of the SYP row), so after changing a rate you must `php artisan cache:clear`.

`tests/Feature/ExchangeTypePricingTest.php` pins all of the above (runs on SQLite, no DB setup needed).

`tests/Feature/ExchangeTypeRuntimeTest.php` is a **live** test: it runs the real controllers and Blade views against the real MySQL database (cart page, checkout page, product page, admin report pages). It is skipped unless you opt in, and everything it does is wrapped in a transaction that is rolled back — it uses no DDL and never `RefreshDatabase`:

```bash
KAPITANO_RUNTIME_DB_TESTS=1 vendor/bin/phpunit --filter ExchangeTypeRuntimeTest
```

Needs XAMPP MySQL running. Note it has to set `$_SERVER['HTTP_HOST']`/`SERVER_NAME` by hand, because `getBaseURL()` and several controllers read `$_SERVER` directly instead of `request()` — that also breaks any CLI/queue context, unrelated to pricing.
- **Settings**: `get_setting()` reads the `business_settings` table (cached).
- **i18n**: `translate()` for UI strings.
- **Assets**: `uploaded_asset()` / `my_asset()` — files are referenced by upload ID, not path.

### Caching gotchas

Heavy use of long-lived caches: `Cache::rememberForever('verified_sellers_id')`, 86400s caches for products per category, `system_default_currency`, business settings. If a change to settings/products/currency "doesn't work", the cache is usually why — `php artisan cache:clear` (be deliberate on production).

### Routes are split by domain — 23 files in `routes/`

`web.php` (customer storefront), `admin.php`, `seller.php`, `api.php` + `api_seller.php` (mobile), plus feature files: `auction.php`, `pos.php`, `preorder.php`, `wholesale.php`, `delivery_boy.php`, `affiliate.php`, `club_points.php`, `otp.php`, `refund_request.php`, and per-gateway payment routes. When touching a feature, check whether it also has its own route file.

### API for mobile apps

`app/Http/Controllers/Api/V2/` mirrors much of the web controllers (its own `CartController`, `CheckoutController`, etc.) — the duplication warning applies most strongly here. Auth is Sanctum.

### Translations (i18n)

Models use a `*Translation` companion-model pattern (`ProductTranslation`, `CategoryTranslation`, …) with a `getTranslation('field', $lang)` method that falls back to the base model attribute. Store is Arabic-first; `translate()` handles UI strings.

### Database schema changes

Both `database/migrations/` and `sqlupdates/` (raw versioned SQL files, `v15.sql`…`v23.sql`, shipped by the CMS vendor) exist. Check both when reasoning about schema history; don't assume migrations tell the whole story.

### Other notable pieces

- `app/Services/` and `app/Utility/` both exist with overlapping roles (e.g. `OrderService` vs `CartUtility`, `SendSmsService` vs `SendSMSUtility`) — check both before adding new logic.
- Payments: many gateways bundled by the CMS, but the store actually uses **Cash on Delivery and Paymera** (`PaymeraService`, `PaymeraController`); OTP sign-in via phone/WhatsApp (`OtpService`, `app/Services/OTP/`).

### Paymera gateway (verified live 2026-08-30)

- API: `POST /api/create-payment`, `GET /api/get-payment-status/{paymentId}`, `POST /api/cancel-payment` on `config('paymera.base_url')` with HTTP basic auth. Responses are `{ErrorCode, ErrorMessage, Data}`; `ErrorCode == 0` is success. Payment status is `Data.status`: `A` accepted, `P` pending, `C` cancelled, `F` failed.
- **Amount unit**: the amount sent to Paymera is `grand_total (USD) × SYP exchange_rate` (the `currencies` table row for SYP, post-redenomination rate ≈ 138) — i.e. plain new Syrian Pounds. Do **NOT** divide by 100: real accepted payments (e.g. $32 order → 4416 SYP, status A) confirm the undivided amount is correct. A `÷100` existed briefly in the old web `rePayment` and was a bug.
- `App\Utility\PaymeraUtility::syncPaymentStatus()` is the single mark-paid path shared by web (`PaymeraController`) and mobile (`Api/V2/CheckoutController`). Both a browser `callback` (GET) and a server-to-server `trigger` (POST) hit it; trigger URLs must stay in `VerifyCsrfToken::$except`.
- Payment creation stores `payment_id` on `combined_orders`; syncing marks every `orders` row of that combined order paid. Beware: many `combined_orders` rows carry a `payment_id` but have **no `orders` rows** — paying those updates nothing.
- `performance-baselines/` holds Lighthouse configs/results used for performance work.
