Files

1314 lines
121 KiB
Markdown
Raw Permalink Normal View History

# Inventory — henza_server (reference backend)
> Evidence-based inventory. All `path:line` are relative to `henza_server/`.
> **Domain note:** this codebase is a **gold jewelry B2B/B2C e-commerce + wholesale ledger**, not a "gold bank". There is **no buy/sell order book, no deposit/withdraw, no KYC-level system, no transfer between users**. Gold is handled as (a) a live 18-ayar price feed used in product pricing, and (b) a **gram-denominated wallet** (cashback/loyalty). See §7 for what exists vs. what does not.
---
## 1. Stack & versions
| Item | Value | Evidence |
|---|---|---|
| Framework | `laravel/framework: ^8.0` | `composer.json:19` |
| PHP | `^7.3` | `composer.json:10` |
| App name/env | `APP_NAME`, `APP_ENV`, `APP_DEBUG`, `APP_URL` | `config/app.php` |
| Autoload PSR-4 | `App\` → `app/`, `Rooyesh\Wallet\` → `packages/rooyesh/wallet/src/` | `composer.json:41-46` |
| Custom app version | `2.2.0` | `config/version.php:3` |
### Key packages (`composer.json:9-27`)
| Package | Constraint | Line | Purpose |
|---|---|---|---|
| `apility/laravel-fcm` | `^1.4` | 11 | Push (FCM) |
| `brozot/laravel-fcm` | `^1.3` | 13 | Push (FCM, second impl) |
| `barryvdh/laravel-dompdf` | `^0.9.0` | 12 | PDF invoices |
| `niklasravnsborg/laravel-pdf` | `^4.1` | 24 | PDF (mPDF) |
| `fideloper/proxy` | `^4.2` | 14 | Trusted proxies |
| `fruitcake/laravel-cors` | `^2.0` | 15 | CORS |
| `guzzlehttp/guzzle` | `^7.0.1` | 16 | HTTP (SMS/price feeds) |
| `hekmatinasser/verta` | `^2.1` | 17 | Jalali dates |
| `morilog/jalali` | `3.*` | 22 | Jalali dates (primary, used in `Utility`) |
| `ixudra/curl` | `^6.21` | 18 | cURL (payping) |
| `jgrossi/corcel` | `^5.0` | 19 | WordPress DB bridge (`config/corcel.php`) |
| `maatwebsite/excel` | `^3.1` | 21 | Excel export (TableEngine) |
| `phpoffice/phpspreadsheet` | `*` | 23 | Excel |
| `phpro/soap-client` | `^1.4` | 25 | SOAP (legacy Zarinpal) |
| `laravel/tinker` | `^2.0` | 20 | — |
Dev: `facade/ignition ^2.3.6`, `fzaninotto/faker ^1.9.1`, `mockery/mockery ^1.3.1`, `nunomaduro/collision ^5.0`, `phpunit/phpunit ^9.3` (`composer.json:29-35`).
**Local package:** `packages/rooyesh/wallet` — in-repo gold/credit wallet module (see §7).
---
## 2. Bootstrap / Middleware / Auth
### 2.1 Global middleware — `app/Http/Kernel.php:17-25` (order as declared)
| # | Class | Line |
|---|---|---|
| — | `App\Http\Middleware\TrustHosts` | 18 (**commented out**) |
| 1 | `App\Http\Middleware\TrustProxies` | 19 |
| 2 | `Fruitcake\Cors\HandleCors` | 20 |
| 3 | `App\Http\Middleware\PreventRequestsDuringMaintenance` | 21 |
| 4 | `Illuminate\Foundation\Http\Middleware\ValidatePostSize` | 22 |
| 5 | `App\Http\Middleware\TrimStrings` | 23 |
| 6 | `Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull` | 24 |
### 2.2 Middleware groups — `app/Http/Kernel.php:32-47`
| Group | Stack (in order) | Lines |
|---|---|---|
| `web` | `EncryptCookies`, `AddQueuedCookiesToResponse`, `StartSession`, *(`AuthenticateSession` commented, :37)*, `ShareErrorsFromSession`, `VerifyCsrfToken`, `SubstituteBindings` | 33-41 |
| `api` | `throttle:api`, `SubstituteBindings` | 43-46 |
Rate limit: `Limit::perMinute(500)` for the `api` limiter — `app/Providers/RouteServiceProvider.php:76-78`.
### 2.3 Route middleware aliases — `app/Http/Kernel.php:56-68`
| Alias | Class | Line |
|---|---|---|
| `auth` | `App\Http\Middleware\Authenticate` | 57 |
| `auth.basic` | `Illuminate\Auth\Middleware\AuthenticateWithBasicAuth` | 58 |
| `cache.headers` | `Illuminate\Http\Middleware\SetCacheHeaders` | 59 |
| `can` | `Illuminate\Auth\Middleware\Authorize` | 60 |
| `guest` | `App\Http\Middleware\RedirectIfAuthenticated` | 61 |
| `password.confirm` | `Illuminate\Auth\Middleware\RequirePassword` | 62 |
| `signed` | `Illuminate\Routing\Middleware\ValidateSignature` | 63 |
| `throttle` | `Illuminate\Routing\Middleware\ThrottleRequests` | 64 |
| `verified` | `Illuminate\Auth\Middleware\EnsureEmailIsVerified` | 65 |
| `version` | `App\Http\Middleware\VersionMiddleware` | 66 |
| `create_log` | `App\Http\Middleware\CreateLog` | 67 |
Custom middleware behavior:
- `VersionMiddleware` — reads `config('version.version')`; if request header `version` present and ≠ config, returns `{"version": false}` — `app/Http/Middleware/VersionMiddleware.php:19-33`.
- `CreateLog` — writes a `RequestLog` row (customer_id from `auth()->id()`, ip, data, user_agent, route, method) on every request in the group — `app/Http/Middleware/CreateLog.php:18-30`. Note bug: `'device',` is a positional value, not a key (`:23`).
- `Authenticate::redirectTo()` — `abort(401,'no authorised')` instead of redirect — `app/Http/Middleware/Authenticate.php:15-21`.
### 2.4 Route file prefixes/middleware — `app/Providers/RouteServiceProvider.php:41-66`
| File | Prefix | Group middleware | Lines |
|---|---|---|---|
| `routes/api.php` | `api` | `api` | 43-46 |
| `routes/web.php` | *(none)* | `web` | 48-50 |
| `routes/admin.php` | `admin` | `api` | 52-55 |
| `routes/site.php` | `site` | `api` | 57-60 |
| `routes/customer.php` | `cu` | `api` | 62-65 |
`HOME = '/home'` (`:20`). `$namespace` is **commented out** (`:29`) so string-controller routes use FQCN strings.
### 2.5 Auth guards & providers — `config/auth.php`
| Guard | Driver | Provider | hash | Line |
|---|---|---|---|---|
| `web` | session | `users` | — | 39-42 |
| `api` (**default**) | token | `users` | `false` | 44-48 |
| `api-customer` | token | `customers` | `false` | 50-54 |
| `api-singleCustomer` | token | `singleCustomers` | `false` | 56-60 |
Defaults: guard `api`, passwords `users` (`:16-19`).
| Provider | Driver | Model | Line |
|---|---|---|---|
| `users` | eloquent | `App\Models\User` | 81-84 |
| `customers` | eloquent | `App\Models\Customer` | 85-88 |
| `singleCustomers` | eloquent | `App\Models\SingleCustomer` | 89-92 |
Password resets: table `password_resets`, expire 60, throttle 60 (`:115-122`). `password_timeout: 10800` (`:135`).
**Three-actor model:** `User` = staff/admin (guard `api`), `Customer` = wholesale/major shopkeeper (guard `api-customer`), `SingleCustomer` = retail web customer, table `login` (guard `api-singleCustomer`).
---
## 3. Routes (exhaustive)
> `name` column: **no route is given a `->name()` anywhere** in any route file. Resource routes get Laravel's implicit names (`cat.index`, …) which are listed.
> Middleware column = group middleware from RouteServiceProvider + any explicit route middleware.
### 3.1 `routes/admin.php` — prefix `admin`, middleware `api`
**⚠️ The entire file's auth wrapper `Route::middleware('auth:api')->group(...)` is COMMENTED OUT** at `routes/admin.php:12` and `:264`. **Every admin route below is unauthenticated.**
| METHOD | path | name | controller@method | middleware/guards | line |
|---|---|---|---|---|---|
| POST | `admin/product/create` | — | `Adm\ProductController@store` | api | 14 |
| GET | `admin/product/show/{id}` | — | `Adm\ProductController@show` | api | 15 |
| PUT | `admin/product/update/{id}` | — | `Adm\ProductController@update` | api | 16 |
| POST | `admin/product/test` | — | `Adm\ProductController@testSearch` | api | 17 |
| POST | `admin/product/report` | — | `Adm\ProductController@report` | api | 18 |
| GET | `admin/product/color` | — | `Adm\ProductController@color` | api | 19 |
| POST | `admin/product/variant` | — | `Adm\ProductController@getProductsVaraints` | api | 20 |
| GET | `admin/product/offer/{product_id}` | — | `Adm\ProductController@changeOffer` | api | 21 |
| GET | `admin/product/exist/{product_id}` | — | `Adm\ProductController@changeIsExist` | api | 22 |
| GET | `admin/product/warranty/{product_id}` | — | `Adm\ProductController@changeHasWarranty` | api | 23 |
| GET | `admin/dashboard/all` | — | `Adm\DashboardController@getAll` | api | 26 |
| GET | `admin/dashboard/change` | — | `Adm\DashboardController@change` | api | 27 |
| GET | `admin/dashboard/info` | — | `Adm\DashboardController@info` | api | 28 |
| GET | `admin/dashboard/link` | — | `Adm\DashboardController@link` | api | 29 |
| POST | `admin/admin-login` | — | `Adm\AdminController@login` | api | 33 |
| POST | `admin/products` | — | `Adm\AdminController@getProducts` | api | 34 |
| POST | `admin/single-customer` | — | `Adm\AdminController@getSingleCustomer` | api | 35 |
| GET | `admin/category` | — | `Adm\AdminController@queryCategory` | api | 36 |
| GET | `admin/get-analytics` | — | `Adm\AdminController@getAnalytics` | api | 37 |
| GET | `admin/get-analytics-tak` | — | `Adm\AdminController@getAnalyticsTak` | api | 38 |
| POST | `admin/product` | — | `Adm\AdminController@addProduct` | api | 39 |
| POST | `admin/add/variant` | — | `Adm\AdminController@addVariant` | api | 40 |
| GET | `admin/get/variant` | — | `Adm\AdminController@getVariant` | api | 41 |
| POST | `admin/get/variants` | — | `Adm\AdminController@getVariants` | api | 42 |
| GET | `admin/config/statics` | — | `Adm\AdminController@getConfigStatics` | api | 43 |
| GET | `admin/product/{id}` | — | `Adm\AdminController@getProduct` | api | 44 |
| POST | `admin/delete/product/{id}` | — | `Adm\AdminController@deleteProduct` | api | 45 |
| POST | `admin/category` | — | `Adm\AdminController@getCategory` | api | 46 |
| POST | `admin/add/category` | — | `Adm\AdminController@AddCategory` | api | 47 |
| GET | `admin/category/{id}` | — | `Adm\AdminController@getOneCategory` | api | 48 |
| GET | `admin/havale/list` | — | `Adm\AdminController@getHavaleList` | api | 49 |
| DELETE | `admin/category/{id}` | — | `Adm\AdminController@deleteCategory` | api | 50 |
| GET | `admin/price` | — | *Closure* → `WebServiceGold::getAllPrice()` | api | 51-53 |
| POST | `admin/change/percent` | — | `Adm\AdminController@changePercent` | api | 54 |
| POST | `admin/change/percent/count` | — | `Adm\AdminController@countProductForPercent` | api | 55 |
| POST | `admin/change/all` | — | `Adm\AdminController@changeAll` | api | 56 |
| POST | `admin/single/order-sts/edit` | — | `Adm\AdminController@orderStsSingle` | api | 59 |
| POST | `admin/single-order` | — | `Adm\AdminController@getSingleOrders` | api | 60 |
| POST | `admin/order` | — | `Adm\OrderController@store` | api | 61 |
| PUT | `admin/order/{id}` | — | `Adm\OrderController@update` | api | 62 |
| GET | `admin/order/type` | — | `Adm\OrderController@getType` | api | 63 |
| GET | `admin/order/pdf/{id}` | — | `Customer\OrderController@generateReportOrder` | api | 64 |
| POST | `admin/cat` | — | `Adm\CategoryController@index` | api | 70 |
| GET | `admin/cat` | `cat.index` | `Adm\CategoryController@index` | api | 71 (resource) |
| GET | `admin/cat/create` | `cat.create` | `Adm\CategoryController@create` | api | 71 |
| POST | `admin/cat` | `cat.store` | `Adm\CategoryController@store` | api | 71 |
| GET | `admin/cat/{cat}` | `cat.show` | `Adm\CategoryController@show` | api | 71 |
| GET | `admin/cat/{cat}/edit` | `cat.edit` | `Adm\CategoryController@edit` | api | 71 |
| PUT/PATCH | `admin/cat/{cat}` | `cat.update` | `Adm\CategoryController@update` | api | 71 |
| DELETE | `admin/cat/{cat}` | `cat.destroy` | `Adm\CategoryController@destroy` | api | 71 |
| POST | `admin/major/order/table` | — | `Adm\MajorOrderController@index` | api | 78 |
| GET | `admin/major/order` | `major/order.index` | `Adm\MajorOrderController@index` | api | 79 (resource) |
| GET | `admin/major/order/create` | `major/order.create` | `Adm\MajorOrderController@create` | api | 79 |
| POST | `admin/major/order` | `major/order.store` | `Adm\MajorOrderController@store` | api | 79 |
| GET | `admin/major/order/{order}` | `major/order.show` | `Adm\MajorOrderController@show` | api | 79 |
| GET | `admin/major/order/{order}/edit` | `major/order.edit` | `Adm\MajorOrderController@edit` | api | 79 |
| PUT/PATCH | `admin/major/order/{order}` | `major/order.update` | `Adm\MajorOrderController@update` | api | 79 |
| DELETE | `admin/major/order/{order}` | `major/order.destroy` | `Adm\MajorOrderController@destroy` | api | 79 |
| DELETE | `admin/major/order_item/{id}` | — | `Adm\MajorOrderController@deleteOrderItem` | api | 80 |
| POST | `admin/order-tamin` | — | `Adm\MajorOrderController@getTaminOrders` | api | 81 |
| POST | `admin/order-sts/edit` | — | `Adm\MajorOrderController@orderSts` | api | 82 |
| POST | `admin/order-item/edit` | — | `Adm\MajorOrderController@orderItemEdit` | api | 83 |
| POST | `admin/delete-order` | — | `Adm\MajorOrderController@deleteOrder` | api | 84 |
| POST | `admin/brand/create` | — | `Adm\BrandController@store` | api | 88 |
| GET | `admin/brand` | — | `Adm\BrandController@index` | api | 89 |
| PUT | `admin/brand/{id}` | — | `Adm\BrandController@update` | api | 90 |
| DELETE | `admin/brand/{id}` | — | `Adm\BrandController@destroy` | api | 91 |
| POST | `admin/report/order_item` | — | `Adm\ReportController@getOrderItemWithDetails` | api | 96 |
| POST | `admin/major/table` | — | `Adm\MajorCustomerController@index` | api | 100 |
| GET | `admin/major/dashboard/{id}` | — | `Adm\MajorCustomerController@dashboard` | api | 101 |
| POST | `admin/major/order` | — | `Adm\MajorCustomerController@order` | api | 102 (**shadowed** by resource at :79) |
| POST | `admin/major/cash` | — | `Adm\MajorCustomerController@cash` | api | 103 |
| POST | `admin/major/customer` | — | `Adm\MajorCustomerController@shopkeeper` | api | 104 |
| POST | `admin/major/sms` | — | `Adm\MajorCustomerController@indexSms` | api | 105 |
| POST | `admin/major/verify` | — | `Adm\MajorCustomerController@verifyRegister` | api | 106 |
| POST | `admin/major/archive/table` | — | `Adm\MajorCustomerController@indexArchive` | api | 107 |
| POST | `admin/major/verify/table` | — | `Adm\MajorCustomerController@indexVerify` | api | 108 |
| GET | `admin/major/archive/{customer_id}` | — | `Adm\MajorCustomerController@archive` | api | 109 |
| GET | `admin/major/list_type` | — | `Adm\MajorCustomerController@listTypeCustomer` | api | 110 |
| POST | `admin/reagent` | — | `Adm\AdminController@getReagent` | api | 113 |
| POST | `admin/banker` | — | `Adm\AdminController@getBanker` | api | 114 |
| POST | `admin/shopkeeper` | — | `Adm\AdminController@getShopkeeper` | api | 115 |
| POST | `admin/customer/toggle-sts` | — | `Adm\AdminController@customerToggleSts` | api | 116 |
| POST | `admin/customer/toggle-active` | — | `Adm\AdminController@customerToggleActive` | api | 117 |
| GET | `admin/customer/{id}` | — | `Adm\AdminController@getCustomerById` | api | 118 |
| POST | `admin/customer` | — | `Adm\AdminController@getCustomer` | api | 119 |
| GET | `admin/query/customer` | — | `Adm\AdminController@queryCustomer` | api | 120 |
| GET | `admin/query/reagent` | — | `Adm\AdminController@queryReagent` | api | 121 |
| POST | `admin/major/promotion/table` | — | `Adm\PromotionController@index` | api | 125 |
| PUT | `admin/autopromotion` | — | `Adm\PromotionController@updateAutoPromotion` | api | 126 |
| GET | `admin/autopromotion` | — | `Adm\PromotionController@getAutoPromotion` | api | 127 |
| POST | `admin/brand` | — | `Adm\PromotionController@getBrand` | api | 128 |
| GET | `admin/major/promotion` | `major/promotion.index` | `Adm\PromotionController@index` | api | 129 (resource) |
| GET | `admin/major/promotion/create` | `major/promotion.create` | `Adm\PromotionController@create` | api | 129 |
| POST | `admin/major/promotion` | `major/promotion.store` | `Adm\PromotionController@store` | api | 129 |
| GET | `admin/major/promotion/{promotion}` | `major/promotion.show` | `Adm\PromotionController@show` | api | 129 |
| GET | `admin/major/promotion/{promotion}/edit` | `major/promotion.edit` | `Adm\PromotionController@edit` | api | 129 |
| PUT/PATCH | `admin/major/promotion/{promotion}` | `major/promotion.update` | `Adm\PromotionController@update` | api | 129 |
| DELETE | `admin/major/promotion/{promotion}` | `major/promotion.destroy` | `Adm\PromotionController@destroy` | api | 129 |
| GET | `admin/ignore` | `ignore.index` | `Adm\IgnoreController@index` | api | 132 (resource) |
| GET | `admin/ignore/create` | `ignore.create` | `Adm\IgnoreController@create` *(not defined)* | api | 132 |
| POST | `admin/ignore` | `ignore.store` | `Adm\IgnoreController@store` | api | 132 |
| GET | `admin/ignore/{ignore}` | `ignore.show` | `Adm\IgnoreController@show` | api | 132 |
| GET | `admin/ignore/{ignore}/edit` | `ignore.edit` | `Adm\IgnoreController@edit` *(not defined)* | api | 132 |
| PUT/PATCH | `admin/ignore/{ignore}` | `ignore.update` | `Adm\IgnoreController@update` *(not defined)* | api | 132 |
| DELETE | `admin/ignore/{ignore}` | `ignore.destroy` | `Adm\IgnoreController@destroy($type)` | api | 132 |
| POST | `admin/cash/table` | — | `Adm\CashController@index` | api | 137 |
| GET | `admin/cash` | `cash.index` | `Adm\CashController@index` | api | 138 (resource) |
| GET | `admin/cash/create` | `cash.create` | `Adm\CashController@create` | api | 138 |
| POST | `admin/cash` | `cash.store` | `Adm\CashController@store` | api | 138 |
| GET | `admin/cash/{cash}` | `cash.show` | `Adm\CashController@show` | api | 138 |
| GET | `admin/cash/{cash}/edit` | `cash.edit` | `Adm\CashController@edit` | api | 138 |
| PUT/PATCH | `admin/cash/{cash}` | `cash.update` | `Adm\CashController@update` | api | 138 |
| DELETE | `admin/cash/{cash}` | `cash.destroy` | `Adm\CashController@destroy` | api | 138 |
| POST | `admin/cash/confirem/{id}` | — | `Adm\CashController@toggleConfiremCash` | api | 139 |
| POST | `admin/file/create` | — | `Adm\FileController@storeWithoutData` | api | 145 |
| PUT | `admin/file/update/{file_id}` | — | `Adm\FileController@updateBody` | api | 146 |
| POST | `admin/comment/table` | — | `Adm\CommentController@index` | api | 150 |
| GET | `admin/comment` | `comment.index` | `Adm\CommentController@index` | api | 151 (resource) |
| GET | `admin/comment/create` | `comment.create` | `Adm\CommentController@create` | api | 151 |
| POST | `admin/comment` | `comment.store` | `Adm\CommentController@store` | api | 151 |
| GET | `admin/comment/{comment}` | `comment.show` | `Adm\CommentController@show` | api | 151 |
| GET | `admin/comment/{comment}/edit` | `comment.edit` | `Adm\CommentController@edit` | api | 151 |
| PUT/PATCH | `admin/comment/{comment}` | `comment.update` | `Adm\CommentController@update` | api | 151 |
| DELETE | `admin/comment/{comment}` | `comment.destroy` | `Adm\CommentController@destroy` | api | 151 |
| POST | `admin/comment/sts` | — | `Adm\CommentController@changeStatus` | api | 152 |
| GET | `admin/const` | `const.index` | `Adm\ConstController@index` *(not defined)* | api | 155 (resource) |
| GET | `admin/const/create` | `const.create` | `Adm\ConstController@create` *(not defined)* | api | 155 |
| POST | `admin/const` | `const.store` | `Adm\ConstController@store` | api | 155 |
| GET | `admin/const/{const}` | `const.show` | `Adm\ConstController@show` | api | 155 |
| GET | `admin/const/{const}/edit` | `const.edit` | `Adm\ConstController@edit` *(not defined)* | api | 155 |
| PUT/PATCH | `admin/const/{const}` | `const.update` | `Adm\ConstController@update` | api | 155 |
| DELETE | `admin/const/{const}` | `const.destroy` | `Adm\ConstController@destroy` | api | 155 |
| DELETE | `admin/const/by-id/{id}` | — | `Adm\ConstController@deleteById` | api | 156 |
| POST | `admin/roles` | — | `Adm\PermissionController@store` | api | 160 |
| POST | `admin/roles/create` | — | `Adm\PermissionController@create` | api | 161 |
| DELETE | `admin/roles/{ut}/{rid}` | — | `Adm\PermissionController@destroy` | api | 162 |
| GET | `admin/roles/{ut}/{rid}` | — | `Adm\PermissionController@has` | api | 163 |
| GET | `admin/roles` | — | `Adm\PermissionController@get` | api | 164 |
| GET | `admin/roles/{id}` | — | `Adm\PermissionController@getByUt` | api | 165 |
| GET | `admin/cities` | `cities.index` | `Adm\CitiesController@index` *(not defined)* | api | 168 (resource) |
| GET | `admin/cities/create` | `cities.create` | `Adm\CitiesController@create` *(not defined)* | api | 168 |
| POST | `admin/cities` | `cities.store` | `Adm\CitiesController@store` | api | 168 |
| GET | `admin/cities/{city}` | `cities.show` | `Adm\CitiesController@show` | api | 168 |
| GET | `admin/cities/{city}/edit` | `cities.edit` | `Adm\CitiesController@edit` *(not defined)* | api | 168 |
| PUT/PATCH | `admin/cities/{city}` | `cities.update` | `Adm\CitiesController@update` | api | 168 |
| DELETE | `admin/cities/{city}` | `cities.destroy` | `Adm\CitiesController@destroy` | api | 168 |
| POST | `admin/cities/list` | — | `Adm\CitiesController@list` | api | 169 |
| GET | `admin/cities/get-percent/{id}` | — | `Adm\CitiesController@getPercent` | api | 170 (**shadowed** by `cities.show`) |
| GET | `admin/cities/get-percent-aux/{id}` | — | `Adm\CitiesController@getPercentAux` | api | 171 (**shadowed**) |
| POST | `admin/customer/create` | — | `Adm\AdminController@createCustomer` | api | 175 |
| GET | `admin/report/order` | — | `Adm\AdminController@generateReportOrder` | api | 176 |
| GET | `admin/report/doc` | — | `Adm\AdminController@generateReportDocument` | api | 177 |
| GET | `admin/users/show` | — | `Adm\UserController@showData` | api | 180 |
| POST | `admin/users/list` | — | `Adm\UserController@index` | api | 181 |
| GET | `admin/users` | `users.index` | `Adm\UserController@index` | api | 182 (resource) |
| GET | `admin/users/create` | `users.create` | `Adm\UserController@create` *(not defined)* | api | 182 |
| POST | `admin/users` | `users.store` | `Adm\UserController@store` | api | 182 |
| GET | `admin/users/{user}` | `users.show` | `Adm\UserController@show` | api | 182 |
| GET | `admin/users/{user}/edit` | `users.edit` | `Adm\UserController@edit` *(not defined)* | api | 182 |
| PUT/PATCH | `admin/users/{user}` | `users.update` | `Adm\UserController@update` | api | 182 |
| DELETE | `admin/users/{user}` | `users.destroy` | `Adm\UserController@destroy` | api | 182 |
| POST | `admin/wallet/percent` | — | `Adm\WalletController@changePercent` | api | 186 |
| POST | `admin/wallet/table` | — | `Adm\WalletController@index` | api | 187 |
| POST | `admin/wallet/charge` | — | `Adm\WalletController@charge` | api | 188 |
| POST | `admin/wallet/config` | — | `Adm\WalletController@setConst` | api | 189 |
| GET | `admin/wallet/config` | — | `Adm\WalletController@getConfig` | api | 190 |
| GET | `admin/wallet/birthday/config` | — | `Adm\WalletController@getConfigBirthday` | api | 191 |
| POST | `admin/single/order` | — | `Adm\SingleCustomerController@indexOrder` | api | 194 |
| POST | `admin/single/transaction` | — | `Adm\SingleCustomerController@indexTransaction` | api | 195 |
| POST | `admin/single/sms` | — | `Adm\SingleCustomerController@indexSms` | api | 196 |
| GET | `admin/single/wallet/{id}` | — | `Adm\SingleCustomerController@getWallet` | api | 197 |
| GET | `admin/single/report/{id}` | — | `Adm\SingleCustomerController@report` | api | 198 |
| GET | `admin/single` | `single.index` | `Adm\SingleCustomerController@index` *(not defined)* | api | 199 (resource) |
| GET | `admin/single/create` | `single.create` | `@create` *(not defined)* | api | 199 |
| POST | `admin/single` | `single.store` | `Adm\SingleCustomerController@store` | api | 199 |
| GET | `admin/single/{single}` | `single.show` | `Adm\SingleCustomerController@show` | api | 199 |
| GET | `admin/single/{single}/edit` | `single.edit` | `@edit` *(not defined)* | api | 199 |
| PUT/PATCH | `admin/single/{single}` | `single.update` | `Adm\SingleCustomerController@update` | api | 199 |
| DELETE | `admin/single/{single}` | `single.destroy` | `@destroy` *(only `delete($id)` exists)* | api | 199 |
| POST | `admin/sms/table` | — | `Adm\SmsController@index` | api | 202 |
| GET | `admin/sms/customer` | — | `Adm\SmsController@getCustomer` | api | 203 |
| POST | `admin/sms/other` | — | `Adm\SmsController@sendSmsToOther` | api | 204 |
| POST | `admin/notify/customer` | — | `Adm\NotifyController@customer` | api | 207 |
| GET | `admin/notify/unread` | — | `Adm\NotifyController@getUnreadNotify` | api | 208 |
| GET | `admin/notify/read/{id}` | — | `Adm\NotifyController@read` | api | 209 |
| POST | `admin/notify/table` | — | `Adm\NotifyController@index` | api | 210 |
| GET | `admin/notify` | `notify.index` | `Adm\NotifyController@index` | api | 211 (resource) |
| GET | `admin/notify/create` | `notify.create` | `@create` *(not defined)* | api | 211 |
| POST | `admin/notify` | `notify.store` | `Adm\NotifyController@store` | api | 211 |
| GET | `admin/notify/{notify}` | `notify.show` | `Adm\NotifyController@show` | api | 211 |
| GET | `admin/notify/{notify}/edit` | `notify.edit` | `@edit` *(not defined)* | api | 211 |
| PUT/PATCH | `admin/notify/{notify}` | `notify.update` | `@update` *(not defined)* | api | 211 |
| DELETE | `admin/notify/{notify}` | `notify.destroy` | `@destroy` *(not defined)* | api | 211 |
| POST | `admin/log/title` | — | `Adm\LogController@queryTitle` | api | 214 |
| POST | `admin/log/customer/all` | — | `Adm\LogController@queryAllCustomer` | api | 215 |
| POST | `admin/log/customer/{id}` | — | `Adm\LogController@queryCustomer` | api | 216 |
| GET | `admin/log/list/customer` | — | `Adm\LogController@listCu` | api | 217 |
| GET | `admin/log/ago/{id}` | — | `Adm\LogController@ago` | api | 218 |
| POST | `admin/fashion/table` | — | `Adm\AdminController@getListFashion` | api | 221 |
| POST | `admin/branch/table` | — | `Adm\BranchController@index` | api | 224 |
| GET | `admin/branch/active/{id}` | — | `Adm\BranchController@activeBranch` | api | 225 |
| GET | `admin/branch/list` | — | `Adm\BranchController@listBranch` | api | 226 |
| GET | `admin/user/list` | — | `Adm\BranchController@listUser` | api | 227 (**overridden** at :248) |
| POST | `admin/branch/product` | — | `Adm\BranchController@addProduct` | api | 228 |
| PUT | `admin/branch/product/{inventory_id}` | — | `Adm\BranchController@updateProduct` | api | 229 |
| GET | `admin/branch/product/{inventory_id}` | — | `Adm\BranchController@getProduct` | api | 230 |
| DELETE | `admin/branch/product/{inventory_id}` | — | `Adm\BranchController@deleteProduct` | api | 231 |
| POST | `admin/branch/product/table` | — | `Adm\BranchController@productTable` | api | 232 |
| GET | `admin/branch/dashboard` | — | `Adm\BranchController@dashboard` | api | 233 |
| GET | `admin/branch/admin/dashboard` | — | `Adm\BranchController@adminDashboard` | api | 234 |
| GET | `admin/branch` | `branch.index` | `Adm\BranchController@index` | api | 235 (resource) |
| GET | `admin/branch/create` | `branch.create` | `@create` *(not defined)* | api | 235 |
| POST | `admin/branch` | `branch.store` | `Adm\BranchController@store` | api | 235 |
| GET | `admin/branch/{branch}` | `branch.show` | `Adm\BranchController@show` | api | 235 |
| GET | `admin/branch/{branch}/edit` | `branch.edit` | `@edit` *(not defined)* | api | 235 |
| PUT/PATCH | `admin/branch/{branch}` | `branch.update` | `Adm\BranchController@update` | api | 235 |
| DELETE | `admin/branch/{branch}` | `branch.destroy` | `Adm\BranchController@destroy` | api | 235 |
| GET | `admin/get-custom-order/{id}` | — | `Api\CustomOrderController@getCustomOrder` | api | 238 |
| PUT | `admin/custom-order/{id}` | — | `Api\CustomOrderController@editCustomOrder` | api | 239 |
| POST | `admin/get-custom-orders` | — | `Api\CustomOrderController@getCustomOrdersAdmin` | api | 240 |
| POST | `admin/create-custom-order` | — | `Api\CustomOrderController@creatCustomOrder` | api | 241 |
| POST | `admin/workshop/table` | — | `Adm\WorkshopController@index` | api | 245 |
| GET | `admin/workshop/active/{id}` | — | `Adm\WorkshopController@activeWorkshop` | api | 246 |
| GET | `admin/workshop/list` | — | `Adm\WorkshopController@listWorkshop` | api | 247 |
| GET | `admin/user/list` | — | `Adm\WorkshopController@listUser` | api | 248 (**duplicate path**, wins over :227) |
| POST | `admin/workshop/product` | — | `Adm\WorkshopController@addProduct` | api | 249 |
| PUT | `admin/workshop/product/{inventory_id}` | — | `Adm\WorkshopController@updateProduct` | api | 250 |
| GET | `admin/workshop/product/{inventory_id}` | — | `Adm\WorkshopController@getProduct` | api | 251 |
| DELETE | `admin/workshop/product/{inventory_id}` | — | `Adm\WorkshopController@deleteProduct` | api | 252 |
| POST | `admin/workshop/product/table` | — | `Adm\WorkshopController@productTable` | api | 253 |
| GET | `admin/workshop/dashboard` | — | `Adm\WorkshopController@dashboard` | api | 254 |
| GET | `admin/workshop/admin/dashboard` | — | `Adm\WorkshopController@adminDashboard` | api | 255 |
| GET | `admin/workshop` | `workshop.index` | `Adm\WorkshopController@index` | api | 256 (resource) |
| GET | `admin/workshop/create` | `workshop.create` | `@create` *(not defined)* | api | 256 |
| POST | `admin/workshop` | `workshop.store` | `Adm\WorkshopController@store` | api | 256 |
| GET | `admin/workshop/{workshop}` | `workshop.show` | `Adm\WorkshopController@show` | api | 256 |
| GET | `admin/workshop/{workshop}/edit` | `workshop.edit` | `@edit` *(not defined)* | api | 256 |
| PUT/PATCH | `admin/workshop/{workshop}` | `workshop.update` | `Adm\WorkshopController@update` | api | 256 |
| DELETE | `admin/workshop/{workshop}` | `workshop.destroy` | `Adm\WorkshopController@destroy` | api | 256 |
| GET | `admin/ciny/list` | — | `Adm\CinyController@listCiny` | api | 261 |
| POST | `admin/ciny/table` | — | `Adm\CinyController@index` | api | 262 |
| GET | `admin/ciny` | `ciny.index` | `Adm\CinyController@index` | api | 263 (resource) |
| GET | `admin/ciny/create` | `ciny.create` | `@create` *(not defined)* | api | 263 |
| POST | `admin/ciny` | `ciny.store` | `Adm\CinyController@store` | api | 263 |
| GET | `admin/ciny/{ciny}` | `ciny.show` | `Adm\CinyController@show` | api | 263 |
| GET | `admin/ciny/{ciny}/edit` | `ciny.edit` | `@edit` *(not defined)* | api | 263 |
| PUT/PATCH | `admin/ciny/{ciny}` | `ciny.update` | `Adm\CinyController@update` | api | 263 |
| DELETE | `admin/ciny/{ciny}` | `ciny.destroy` | `Adm\CinyController@destroy` | api | 263 |
**admin.php total: 216 registered routes** (77 explicit + 139 from 14 `Route::resource` declarations... see count table §3.6).
### 3.2 `routes/api.php` — prefix `api`, middleware `api`
**Public (no auth):**
| METHOD | path | name | controller@method | middleware/guards | line |
|---|---|---|---|---|---|
| GET | `api/user` | — | *Closure* → `$request->user()` | api + **`auth:api`** | 31-33 |
| POST | `api/check/version` | — | `Api\ApiMobileController@checkVersion` | api | 37 |
| POST | `api/login` | — | `Api\ApiMobileController@login` | api | 38 |
| POST | `api/user-auth` | — | `Api\ApiMobileController@userAuth` | api | 39 |
| POST | `api/user-verfiy` | — | `Api\ApiMobileController@userVerfiy` | api | 40 |
| POST | `api/sms` | — | `Api\ApiMobileController@sms` *(method not defined)* | api | 41 |
| GET | `api/sms` | — | `Api\ApiMobileController@sendSms` *(not defined)* | api | 42 |
| GET | `api/report/order` | — | `Adm\AdminController@generateReportOrder` | api | 43 |
| GET | `api/report/doc` | — | `Adm\AdminController@generateReportDocument` | api | 44 |
| POST | `api/customer/create` | — | `Adm\AdminController@createCustomer` | api | 45 |
| POST | `api/product/luxe` | — | `Api\ApiMobileController@getLuxeProduct` | api | 46 |
| GET | `api/price` | — | `Api\ApiMobileController@PriceFromWebService` | api | 47 |
| POST | `api/front` | — | `Api\ApiMobileController@front` | api | 49 |
| GET | `api/config` | — | `Api\ApiMobileController@getConfig` | api | 50 |
| GET | `api/product/{id}` | — | `Api\ApiMobileController@getProduct` | api | 51 |
| POST | `api/get-products` | — | `Api\ApiMobileController@getProducts` | api | 52 |
| GET | `api/ciny/{collection_id}` | — | `Api\ApiMobileController@getCinyByCollectionId` | api | 53 |
| POST | `api/get-products/test` | — | `Api\ApiMobileController@getProductsTest` *(not defined)* | api | 54 |
| POST | `api/log/create/{title}` | — | `Api\LogController@create` | api | 57 |
| GET | `api/log/update/{id}` | — | `Api\LogController@update` | api | 58 |
| POST | `api/major/register` | — | `Major\CustomerController@register` | api | 61 |
**Authenticated group — `Route::middleware('auth:api-customer')` (`routes/api.php:64-117`); guard = `api-customer` → `Customer` model:**
| METHOD | path | name | controller@method | middleware/guards | line |
|---|---|---|---|---|---|
| GET | `api/get-custom-order/{id}` | — | `Api\CustomOrderController@getCustomOrder` | api + `auth:api-customer` | 68 |
| GET | `api/get-custom-orders` | — | `Api\CustomOrderController@getCustomOrders` | api + `auth:api-customer` | 69 |
| POST | `api/create-custom-order` | — | `Api\CustomOrderController@creatCustomOrder` | api + `auth:api-customer` | 70 |
| POST | `api/update-custom-order/{id}` | — | `Api\CustomOrderController@updateCustomOrder` | api + `auth:api-customer` | 71 |
| POST | `api/set-order` | — | `Api\ApiMobileController@setOrder` | api + `auth:api-customer` | 74 |
| POST | `api/product` | — | `Api\ApiMobileController@setProduct` | api + `auth:api-customer` | 75 |
| POST | `api/me-data` | — | `Api\ApiMobileController@me` | api + `auth:api-customer` | 76 |
| POST | `api/get-notify` | — | `Api\ApiMobileController@getNotify` | api + `auth:api-customer` | 77 |
| GET | `api/notify/count` | — | `Api\ApiMobileController@getCountNotify` | api + `auth:api-customer` | 78 |
| POST | `api/get-orders` | — | `Api\ApiMobileController@myOrder` | api + `auth:api-customer` | 79 |
| POST | `api/get-my-findoc` | — | `Api\ApiMobileController@myFindoc` | api + `auth:api-customer` | 80 |
| POST | `api/update-profile` | — | `Api\ApiMobileController@updateProfile` | api + `auth:api-customer` | 81 |
| POST | `api/set-findoc` | — | `Api\ApiMobileController@setFindoc` | api + `auth:api-customer` | 82 |
| POST | `api/my-child` | — | `Api\ApiMobileController@myChild` | api + `auth:api-customer` | 83 |
| POST | `api/add-to-fav` | — | `Api\ApiMobileController@toggleFavorite` | api + `auth:api-customer` | 84 |
| POST | `api/get-favorites` | — | `Api\ApiMobileController@getFavorites` | api + `auth:api-customer` | 85 |
| POST | `api/tokenmobile` | — | `Api\ApiMobileController@updateTokenMobile` | api + `auth:api-customer` | 86 |
| GET | `api/promotion` | — | `Api\ApiMobileController@getPromotion` | api + `auth:api-customer` | 87 |
| GET | `api/ignore` | — | `Api\ApiMobileController@getIgnore` | api + `auth:api-customer` | 88 |
| POST | `api/order/child` | — | `Api\ApiMobileController@getChildOrder` | api + `auth:api-customer` | 89 |
| PUT | `api/order/child/{id}` | — | `Api\ApiMobileController@updateChildOrder` | api + `auth:api-customer` | 90 |
| PUT | `api/notify/read/{id}` | — | `Api\ApiMobileController@readNotify` | api + `auth:api-customer` | 91 |
| POST | `api/file/create` | — | `Adm\FileController@storeWithoutData` | api + `auth:api-customer` | 95 |
| POST | `api/customer/update` | — | `Adm\AdminController@updateCustomer` | api + `auth:api-customer` | 98 |
| POST | `api/customer/toggle-active` | — | `Adm\AdminController@customerToggleActive` | api + `auth:api-customer` | 99 |
| POST | `api/customer/toggle-sts` | — | `Adm\AdminController@customerToggleSts` | api + `auth:api-customer` | 100 |
| POST | `api/hesab_detail/table` | — | `Api\ApiMobileController@hesabDetail` | api + `auth:api-customer` | 103 |
| POST | `api/moshtarian` | — | `Api\ApiMobileController@moshtarian` | api + `auth:api-customer` | 104 |
| GET | `api/tahesab/me` | — | `Customer\TahesabController@mandeh` | api + `auth:api-customer` | 108 |
| GET\|POST | `api/fin_tahesab/table` | — | `Customer\TahesabController@index` | api + `auth:api-customer` | 109 |
| GET | `api/fin_tahesab/detail/{id}` | — | `Customer\TahesabController@detail` | api + `auth:api-customer` | 110 |
| GET | `api/fin_tahesab/my_detail/{offset}` | — | `Customer\TahesabController@my_detail` | api + `auth:api-customer` | 111 |
| POST | `api/major/complete` | — | `Major\CustomerController@complete` | api + `auth:api-customer` | 114 |
**Public (after group):**
| METHOD | path | name | controller@method | middleware/guards | line |
|---|---|---|---|---|---|
| POST | `api/sms/batch` | — | `Adm\SmsController@batch` | api | 122 |
| POST | `api/sms/batch/test` | — | `Adm\SmsController@testNet` | api | 123 |
| GET | `api/send/all` | — | *Closure* — **mass-SMS to ALL customers** | api | 126-180 |
| POST | `api/test` | — | *Closure* → `"this is true"` | api | 184-220 |
| GET | `api/cities` | `cities.index` | `Adm\CitiesController@index` *(not defined)* | api | 223 (resource) |
| GET | `api/cities/create` | `cities.create` | `@create` *(not defined)* | api | 223 |
| POST | `api/cities` | `cities.store` | `Adm\CitiesController@store` | api | 223 |
| GET | `api/cities/{city}` | `cities.show` | `Adm\CitiesController@show` | api | 223 |
| GET | `api/cities/{city}/edit` | `cities.edit` | `@edit` *(not defined)* | api | 223 |
| PUT/PATCH | `api/cities/{city}` | `cities.update` | `Adm\CitiesController@update` | api | 223 |
| DELETE | `api/cities/{city}` | `cities.destroy` | `Adm\CitiesController@destroy` | api | 223 |
| POST | `api/cities/list` | — | `Adm\CitiesController@list` | api | 224 |
| GET | `api/cities/get-percent/{id}` | — | `Adm\CitiesController@getPercent` | api | 225 (**shadowed**) |
| GET | `api/cities/get-percent-aux/{id}` | — | `Adm\CitiesController@getPercentAux` | api | 226 (**shadowed**) |
| POST | `api/web/auth` | — | `WebApp\AuthController@userAuth` | api | 229 |
| GET | `api/testt` | — | `Customer\TahesabController@test` | api | 235 |
> **Duplicate name collision:** `cities.*` resource names are registered in BOTH `routes/admin.php:168` and `routes/api.php:223`. Laravel 8 keeps the first registration for name lookup; `api.php` loads first (RouteServiceProvider:43), so `cities.*` names resolve to `/api/cities/*`.
### 3.3 `routes/customer.php` — prefix `cu`, middleware `api`
Whole file wrapped in `Route::middleware('create_log')` (`routes/customer.php:3`).
| METHOD | path | name | controller@method | middleware/guards | line |
|---|---|---|---|---|---|
| POST | `cu/register` | — | `Api\SingleCustomerController@register` | api, `create_log` | 5 |
| POST | `cu/login` | — | `Api\SingleCustomerController@login` | api, `create_log` | 6 |
| POST | `cu/verfiy/token` | — | `Api\SingleCustomerController@verfiy` | api, `create_log` | 7 |
| GET | `cu/order/pdf/{id}` | — | `Customer\OrderController@generateReportOrder` | api, `create_log` | 8 |
Inner group `['middleware' => ['auth:api-singleCustomer']]` (`:10-46`) — guard `api-singleCustomer` → `SingleCustomer`:
| METHOD | path | name | controller@method | middleware/guards | line |
|---|---|---|---|---|---|
| POST | `cu/transaction` | — | `Api\SingleCustomerController@getTransaction` | api, `create_log`, `auth:api-singleCustomer` | 11 |
| POST | `cu/update-profile` | — | `Api\SingleCustomerController@updateProfile` *(not defined)* | ″ | 12 |
| GET | `cu/get-user` | — | `Api\SingleCustomerController@getUserInfo` | ″ | 13 |
| GET | `cu/comment` | `comment.index` | `Customer\CommentController@index` | ″ | 15 (resource) |
| GET | `cu/comment/create` | `comment.create` | `Customer\CommentController@create` | ″ | 15 |
| POST | `cu/comment` | `comment.store` | `Customer\CommentController@store` | ″ | 15 |
| GET | `cu/comment/{comment}` | `comment.show` | `Customer\CommentController@show($product_id)` | ″ | 15 |
| GET | `cu/comment/{comment}/edit` | `comment.edit` | `Customer\CommentController@edit` | ″ | 15 |
| PUT/PATCH | `cu/comment/{comment}` | `comment.update` | `Customer\CommentController@update` | ″ | 15 |
| DELETE | `cu/comment/{comment}` | `comment.destroy` | `Customer\CommentController@destroy` | ″ | 15 |
| GET\|POST | `cu/fin_tahesab/table` | — | `Customer\TahesabController@index` | ″ | 18 |
| GET | `cu/fin_tahesab/detail/{id}` | — | `Customer\TahesabController@detail` | ″ | 19 |
| GET | `cu/fin_tahesab/my_detail/{offset}` | — | `Customer\TahesabController@my_detail` | ″ | 20 |
| GET | `cu/fin_tahesab/mandeh` | — | `Customer\TahesabController@mandeh` | ″ | 21 |
| POST | `cu/order/table` | — | `Customer\OrderController@index` | ″ | 23 |
| GET | `cu/order/report` | — | `Customer\OrderController@report` | ″ | 24 |
| GET | `cu/order/promotion` | — | `Customer\OrderController@getPromotion` | ″ | 25 |
| GET | `cu/order` | `order.index` | `Customer\OrderController@index` | ″ | 26 (resource) |
| GET | `cu/order/create` | `order.create` | `Customer\OrderController@create` | ″ | 26 |
| POST | `cu/order` | `order.store` | `Customer\OrderController@store` | ″ | 26 |
| GET | `cu/order/{order}` | `order.show` | `Customer\OrderController@show` | ″ | 26 |
| GET | `cu/order/{order}/edit` | `order.edit` | `Customer\OrderController@edit` | ″ | 26 |
| PUT/PATCH | `cu/order/{order}` | `order.update` | `Customer\OrderController@update` | ″ | 26 |
| DELETE | `cu/order/{order}` | `order.destroy` | `Customer\OrderController@destroy` | ″ | 26 |
| GET | `cu/fav` | `fav.index` | `Customer\FavController@index` | ″ | 29 (resource) |
| GET | `cu/fav/create` | `fav.create` | `Customer\FavController@create` | ″ | 29 |
| POST | `cu/fav` | `fav.store` | `Customer\FavController@store` | ″ | 29 |
| GET | `cu/fav/{fav}` | `fav.show` | `Customer\FavController@show` | ″ | 29 |
| GET | `cu/fav/{fav}/edit` | `fav.edit` | `Customer\FavController@edit` | ″ | 29 |
| PUT/PATCH | `cu/fav/{fav}` | `fav.update` | `Customer\FavController@update` | ″ | 29 |
| DELETE | `cu/fav/{fav}` | `fav.destroy` | `Customer\FavController@destroy` | ″ | 29 |
| GET | `cu/address` | `address.index` | `Customer\AddressController@index` | ″ | 32 (resource) |
| GET | `cu/address/create` | `address.create` | `@create` *(not defined)* | ″ | 32 |
| POST | `cu/address` | `address.store` | `Customer\AddressController@store` | ″ | 32 |
| GET | `cu/address/{address}` | `address.show` | `@show` *(not defined)* | ″ | 32 |
| GET | `cu/address/{address}/edit` | `address.edit` | `@edit` *(not defined)* | ″ | 32 |
| PUT/PATCH | `cu/address/{address}` | `address.update` | `Customer\AddressController@update` | ″ | 32 |
| DELETE | `cu/address/{address}` | `address.destroy` | `Customer\AddressController@destroy` | ″ | 32 |
| POST | `cu/set-default` | — | `Customer\AddressController@setDefaultAddress` | ″ | 33 |
| POST | `cu/transaction/gateway` | — | `Customer\TransactionController@gateway` | ″ | 36 |
| POST | `cu/product/variant` | — | `Customer\ProductController@getVariant` | ″ | 39 |
| GET | `cu/customer/wallet` | — | `Customer\CustomerController@getWallet` | ″ | 42 |
| POST | `cu/customer/wallet/transaction` | — | `Customer\CustomerController@detailWallet` | ″ | 43 |
### 3.4 `routes/site.php` — prefix `site`, middleware `api`
| METHOD | path | name | controller@method | middleware/guards | line |
|---|---|---|---|---|---|
| GET | `site/test` | — | *Closure* → `'this is test'` | api | 4-6 |
| GET | `site/get-config` | — | `Api\SiteController@config` | api | 8 |
| POST | `site/config` | — | `Api\SiteController@newConfig` | api | 9 |
| POST | `site/products` | — | `Api\SiteController@Products` | api | 10 |
| GET | `site/get-product` | — | `Api\SiteController@getProduct` | api | 11 |
| GET | `site/front` | — | `Api\SiteController@ّFront` (note leading Arabic kasra U+0651 in method name) | api | 12 |
| GET | `site/counter` | — | `Api\SiteController@counterInstagram` | api | 13 |
| GET | `site/price` | — | *Closure* → `WebServiceGold::getAllPrice()` | api | 14-16 |
| POST | `site/get/products` | — | `Api\SiteController@getProductsVaraints` | api | 17 |
| POST | `site/get/fashion` | — | `Api\SiteController@setRequestFashion` | api | 18 |
| GET | `site/product/ready` | — | `Api\SiteController@readySend` | api | 19 |
| GET | `site/te/print/{id}` | — | *Closure* → `DB\Te\TableEngine::viewPrint($id)` | api | 22-24 |
| GET | `site/comment/{id}` | — | `Customer\CommentController@show` | api | 26 |
| GET | `site/app/download` | — | `Api\SiteController@downloadApk` | api | 27 |
| GET | `site/fix-cat` | — | *Closure* — one-off category remap, `dd()`s | api | 30-65 |
| GET | `site/const/{id}` | — | `Adm\ConstController@show` | api | 68 |
| GET | `site/body/{id}` | — | `Site\CollectionController@getBody` | api | 71 |
| POST | `site/category/product` | — | `Site\CollectionController@products` | api | 72 |
| GET | `site/product` | — | `Site\SiteController@getProduct` | api + **`version`** | 75-78 |
| GET | `site/product/random` | — | `Api\SiteController@getRandomProduct` | api | 79 |
| GET | `site/login/active` | — | `Api\SiteController@counterActive` | api | 80 |
| GET | `site/landing/page` | — | `Api\SiteController@counterLanding` | api | 81 |
| POST | `site/test` | — | `Site\SiteController@products` | api | 83 |
| GET | `site/customer` | — | `Site\SiteController@getCustomer` | api | 84 |
| GET | `site/tag` | — | `Site\SiteController@addTag` | api | 85 |
| GET | `site/birthday` | — | `Site\SiteController@getBirthday` | api | 86 |
| GET | `site/service` | — | `Site\SiteController@serviceGold` | api | 87 |
| GET | `site/test/tr` | — | `Site\SiteController@test123` | api | 88 |
| GET | `site/test/active` | — | `Site\SiteController@searchDeactive` | api | 89 |
| GET | `site/test/g` | — | `Site\SiteController@testG` | api | 90 |
### 3.5 `routes/web.php` — no prefix, middleware `web`
| METHOD | path | name | controller@method | middleware/guards | line |
|---|---|---|---|---|---|
| GET\|POST | `/fin_tahesab/table` | — | `Customer\TahesabController@index` | web | 26 |
| GET | `/fin_tahesab/detail/{id}` | — | `Customer\TahesabController@detail` | web | 27 |
| GET | `/fin_tahesab/my_detail/{offset}` | — | `Customer\TahesabController@my_detail` | web | 28 |
| GET | `/fin_tahesab/mandeh` | — | `Customer\TahesabController@mandeh` | web | 29 |
| GET | `/` | — | *Closure* — **scrapes tgju.org and writes `gold_price` + `gold_price_pwa` ConfigStatic** | web | 32-124 |
| GET | `/login` | — | *Closure* — `Link::set(...)` + redirect to `https://henzagold.com/app` | web | 127-130 |
| GET | `/download` | — | *Closure* — `Link::set(...)` + redirect to APK | web | 131-134 |
| GET | `/totalify` | — | *Closure* — recount category totals, `dd()`s | web | 137-152 |
| GET | `/confirem/order` | — | `Adm\AdminController@confiremOrder` | web | 156 |
| GET | `/test2` | — | *Closure* — **mass-SMS to ALL customers** | web | 158-186 |
### 3.6 Route counts per file
| File | Explicit routes | `Route::resource` blocks | Routes from resources (7 each) | Total registered |
|---|---|---|---|---|
| `routes/admin.php` | 117 | 14 | 98 | **215** |
| `routes/api.php` | 63 | 1 | 7 | **70** |
| `routes/customer.php` | 16 | 4 | 28 | **44** |
| `routes/site.php` | 30 | 0 | 0 | **30** |
| `routes/web.php` | 10 | 0 | 0 | **10** |
| **TOTAL** | **236** | **19** | **133** | **369** |
Resource blocks: admin.php:71,79,129,132,138,151,155,168,182,199,211,235,256,263 (14); api.php:223 (1); customer.php:15,26,29,32 (4).
---
## 4. Controllers (`app/Http/Controllers`, recursive)
**24 controllers total** (excluding base `Controller.php`, which is listed separately as it holds shared business logic).
| Controller | Public methods |
|---|---|
| `Controller.php` (base) | `addLimit`, `convert`, `createTransactionOrder`, `createSingleOrderActivity`, `createSingleCustomerOrders`, `createSingleCustomerOrderItems`, `createPromotionForOrder`, `afterCreateOrder`, `createTransactionInterface`, `checkCustomer` |
| `Adm/AdminController.php` | `__construct`, `login`, `getProducts`, `queryCategory`, `getCustomer`, `getReagent`, `getBanker`, `getShopkeeper`, `getSingleCustomer`, `updateCustomer`, `createCustomer`, `queryCustomer`, `queryReagent`, `customerToggleSts`, `customerToggleActive`, `confiremOrder`, `getAnalytics`, `getAnalyticsTak`, `addProduct`, `addVariant`, `getVariant`, `getVariants`, `getConfigStatics`, `getProduct`, `deleteProduct`, `getCustomerById`, `getCategory`, `AddCategory`, `getOneCategory`, `deleteCategory`, `getSingleOrders`, `changePercent`, `countProductForPercent`, `generateReportOrder`, `orderStsSingle`, `chargeWalletAfterOrder`, `getHavaleList`, `changeAll`, `generateReportDocument`, `getListFashion` |
| `Adm/BranchController.php` | `index`, `store`, `show`, `update`, `destroy`, `activeBranch`, `listBranch`, `listUser`, `allocateProduct`, `addProduct`, `updateProduct`, `getProduct`, `deleteProduct`, `createInventoryTransaction`, `productTable`, `dashboard`, `adminDashboard` |
| `Adm/BrandController.php` | `index`, `store`, `update`, `destroy` |
| `Adm/CashController.php` | `index`, `create`, `store`, `show`, `edit`, `update`, `destroy`, `toggleConfiremCash` |
| `Adm/CategoryController.php` | `index` |
| `Adm/CinyController.php` | `store`, `update`, `destroy`, `show`, `listCiny`, `index` |
| `Adm/CitiesController.php` | `store`, `destroy`, `show`, `list`, `getPercent`, `getPercentAux`, `update` |
| `Adm/CommentController.php` | `index`, `create`, `store`, `show`, `edit`, `update`, `destroy`, `changeStatus` |
| `Adm/ConstController.php` | `store`, `destroy`, `deleteById`, `show`, `update` |
| `Adm/DashboardController.php` | `getAll`, `change`, `info`, `link` |
| `Adm/FileController.php` | `index`, `store`, `storeWithoutData`, `show`, `update`, `destroy`, `updateBody` |
| `Adm/IgnoreController.php` | `index`, `store`, `show`, `destroy` |
| `Adm/LogController.php` | `queryTitle`, `queryAllCustomer`, `queryCustomer`, `queryForLog`, `listCu`, `ago` |
| `Adm/MajorCustomerController.php` | `index`, `dashboard`, `order`, `cash`, `shopkeeper`, `indexSms`, `verifyRegister`, `indexArchive`, `indexVerify`, `archive`, `listTypeCustomer` |
| `Adm/MajorOrderController.php` | `index`, `create`, `store`, `show`, `edit`, `update`, `destroy`, `getTaminOrders`, `orderSts`, `orderItemEdit`, `deleteOrder`, `deleteOrderItem` |
| `Adm/NotifyController.php` | `store`, `customer`, `index`, `getUnreadNotify`, `show`, `read` |
| `Adm/OrderController.php` | `getType`, `store`, `update` |
| `Adm/PermissionController.php` | `create`, `store`, `destroy`, `has`, `get`, `getByUt` |
| `Adm/ProductController.php` | `createImage`, `store`, `show`, `update`, `testSearch`, `report`, `color`, `getProductsVaraints`, `changeOffer`, `changeIsExist`, `changeHasWarranty` |
| `Adm/PromotionController.php` | `index`, `create`, `createTablePromotionProduct`, `createPromotionProduct`, `store`, `show`, `edit`, `update`, `destroy`, `updateAutoPromotion`, `getAutoPromotion`, `getBrand` |
| `Adm/ReportController.php` | `__construct`, `collection`, `registerEvents`, `headings`, `getOrderItemWithDetails` (implements Maatwebsite Excel export contracts) |
| `Adm/SingleCustomerController.php` | `store`, `update`, `delete`, `show`, `indexOrder`, `indexTransaction`, `indexSms`, `getWallet`, `report` |
| `Adm/SmsController.php` | `batch`, `testNet`, `replaceName`, `index`, `getCustomer`, `sendSmsToOther` |
| `Adm/UserController.php` | `store`, `index`, `destroy`, `update`, `show`, `showData` |
| `Adm/WalletController.php` | `charge`, `changePercent`, `index`, `setConst`, `getConfig`, `getConfigBirthday` |
| `Adm/WorkshopController.php` | `index`, `store`, `show`, `update`, `destroy`, `activeWorkshop`, `listWorkshop`, `listUser`, `allocateProduct`, `addProduct`, `updateProduct`, `getProduct`, `deleteProduct`, `createInventoryTransaction`, `productTable`, `dashboard`, `adminDashboard` |
| `Api/ApiMobileController.php` | `userAuth`, `userVerfiy`, `login`, `me`, `setFindoc`, `updateProfile`, `getProduct`, `myChild`, `myOrder`, `getNotify`, `getCountNotify`, `myFindoc`, `front`, `getCinyByCollectionId`, `getProducts`, `setProduct`, `toggleFavorite`, `getFavorites`, `updateTokenMobile`, `getConfig`, `getPromotion`, `getIgnore`, `setOrder`, `getChildOrder`, `readNotify`, `updateChildOrder`, `getLuxeProduct`, `checkVersion`, `PriceFromWebService`, `hesabDetail`, `moshtarian`, `moshtarianFilter` |
| `Api/CustomOrderController.php` | `getCustomOrders`, `getCustomOrdersAdmin`, `getCustomOrder`, `editCustomOrder`, `creatCustomOrder`, `updateCustomOrder`, `acceptCustomOrder`, `deleteCustomOrder` |
| `Api/CustomerController.php` | `getVaraints` |
| `Api/LogController.php` | `create`, `update` |
| `Api/SingleCustomerController.php` | `register`, `verfiy`, `login`, `getTransaction`, `getUserInfo` |
| `Api/SiteController.php` | `getConfig`, `config`, `newConfig`, `getMenu`, `getProduct`, `sendDiscount`, `products`, `searchProduct`, `ّFront`, `getProductsVaraints`, `test`, `counterInstagram`, `getRandomProduct`, `counterActive`, `counterLanding`, `setRequestFashion`, `readySend` |
| `Customer/AddressController.php` | `index`, `store`, `update`, `destroy`, `setDefaultAddress` |
| `Customer/CommentController.php` | `index`, `create`, `store`, `show`, `edit`, `update`, `destroy` |
| `Customer/CustomerController.php` | `getWallet`, `detailWallet` |
| `Customer/FavController.php` | `index`, `create`, `store`, `show`, `edit`, `update`, `destroy` |
| `Customer/OrderController.php` | `index`, `create`, `store`, `cancel`, `show`, `edit`, `update`, `destroy`, `report`, `generateReportOrder`, `getPromotion` |
| `Customer/ProductController.php` | `getVariant` |
| `Customer/TahesabController.php` | `index`, `my_detail`, `detail`, `mandeh`, `test` |
| `Customer/TransactionController.php` | `gateway` |
| `Major/CustomerController.php` | `register`, `complete`, `sendSmsToAdmin` |
| `ProductController.php` (root) | `index` |
| `Site/CollectionController.php` | `getBody`, `products` |
| `Site/SiteController.php` | `products`, `getCustomer`, `getProduct`, `addTag`, `getBirthday`, `serviceGold`, `test123`, `searchDeactive`, `testG` |
| `WebApp/AuthController.php` | `userAuth` |
**Controller count: 46 files** (45 concrete controllers + base `Controller.php`).
---
## 5. Models (`app/Models`) — 57 files
> `Selector` (`app/DB/Selector.php`) is a shared trait providing Jalali accessors (`caj`,`uaj`,`eaj`,`daj`), `sts_str`, `type_str`, `sts_verify_step_one_str`/`two_str`, `type_show_str`, `birth_date_j`, `brj`, `expire_at_j`, plus static lookups `GetSts`, `GetTypes`, `GetConfirmed`, `GetShowType`, `GetStsVerifyStepOne/Two`, `selectWithjDate`, `convertColumns` (`app/DB/Selector.php:57-206`). Duplicated at `app/Te/*` and `app/DB/Te/*` (TableEngine).
| Model | Table | Key fillable | Casts | Relations | Scopes | Constants / statics | Accessors / mutators |
|---|---|---|---|---|---|---|---|
| `ApkLog` `:14` | `apk_log` `:17` | `ip`,`version` `:30` | — | — | — | `$timestamps=false` `:19`; `boot()` `:21` | — |
| `Branch` `:10` | *(branches)* | `name`,`address`,`phone`,`user_id`,`is_active` `:13` | `is_active`→bool `:20` | `user()`→`User` `:23` | — | — | — |
| `Category` `:10` | `category_collection` `:32` | `title`,`parent_id_id`,`priority`,`image_id`,`meta`,`body`,`icon_id`,`disabled`,`type`,`total`,`is_master`,`header`,`is_active` `:16` | `disabled`,`is_active`→bool `:45` | `parent()` hasOne self `:51`; `childs()` hasMany self `:57`; `image()`→`Image` `:63`; `icon()`→`Image` `:68` | `scopeDesktop` `:74`, `scopeMobile` `:80` | `reloadTotal($id)` `:86`, `reloadCategory($cats,$pid)` `:93` | `$hidden` `:34` |
| `Ciny` `:9` | `cinies` | `title`,`icon_id`,`collection_id`,`is_active`,`description` `:12` | `:19` | `icon()`→`Image` `:22`; `collection()`→`Category` `:26` | — | — | — |
| `CinyProduct` `:9` | `ciny_products` | `product_id`,`ciny_id` `:12` | — | `product()` `:16`; `ciny()` `:20` | — | — | — |
| `City` `:9` | `cities` | `title`,`parent`,`level`,`percent`,`percent_aux` `:14` | — | — | — | — | — |
| `Collect` `:9` | `category_collect` `:14` | `collection_id_id`,`product_id_id` `:16` | — | `product()` `:21`; `collection()` `:26` | — | — | — |
| `Comment` `:10` | *(comments)* | `product_id`,`comment`,`reply_to`,`sts`,`customer_id` `:16` | — | `Reply()` hasMany self `:31`; `customer()`→`SingleCustomer` `:36`; `product()` `:42` | — | `$SELECT_CAJ` (via Selector) | — |
| `ConfigStatic` `:10` | `config_statics` `:15` | `key`,`val` `:17` | *(`val`→array commented out `:21-23`)* | — | — | **`getValue($key)` `:25` — throws `NotFoundHttpException` if 0 rows** | — |
| `CustomOrder` `:12` | *(custom_orders)* | `customer_id`,`file_id`,`city_id`,`weight`,`color`,`size`,`sum`,`color_stone`,`color_mina`,`workshop_id`,`sts`,`description` `:17` | `sts`→int, `weight`/`sum`→float, `deleted_at`→datetime `:32` | `file()`→`Image` `:77`; `customer()` `:81`; `CustomOrderLog()` hasMany `:85` | — | `$appends=['sts_str_en','sts_str_fa','time_creat_fa','time_update_fa']` `:15` | `getTimeCreatFaAttribute` `:59`, `getTimeUpdateFaAttribute` `:63`, `getStsStrEnAttribute` `:68`, `getStsStrFaAttribute` `:73` |
| `CustomOrderLog` `:10` | — | `:15` | `:23` | `order()` `:37`; `user()` withDefault `:43` | — | `$appends` `:13` | `getTimeCreatFa/UpdateFa` `:28`,`:32` |
| **`Customer`** `:13` (Authenticatable, guard `api-customer`) | `customers` | `api_token`,`address`,`phone`,`fname`,`lname`,`name`,`mobile`,`rnd`,`ncode`,`notify_count`,`total_order`,**`gram_level`**,**`percent`**,`input`,`output`,**`remain`**,`city_id`,`new_city`,`rial_remain`,`rial_output`,`rial_input`,`reagent_id`,`type`,`open_order`,`child`,`sts`,`token_mobile`,`send_password`,`store_name`,`zone`,`pasazh_name`,`active`,`birth_date`,`marriage_date`,`image_id`,`mcode`,`sts_verify_step_one`,`sts_verify_step_two`,`is_archive`,`business_license_image_id`,`shop_image_id`,`ncode_image_id` `:18-72` | `is_archive`→bool `:73` | `reagent()` `:119`; `cash()` hasMany `Major\Cash` `:124`; `order()` hasMany `:130`; `orderOne()` (sts=6) `:135`; `children()` hasMany Order by `reagent_id` `:140`; `city()` `:145`; `image()` `:166`; `business_license_image()` `:170`; `shop_image()` `:174`; `ncode_image()` `:178`; `workshop()` `:182` | `scopeSelectify` `:105`, `scopeActive` `:162` | `$hidden=['password','send_password']` `:83` | `setPasswordAttribute` (Hash) `:100`; `getToAttribute` `:110`; `getOrderTitleAttribute` `:115`; `getRemainAttribute` `:150`; `getGramsSellAttribute` `:156` |
| `CustomerTest` `:13` | `customers_test` `:18` | mirror of `Customer` `:20` | — | same set `:94-137` | `scopeSelectify`,`scopeActive` | `$hidden` `:62` | same |
| `Fav` `:14` | `fav` `:17` | `customer_id`,`product_id` `:23` | — | `product()` (active only) `:30`; `customer()` `:34` | — | `$timestamps=false` `:22`; composite PK (migration) | — |
| `Image` `:8` | `image` `:13` | `title`,`position`,`image`,`resize*`,`link`,`width`,`height`,`banner`,`original_name`,`ext`,`body` `:15` | — | `product()` `:42`; `productImage()` `:46`; `pi()` `:53` | — | `$SELECT_FULL` | `getFullAttribute` `:35`, `getImageAttribute` `:58` |
| `InventoryBranch` `:10` | — | `:13` | — | `allocator_user()` `:20`; `branch()` `:24`; `product()` `:28` | — | — | — |
| `InventoryTransaction` `:9` | — | `product_id`,`product_variant_id`,`branch_id`,`allocator_user_id`,`order_id`,`type` `:12` | — | `product()` `:25`; `branch()` `:29`; `product_variant()` `:33`; `allocator_user()` `:37` | — | — | — |
| `Kids` `:10` | `kids` `:15` | `:17` | — | — | — | — | — |
| `Link` `:8` | *(links)* | `:11` | — | — | — | **`set($url)` `:15`** — click tracker | — |
| `Log` `:9` | *(logs)* | `customer_id`,`time_present`,`start_at`,`end_at`,`ip`,`title`,`data`,`title_fa`,`product_id` `:12` | `data`→array `:23` | `customer()` `:27`; `product()` `:31` | — | — | — |
| **`Major\Cash`** `:13` | *(cashes)* | `user_id`,`customer_id`,`input`,`output`,`remain`,`rial_input`,`rial_output`,`rial_remain`,`type`,`sts`,`title`,`to`,`note`,`ayar`,`lab`,`balance`,`value`,`price`,`confirmed`,`no`,`proceed_at`,`account_name`,`bank_name`,`recive_date`,`order_id`,`ang`,`original_value`,`price_per_gram`,`discount` `:19` | — | `customer()` `:86` | `scopeMe` `:74` | **`$TYPES = [9=>سفارش, 1=>آب شده, 2=>حواله, 3=>چک, 4=>پولی]` `:56`; `$STS = [-1=>بررسی نشده, 1=>ثبت سند]` `:63`; `$CONFIRMED = [1=>تایید شده, 0=>تایید نشده]` `:68`** | `getTimeAttribute` `:81`; `getValueAttribute` → `Utility::round()` `:91`; `getConfirmedStrAttribute` `:96`; `$appends=['time','confirmed_str']` `:51` |
| `Major\Favorite` `:9` | `major_favorites` `:14` | `:16` | — | — | — | — | — |
| **`Major\Promotion`** `:14` | `major_promotion` `:21` | `title`,`description`,`start_at`,`end_at`,`type`,`type_customer`,`discount_percent`,`discount_price`,`is_active`,`use_only`,`show` `:32` | `is_active`,`use_only`,`show`→bool `:57` | `items()` hasMany `PromotionItem` `:86` | `scopeActive` `:24`, `scopeWeb` `:27` | `safeDelete($promotionID)` `:79` | `getStartAtjAttribute` `:69`, `getEndAtjAttribute` `:74`, `getItemsAttribute` `:91`; `$appends` `:63` |
| `Major\PromotionIgnore` `:11` | `major_promotion_ignores` `:17` | `:19` | — | — | — | — | — |
| `Major\PromotionItem` `:11` | `major_promotion_item` `:17` | `:19` | — | `promotion()` `:24` | — | — | — |
| `Major\PromotionUsed` `:10` | `major_promotion_used` `:16` | `customer_id`,`promotion_id`,`promotion_item_id`,`order_id`,`order_item_id` `:18` | — | — | — | — | — |
| `Notification` `:11` | — | `:13` | `:21` | `user()` `:24`; `customer()` `:28` | — | `store($token,$title,$body)` `:33` (FCM) | — |
| **`Notify`** `:14` | `notify` `:17` | `title`,`customer_id`,`user_id`,`note`,`read`,`value`,`type`,`type_send`,`sender_id` `:21` | `read`→bool `:32` | — | `scopeMe` `:50` | **`store($token,$title,$body)` `:57` (FCM push); `sendToGroup($tokens,$title,$body)` `:88`** | — |
| **`Order`** `:16` (major/wholesale order) | `orders` | `customer_id`,`reagent_id`,`type`,`input`,`output`,`remain`,`w`,`wage`,`city_id`,`price`,`percent`,`sts`,`confirmed`,`rial_input`,`discount` `:25` | *(empty `:144`)* | `items()` hasMany `OrderItem` `:149`; `customer()` `:156`; `parent()`→Customer by `reagent_id` `:168`; `activity()` hasMany `OrderActivity` `:173`; `cashe()` hasOne `Major\Cash` by `order_id` `:232` | `scopeMe` `:161` | **`$TYPES=[1=>ثبت سفارش توسط مسئول فروش, 2=>ثبت مشتری]` `:45`; `$CONFIRMED=[1=>تایید شده,2=>در انتظار تایید]` `:60`; `$STS = [0=>معلق, 1=>ثبت سفارش, 2=>تایید مدیر سیستم, 3=>درحال تامین, 4=>درحال آماده سازی, 5=>تحویل مسئول فروش, 6=>تحویل مشتری]` `:65`; `$STS_TAB` `:73`; `$STS_COLOR` `:105`; `$STS_IMG` `:122`; `$SELECT_COLOR`,`$SELECT_IMG` `:23-24`; `safeDelete($orderID)` `:211`** | `getConfirmedStrAttribute` `:178`, `getColorAttribute` `:184`, `getImgAttribute` `:190`, `getInputAttribute` `:197`, `getPercentAttribute` `:202`, `getWageAttribute` `:207`, `getWAttribute` `:222`, `getTimeAttribute` `:227`; `$appends=['time','confirmed_str']` `:121`; `$hidden` `:138` |
| `OrderActivity` `:10` | `order_activity` `:13` | `title`,`note`,`user_id`,`customer_id`,`order_id`,`meta`,`proceed`,`key` `:17` | `:28` | `order()` `:34`; `customer()` `:39`; `user()` `:44` | — | — | — |
| `OrderItem` `:10` | `order_items` | `order_id`,`variant_id`,`product_id`,`product_title`,`variant_title`,`percent`,`price`,`rial_pay`,`note`,`wage`,`qty`,`sts`,`w`,`rash_weight`,`original_rash_weight`,`discount`,`original_percent` `:15` | — | `product()` `:36`; `variants()`→`ProductVariant` `:40`; `order()` `:54` | — | — | `getWAttribute` → `Utility::round` `:45`; `getWageAttribute` `:49` |
| **`Permission`** `:7` | `permission` `:14` | `rid`,`ut` `:9` | — | — | — | **Composite PK `['rid','ut']` `:16`; `$timestamps=false` `:17`; `$incrementing=false` `:18`** | — |
| **`Product`** `:14` | `product` `:20` | `title`,`body_html`,`desk_note`,`product_code`,`indicator_id_id`,`mobile_pic_id`,`number_of_purchase`,`is_active`,`brand_id`,`disabled`,`file_id`,`show_type`,`delivery_type`,`message_long_delivery`,`count_fav`,`count_order`,`count_view`,`min_gram`,`max_gram`,`offer_day`,`branch_id`,`inventory_id`,`is_exist`,`has_warranty` `:22` | `is_active`,`disabled`,`offer_day`,`is_exist`,`has_warranty`→bool `:60` | `images()` `:75`; `ciny()` `:79`; `thumb()` `:90`; `tmobile()` `:96`; `variants()` (active) `:102`; `variantsAll()` `:106`; `variantOne()` `:111`; `variantsPromotion()` `:116`; `collect()` `:123`; `category()` `:128`; `image()` `:140`; `brand()`→`ProductBrand` `:145`; `gallery()` hasManyThrough `:153`; `file()` `:187`; `tags()` `:192`; `promotion()`→`PromotionProduct` `:196`; `inventory()`→`InventoryBranch` `:204`; `branch()` `:209` | `scopeCool` `:69`, `scopeMobile` `:173`, `scopeSite` `:178`, `scopeActive` `:182` | `safeDelete($productId)` `:165` | — |
| `ProductBrand` `:10` | `product_brand` `:17` | `:19` | — | — | — | — | — |
| `ProductImage` `:9` | `products_productimage` `:14` | `:15` | — | `image()` `:22`; `product()` `:27` | `scopeMobile` `:32`, `scopeDesktop` `:37` | — | — |
| `ProductTags` `:10` | `product_tags` `:16` | `product_id`,`const_id` `:18` | — | `product()` `:24`; `cnt()`→`ConfigStatic` `:28` | — | `$timestamps=false` `:15` | — |
| **`ProductVariant`** `:15` | `products_productvarient` `:20` | `title`,`qty`,**`grams`**,**`percentage_pay`**,`product_id_id`,`barcode`,`color`,`width`,`length`,`height`,**`rial_pay`**,`inventory`,`details`,`description`,`meta`,`position`,`requires_shipping`,`taxable`,`is_pre_order`,`is_active` `:24` | `is_pre_order`,`is_active`→bool; `rial_pay`,`percentage_pay`,`grams`→double `:121` | `product()` `:312`; `promotion()` `:316`; `inventory()`→`InventoryTransaction` `:324`; `inventory_branch()` `:328` | — | **`$product_price_extra_percent` (loaded in `boot()` from ConfigStatic `product_price_extra_percent`, default `7`) `:45-52`; `$goldPrice` `:132`; `$dis_product` `:133`; `$COLOR_STR` `:57`; `$COLORS = [0=>سفید,1=>زرد,2=>شکلاتی,3=>رزگلد]` `:94`** | **`getPriceAttribute` `:156` (core gold price formula); `getPriceWithDiscountAttribute` `:248`; `getColorStrAttribute` `:135`; `$appends=['price','color_str','price_with_discount']` `:115`** |
| `PromotionProduct` `:9` | `promotion_products` | `promotion_id`,`promotion_item_id`,`product_variant_id`,`product_id`,`discount`,`type`,`active`,`start_at`,`end_at`,`use_only` `:12` | `active`,`use_only`→bool `:28` | — | — | — | — |
| `RequestFashion` `:8` | — | `:11` | — | — | — | — | — |
| `RequestLog` `:9` | *(request_logs)* | `customer_id`,`ip`,`device`,`data`,`user_agent`,`route`,`method` `:14` | `data`→array `:23` | — | — | `UPDATED_AT = null` `:12`; `$timestamps=['created_at']` `:13` | — |
| **`SingleCustomer`** `:13` (Authenticatable, guard `api-singleCustomer`) | **`login`** `:19` | `id`,`password`,`username`,`lname`,`fname`,`name`,`phone`,`email`,`user_type`,`is_active`,`last_login`,**`token_sms`**,**`percent`**,`birth_day`,`branch_id` `:21` | — | `transaction()` `:45`; `branch()` `:48` | — | `$hidden=['password']` `:39` | `setPasswordAttribute` (Hash) `:41` |
| `SingleCustomer\OrderActivity` `:10` | `orders_histroy` `:17` | `:19` | `:28` | — | — | — | — |
| `SingleCustomer\PromotionUsed` `:10` | `single_promotion_used` `:15` | `:17` | — | — | — | — | — |
| `SingleCustomerAddress` `:13` | `accounts_address` `:19` | `reciver_lname`,`reciver_fname`,`reciver_phone`,`reciver_fixed_phone`,`province`,`city`,`zipcode`,`address`,`latitude`,`longitude`,`is_default`,`user_id_id` `:21` | *(empty `:36`)* | — | — | — | — |
| **`SingleCustomerOrderItems`** `:12` | `orders_orderitem` `:18` | `title`,`price`,`approximate_price`,`qty`,`deposit`,`amount_paid`,`approximate_gram`,`exact_gram`,`debtor_amount`,`is_active`,`supply_at`,`delivery_at`,`order_id`,`product_id`,`variant_id`,**`grams`**,**`rial_pay`**,`variant_title`,`is_preorder`,`promotion_id`,`promotion_item_id`,`total_discount`,`total_price`,**`gold_price_in_formula`**,`branch_id` `:20` | `is_preorder`→bool `:48` | `product()` `:54` | — | — | — |
| **`SingleCustomerOrders`** `:13` (retail order) | **`orders_orders`** `:19` | `app`,`billing_address`,`browser_ip`,`buyer_accepts_marketing`,`email`,`phone`,`note`,`prcessing_method`,`processed_at`,`shipping_address`,`shipping_line`,`subtotal_price`,`discount_codes`,`total_discount`,**`total_price`** (pre-discount),**`price`** (post-discount),`total_tax`,**`total_weight`**,`tags`,`sts`,`is_active`,`canceled_at`,`canceled_reason`,`closed_at`,`send_time_id`,`user_id_id`,`gold_price`,**`gold_price_original`**,`type`,`reagent_id`,**`use_wallet`**,**`payment_after_wallet`**,`branch_id` `:21` | `shipping_address`→array, `use_wallet`→bool `:61` | `items()` `:84`; `customer()`→`SingleCustomer` by `user_id_id` `:93`; `user()`→`User` by `reagent_id`; `branch()`; `activity()` hasMany `SingleCustomer\OrderActivity` | — | **`$STS = [1=>ثبت سفارش, 2=>تایید مدیر سیستم, 3=>در حال تامین, 4=>در حال آماده سازی, 5=>خروج از انبار, 6=>ارسال, 7=>تحویل مشتری]` `:66`; `$TYPES = [1=>تلفنی,2=>تلگرام,3=>واتس آپ,4=>اینستا,5=>حضوری]` `:76`** | `getStsStrAttribute` `:88`; `$appends=['sts_str']` `:57` |
| `SingleCustomerTransaction` `:16` | `transactions_transaction` `:22` | `amount`,`transaction_type`,`sts`,`message`,`receipt`,`gateway`,`is_active`,`order_id_id`,`user_id_id` `:24` | — | — | — | **`createAndGetToken($order,$ip)` `:38` — payping/zarinpal payment init (hardcoded amount 1000, hardcoded bearer token `:86`)** | — |
| **`Sms`** `:12` | **`messages`** `:16` | `customer_id`,`mobile`,`massage`,`token`,`type`,`user_id` `:44` | — | — | — | **`$TYPES=[-1=>یک شماره تلفن,0=>همه کاربران,1=>کارشناسان فروش,2=>مشتریان عمده,9=>چند شماره تفن]` `:53`; `Send($to,$msg,…)` `:61`; `SendSms(...)` `:89`; `set(...)` `:125`** | — |
| `TransactionOrder` `:8` | *(transaction_orders)* | `customer_id`,`order_id`,`total_price`,`transaction_type`,`total_weight`,`order_date`,`payment` `:11` | `payment`→bool `:20` | — | — | — | — |
| **`User`** `:12` (Authenticatable, guard `api`/`web`) | `users` | `name`,`fname`,`lname`,`mobile`,**`ut`** (user type = role id),`city_id`,`token_mobile`,`branch_id` `:22` | `email_verified_at`→datetime `:48` | `user_type()`→`ConfigStatic` by `ut` `:51` | — | `$hidden=['password','remember_token']` `:38`; SoftDeletes `:15` | — |
| **`Utility`** `:6` | *(not a real table)* | — | — | — | — | **`$formatJ='Y/n/j-H:i'` `:8`; `round($value)` → `round($value, 2)` `:10`; `convertDate()` (Jalali) `:15`; `faTOen()` Persian/Arabic digit → ASCII `:26`; `baseURLStorage()` = `https://henzagold.com/oapi/storage/` `:30`** | — |
| **`WebServiceGold`** `:13` | *(not a real table)* | — | — | — | — | **`getPriceGold18Ayar()` `:17` — reads `ConfigStatic['gold_price']`, `geram18.p`, strips commas, `intval`, **`/10`** (rial→toman); `getAllPrice()` `:27`; `getAllPriceFromWebService()` `:33` — ratebox.ir feed, maps `4`→mesghal, `5`→geram18, `6`→geram24, writes `ConfigStatic['gold_price']` only when all three present** | — |
| `Workshop` `:10` | *(workshops)* | `name`,`address`,`phone`,`user_id`,`is_active` `:13` | `is_active`→bool `:20` | `user()`→**`Customer`** (note: class mismatch vs. column) `:23` | — | SoftDeletes `:12` | — |
**Model count: 57 files** under `app/Models` (incl. 6 in `Major/`, 2 in `SingleCustomer/`, and `Selector.php` which is a trait, not a model).
---
## 6. Database
### 6.1 Migrations — `database/migrations` (31 files)
| # | File | Op | Table | Key columns |
|---|---|---|---|---|
| 1 | `2014_10_12_000000_create_users_table.php` | create | `users` | `id`, `name`, `mobile` **unique**, `password`, `rememberToken`, `timestamps`, `softDeletes` (`:16-23`) |
| 2 | `2014_10_12_100000_create_password_resets_table.php` | create | `password_resets` | `mobile` **index**, `token`, `created_at` nullable (`:16-19`) |
| 3 | `2019_08_19_000000_create_failed_jobs_table.php` | create | `failed_jobs` | `id`, `uuid` **unique**, `connection`, `queue`, `payload`, `exception`, `failed_at` useCurrent (`:16-23`) |
| 4 | `2020_10_01_120354_order_table.php` | create | `orders` | `id`, `customer_id` uint, `reagent_id` uint null, `type` tinyint, `input`/`output`/`remain`/`w`/`wage`/`percent` double null, `sts` tinyint null, timestamps, softDeletes (`:16-29`) |
| 5 | `2020_10_01_120408_order_items_table.php` | create | `order_items` | `id`, `order_id`, `product_id`, `product_title`, `variant_title`, `w`/`wage`/`percent` double null, timestamps (`:16-25`) |
| 6 | `2020_10_01_120430_customers_table.php` | create | `customers` | `id`, `fname(50)`, `lname(50)`, `name(75)`, `mobile(20)`, `ncode(20)`, `password`, `input`/`output` double, `remain` double **default 0**, `rial_input`/`rial_output`/`rial_remain` bigint uint, `reagent_id`, `type`/`sts` tinyint, timestamps, softDeletes (`:16-34`) |
| 7 | `2020_10_01_120914_cash_table.php` | create | `cashes` | `id`, `user_id`, `customer_id`, `input`/`output`/`remain` double, `rial_*` bigint, `type`/`sts` tinyint, timestamps, softDeletes (`:16-29`) |
| 8 | `2020_10_19_153228_customer_add.php` | alter | `customers` | +`api_token(60)`, `rnd(60)`, `notify_count` def 0, `total_order` def 0, **`gram_level` double def 0**, **`percent` double def 0**, `open_order` def 0, `child` def 0 (`:26-34`) |
| 9 | `2020_10_25_155247_customers_cols.php` | alter | `customers` | +`phone(15)`, `address` (`:16-18`) |
| 10 | `2020_10_25_164102_order_acitvity.php` | create+alter | `order_activity`; `order_items`; `orders`; `cashes` | create `order_activity`: `id`,`title(50)`,`note`,`user_id`,`customer_id`,`order_id`,`meta` json,timestamps (`:16-24`). alter `order_items`: +`variant_id`,`price` double,`sts` tinyint,`qty` (`:26-30`). alter `orders`: +`price` double (`:32-34`). alter `cashes`: +`title`,`to`,`note`,`ayar(20)`,`lab`,`balance` double,`value` double,`price` bigint (`:36-44`) |
| 11 | `2020_10_25_170047_notify_table.php` | create | `notify` | `id`,`title`,`customer_id`,`user_id`,`note(512)`,`type` tinyint,`value` tinyint,`read` bool **def false**,timestamps (`:16-25`) |
| 12 | `2020_10_27_192859_proceed_activity.php` | alter | `order_activity` | +`proceed` bool **def false** (`:16-17`) |
| 13 | `2021_09_18_115342_permissions_table.php` | create | `permission` | `rid` string, `ut` int, **primary(['rid','ut'])** (`:16-19`) |
| 14 | `2021_09_18_164552_file_id_product.php` | alter | `product` | +`file_id` uint null (`:16-17`) |
| 15 | `2021_09_18_171700_city_table.php` | create+alter | `cities`; `users`; `orders` | create `cities`: `id`,`title(50)`,`parent`,`level`,`percent` float,timestamps,softDeletes (`:16-23`). alter `users`: +**`ut`** uint null, `city_id` (`:27-29`). alter `orders`: +`city_id` (`:32-33`) |
| 16 | `2021_09_18_172722_cities_p_2.php` | alter | `cities` | +`percent_aux` float null (`:16-17`) |
| 17 | `2021_09_21_160213_product_tags.php` | create | `product_tags` | `id` increments, `product_id`, `const_id` (`:16-19`) |
| 18 | `2021_09_28_173620_customer_city_id.php` | alter | `customers` | +`city_id` uint null (`:16-17`) |
| 19 | `2021_10_21_092358_fav_table.php` | create | `fav` | `customer_id`, `product_id`, **primary(['customer_id','product_id'])** (`:16-19`) |
| 20 | `2021_10_21_094849_apk_log.php` | create | `apk_log` | `id`,`ip(15)`,`version`,`created_at` (`:16-20`) |
| 21 | `2021_11_01_155307_mobile_note.php` | alter | `product` | +`desk_note` null (`:16-17`) |
| 22 | `2021_11_01_162712_cat_image.php` | alter | `category_collection` | +`image_id` null (`:16-17`) |
| 23 | `2021_11_01_164540_cat_image_total.php` | alter | `category_collection` | +`total` **def 0** (`:16-17`) |
| 24 | `2021_11_03_113752_icon_category.php` | alter | `category_collection` | +`icon_id` null (`:16-17`); down() targets `category` (mismatch, `:28`) |
| 25 | `2021_11_07_103558_product_gallery_id.php` | alter | `products_productimage` | +`w` string null (`:16-17`) |
| 26 | `2021_11_15_202459_type_category.php` | alter | `category_collection` | +`type` tinyint null (`:16-17`) |
| 27 | `2021_11_17_105202_body_category.php` | alter | `category_collection` | +`body` null (`:16-17`) |
| 28 | `2021_11_23_114049_product_disabled.php` | alter | `product`; `category_collection` | +`disabled` bool **def false** (`:16-17`); `category_collection.disabled` bool nullable(false) (`:20-21`) |
| 29 | `2021_12_18_161611_create_promotion_products.php` | create | `promotion_products` | `id`,`promotion_id`,`promotion_item_id`,`product_variant_id`,`product_id`,`discount_percent` double,`discount_price` double,timestamps (`:16-24`) |
| 30 | `2024_06_14_232311_create_cinies.php` | create | `cinies` | `id`,`title(150)`,`icon_id`,`collection_id`,`is_active` bool **def 1**,`description(1024)`,timestamps,softDeletes (`:16-24`) |
| 31 | `2024_06_21_203640_create_ciny_products.php` | create | `ciny_products` | `id`,`product_id`,`ciny_id`,timestamps,softDeletes (`:16-21`) |
**Package migration (autoloaded via `WalletServiceProvider`):**
| File | Table | Columns |
|---|---|---|
| `packages/rooyesh/wallet/src/migrations/2021_12_02_065938_create_wallet_moudle_table.php:16-39` | **`wallets`** | `id`, `owner_type` enum(1=user,2=shop) **def 1**, timestamps, `owner` bigint, **`gold` double** (gold grams held), `hash(128)` null, **`credit` bigint**, **unique(['owner','owner_type'])** |
| ″ `:41-56` | **`transactions`** | `id`, `wallet` bigint uint, `transaction_type` enum(1=gold,2=credit,3=expire) **def 1**, timestamps, `value` bigint, `expire_at` datetime null, `hash(128)` null, `note` text null, `customer_id` bigint uint null |
| ″ `:58-66` | **`transaction_metas`** | `transaction` bigint uint, `order_id` bigint uint null, `total_weight` double uint null, `percent` int uint null, `order_date` timestamp null, `id`, timestamps |
> Note: `transactions` migration lacks a `balance` column, but `TransactionInterface` writes `$_inp['balance']` (`packages/rooyesh/wallet/src/interfaces/TransactionInterface.php:141`) and hashes it (`:110`) — the column exists in the live DB but not in this migration.
### 6.2 Seeders — `database/seeders`
| File | Content |
|---|---|
| `database/seeders/DatabaseSeeder.php:14-17` | `run()` is **empty** — the only line is a commented `User::factory(10)->create()` (`:16`). **No seeders exist.** |
Factory: `database/factories/UserFactory.php` (default Laravel).
### 6.3 Final schema (tables referenced by models but NOT in migrations)
These tables exist in the production DB (legacy/Django-imported; names like `orders_orders`, `accounts_address`, `products_productvarient` indicate a Django origin) and are only known through model `$table` + `$fillable`:
| Table | Model | Source |
|---|---|---|
| `login` | `SingleCustomer` | `app/Models/SingleCustomer.php:19` |
| `orders_orders` | `SingleCustomerOrders` | `app/Models/SingleCustomerOrders.php:19` |
| `orders_orderitem` | `SingleCustomerOrderItems` | `app/Models/SingleCustomerOrderItems.php:18` |
| `orders_histroy` (sic) | `SingleCustomer\OrderActivity` | `app/Models/SingleCustomer/OrderActivity.php:17` |
| `accounts_address` | `SingleCustomerAddress` | `app/Models/SingleCustomerAddress.php:19` |
| `transactions_transaction` | `SingleCustomerTransaction` | `app/Models/SingleCustomerTransaction.php:22` |
| `products_productvarient` (sic) | `ProductVariant` | `app/Models/ProductVariant.php:20` |
| `products_productimage` | `ProductImage` | `app/Models/ProductImage.php:14` |
| `product` | `Product` | `app/Models/Product.php:20` |
| `product_brand` | `ProductBrand` | `app/Models/ProductBrand.php:17` |
| `category_collection` | `Category` | `app/Models/Category.php:32` |
| `category_collect` | `Collect` | `app/Models/Collect.php:14` |
| `image` | `Image` | `app/Models/Image.php:13` |
| `config_statics` | `ConfigStatic` | `app/Models/ConfigStatic.php:15` |
| `messages` | `Sms` | `app/Models/Sms.php:16` |
| `major_promotion` | `Major\Promotion` | `app/Models/Major/Promotion.php:21` |
| `major_promotion_item` | `Major\PromotionItem` | `:17` |
| `major_promotion_used` | `Major\PromotionUsed` | `:16` |
| `major_promotion_ignores` | `Major\PromotionIgnore` | `:17` |
| `major_favorites` | `Major\Favorite` | `:14` |
| `single_promotion_used` | `SingleCustomer\PromotionUsed` | `:15` |
| `customers_test` | `CustomerTest` | `:18` |
| `kids` | `Kids` | `:15` |
| `transaction_orders` | `TransactionOrder` | implicit |
| `branches`, `workshops`, `inventory_branches`, `inventory_transactions`, `custom_orders`, `custom_order_logs`, `comments`, `logs`, `request_logs`, `links`, `notifications`, `request_fashions` | respective models | implicit (no migration) |
**Table count: 31 migration-created + 3 wallet package + ~28 legacy/implicit ≈ 62 tables.** Only **34 have migrations**.
---
## 7. Business logic
### 7.1 Gold price
| Concern | Where | Detail |
|---|---|---|
| **Price source of truth** | `app/Models/ConfigStatic.php:15` key `gold_price` | JSON blob with `mesghal`, `geram18`, `geram24` (each with `p` price string, `h`, `l`, `updated_at`, `uaj` Jalali) |
| **18-ayar price read** | `app/Models/WebServiceGold.php:17-25` | `getPriceGold18Ayar()` = `intval(str_replace(',','', $data['geram18']['p'])) / 10` — **rial→toman conversion baked in** |
| All prices | `app/Models/WebServiceGold.php:27-31` | `getAllPrice()` returns whole decoded blob |
| **Feed #1 (ratebox.ir)** | `app/Models/WebServiceGold.php:85-116` | `Http::get('https://api.ratebox.ir/apijson.php?token='.env('WEBSERVICE_TOKEN'))`; maps index `4`→mesghal, `5`→geram18, `6`→geram24; **only writes `gold_price` if all three arrive** (`:109-112`) |
| **Feed #2 (tgju.org)** | `app/Console/Commands/GetGoldPriceForPwa.php:45-71` | `https://call3.tgju.org/ajax.json`, whitelist of 10 slugs (`geram18`, `silver_999`, `mesghal`, `price_dollar_rl`, `sekeb`, `rob`, `nim`, `sekee_down`, `rob_down`, `nim_down`); writes **both** `gold_price_pwa` and `gold_price` |
| **Feed #3 (duplicate in web route)** | `routes/web.php:32-124` | `GET /` runs the same tgju scrape inline and writes both keys |
| Feed #4 (unused) | `app/Http/Controllers/Site/SiteController.php:261` | ratebox again |
| Live price endpoints | `routes/admin.php:51`, `routes/site.php:14`, `routes/api.php:47` | `GET admin/price`, `GET site/price` (closures), `GET api/price` → `ApiMobileController@PriceFromWebService:886` |
| **No websocket / live push** | — | see §10 |
### 7.2 Gram / weight calculation & pricing formula
**Retail (SingleCustomer) price per variant — `app/Models/ProductVariant.php:156-180` (`getPriceAttribute`):**
```
G = ProductVariant::$goldPrice // injected per-request, = getPriceGold18Ayar()
E = static::$product_price_extra_percent // ConfigStatic 'product_price_extra_percent', default 7 (:45-52)
p = G + G * ((percentage_pay + E) / 100) // ساخت/اجرت (making charge %)
p = p + p * 0.07 // +7% (سود / profit margin)
t = p - G // isolate the non-metal portion
t = t + t * 0.09 // +9% VAT applied ONLY to the non-metal portion
p = G + t
p = p * grams
return floor(p / 1000) * 1000 // round DOWN to nearest 1,000 toman
```
Note `rial_pay` (stone cost) is **not** added in the current formula — only in the commented-out older variants (`:183-206`).
**With promotion discount — `app/Models/ProductVariant.php:248-296` (`getPriceWithDiscountAttribute`):**
- Looks up `self::$dis_product` (pre-loaded `PromotionProduct` array) by `product_id_id` (`:250-252`)
- If `use_only` and a `SingleCustomer\PromotionUsed` row exists for `auth()->id()` + `promotion_id` → discount forced to 0 (`:253-257`)
- `type == 1` → `discount_percent`; else → `discount_price` (a **percent**, applied as `$price -= $price*($discount_price/100)` `:293`)
- Discount percent is subtracted from the **7% margin**: `$price += $price * (0.07 - $discount_percent/100)` (`:289`) — i.e. a discount erodes margin, not metal
- Same `floor(/1000)*1000` rounding (`:294`)
**Wholesale (major `Customer`) order gram math — `app/Http/Controllers/Api/ApiMobileController.php:538-563` (`setOrder`):**
```
percent = item.percent_pay - item.discount
gram_item = rash_weight + rash_weight * (percent/100)
allGram = Σ gram_item * qty // total gold grams owed
wage = Σ rash_weight * qty // net metal weight
order.wage = allGram - wage // the "اجرت" portion, in grams
rial_input = Σ rial_pay * qty // stone/other rial charges
discount = Σ item.discount
```
Per line item written at `:617-634` with both `rash_weight` and `original_rash_weight` preserved, and `w = rash_weight * (1 + percent/100)`.
**Per-item implied gold price stored on retail orders** — `app/Http/Controllers/Controller.php:144`:
`gold_price_in_formula = (item.total_price / item.qty) / item.grams`
### 7.3 Fees / commissions / tax / spread / rounding
| Item | Value | Where |
|---|---|---|
| Making-charge extra % | `product_price_extra_percent` ConfigStatic, **default 7** | `app/Models/ProductVariant.php:45-52` |
| Profit margin | **hardcoded 7%** (`0.07`) | `ProductVariant.php:175`, `:289` |
| VAT | **hardcoded 9%** (`0.09`), applied only to `price - goldPrice` | `ProductVariant.php:178`, `:291` |
| Rounding (price) | `floor($price/1000)*1000` — **round down to 1,000 toman** | `ProductVariant.php:180`, `:294` |
| Rounding (weight display) | `round($value, 2)` — 2 decimals | `app/Models/Utility.php:10-13`; used by `Order::getW/Wage`, `OrderItem::getW/Wage`, `Major\Cash::getValue` |
| Cashback % | `percent` ConfigStatic (global) | `app/Http/Controllers/Adm/AdminController.php:994` |
| Cashback expiry (days) | `expire` ConfigStatic | `AdminController.php:995`, `:1002` |
| Birthday gift value | `birthday_credit` ConfigStatic | `app/Console/Commands/birthdayCommand.php:46` |
| Birthday gift expiry (days) | `birthday_expire` ConfigStatic | `birthdayCommand.php:47`, `:64` |
| City-level percent | `cities.percent`, `cities.percent_aux` | `database/migrations/2021_09_18_171700_city_table.php:21`, `2021_09_18_172722_cities_p_2.php:17` |
| Per-customer percent | `customers.percent` / `login.percent` | migration `2020_10_19_153228:32`; `SingleCustomer` fillable `:32` |
| **No spread** (buy vs sell price) | — | **Not implemented.** Single price per item. |
### 7.4 Wallet / balance — `packages/rooyesh/wallet`
Dual-currency wallet: **`gold` (grams, double)** and **`credit` (toman, bigint)**, one row per `(owner, owner_type)` — `packages/rooyesh/wallet/src/migrations/...:16-30`.
| Concern | Where | Detail |
|---|---|---|
| Wallet open / get | `WalletInterface::__construct` `:17-40` | find-or-create by `(owner_id, owner_type)`; validates hash on load |
| **Hash chain (tamper detection)** | `WalletInterface::generate_hash` `:51-57` | `sha256(sha256(credit) + sha256(gold) + sha256(owner) + sha256(last_tx.hash) + "yhoenszeaf")` — **secret pepper is hardcoded** |
| Wallet validate | `WalletInterface::validate` `:42-49` | throws `UnprocessableEntityHttpException('امکان شارز این کیف پول وجود ندارد')`; the package's own `WalletValidateFailed` is **commented out** (`:45`) |
| Transaction hash | `TransactionInterface::generate_hash` `:101-114` | `sha256(sha256(value)+sha256(balance)+sha256(wallet)+"yhoenszeaf"+last_tx.hash)` |
| **Balance check** | `TransactionInterface::apply_transaction` `:127-135` | if not deposit: type 1 (gold) requires `wallet.gold >= value` → `NotEnoughCashBalance`; type 2 (credit) requires `wallet.credit >= value` → `NotEnoughCreditBalance` |
| Sign convention | `:136` | `value = abs(value) * (deposit ? 1 : -1)` |
| Running balance | `:141` | `balance = wallet.gold + wallet.credit + value` — **mixes grams and toman in one number** (known modeling defect) |
| Validation rules | `:142-153` | `wallet`,`type` required; `expire_at` date; `value` `integer\|required` (**gram values are doubles — this rule is effectively bypassed because `$validator->failed()` is called instead of `fails()` at `:151`, which always returns an array/empty and never throws**) |
| Apply to wallet | `:159-165` | type 1 → `wallet.gold += value`; type 2 → `wallet.credit += value`; then re-hash wallet |
| Meta rows | `:157-158` | `TransactionMeta::create($meta)` with `order_id`, `total_weight`, `percent`, `order_date` |
| Transaction types | migration `:42-46` | `1=gold`, `2=credit`, `3=expire` |
| Owner types | migration `:27-30` | `1=user`, `2=shop` — **all call sites pass `1`** |
| Read wallet (customer) | `app/Http/Controllers/Customer/CustomerController.php:14-19` | `gold`, `credit`, + computed `gold_toman = gold * getPriceGold18Ayar()` |
| Wallet tx list (customer) | `Customer/CustomerController.php:21-30` | paginated via `addLimit` |
| Wallet tx list (admin) | `app/Http/Controllers/Adm/WalletController.php:49-59` | TableEngine |
| **Admin manual charge** | `Adm/WalletController.php:19-39` | `deposit = value > 0`; pre-check credit sufficiency; always type **2 (credit)**, `expire_at = now()+1 year` |
| Wallet config get/set | `Adm/WalletController.php:61-79` | `setConst` writes arbitrary ConfigStatic keys; `getConfig` returns `percent`+`expire`; `getConfigBirthday` returns `birthday_credit`+`birthday_expire` |
| Global percent reset | `Adm/WalletController.php:41-46` | `SingleCustomer::query()->update(['percent' => …])` — **updates every row** |
**Cashback on delivery — `app/Http/Controllers/Adm/AdminController.php:985-1013` (`chargeWalletAfterOrder`):**
```
percent = ConfigStatic['percent']; expire = ConfigStatic['expire']
value = transaction_order.total_weight * (percent / 100) // in GRAMS
new TransactionInterface(wallet, {value, order_id, expire_at: now()+expire days}, type=1 /*gold*/, deposit=true,
meta:{order_id, total_weight, percent, order_date})
transaction_order.payment = 1
```
Triggered from `Controller::afterCreateOrder` when `sts == 6` (`app/Http/Controllers/Controller.php:202-215`), which also SMSes the customer the gram amount and "6-month validity" text (`:207-214`) — note the SMS says 6 months but the actual expiry is `expire` **days** from ConfigStatic.
**Spending the wallet at checkout — `app/Http/Controllers/Customer/OrderController.php:99-136`:**
```
if order.use_wallet:
gold_value_toman = wallet.gold * order.gold_price_original
all = gold_value_toman + wallet.credit
if all <= order.price: // wallet cannot cover → drain it
withdraw all gold (type 1) and all credit (type 2)
else: // wallet covers → partial
if wallet.gold <= order.total_weight: withdraw all gold;
if gold < total_weight: withdraw (all - order.price) credit
else: withdraw exactly order.total_weight grams
// recompute what is still payable
amount = Σ |tx.value| * gold_price_original (type 1) + Σ |tx.value| (type 2)
order.payment_after_wallet = order.price - amount
```
**Refund on cancel — `Customer/OrderController.php:150-168`:** re-deposits every `Transaction` for the order back into the wallet with the same `transaction_type`.
### 7.5 Buy / sell orders
**There is no buy/sell order book, no market order, no limit order.** Two distinct *purchase* order flows exist:
| Flow | Model/table | Entry point | Status enum |
|---|---|---|---|
| **Wholesale ("major") order** — shopkeeper orders gold goods from the wholesaler, denominated in **grams** | `Order` / `orders`, `OrderItem` / `order_items`, `OrderActivity` / `order_activity` | `ApiMobileController@setOrder` `:528-758` (guard `api-customer`); admin `Adm\OrderController@store` `:32`, `Adm\MajorOrderController` | `Order::$STS` 0..6 — `app/Models/Order.php:65-72` |
| **Retail order** — web customer buys a product, denominated in **toman** | `SingleCustomerOrders` / `orders_orders`, `SingleCustomerOrderItems` / `orders_orderitem`, `SingleCustomer\OrderActivity` / `orders_histroy` | `Customer\OrderController@store` `:68-148` (guard `api-singleCustomer`); admin `Adm\AdminController@orderStsSingle` `:898` | `SingleCustomerOrders::$STS` 1..7 — `app/Models/SingleCustomerOrders.php:66-74` |
| **Custom order** (bespoke jewelry request) | `CustomOrder`, `CustomOrderLog` | `Api\CustomOrderController` | `CustomOrder::$appends['sts_str_en'/'sts_str_fa']` `:15`, `:68-76` |
### 7.6 Order & transaction status enums and transitions
**`Order::$STS` (wholesale) — `app/Models/Order.php:65-72`:**
| key | Persian | notes |
|---|---|---|
| 0 | معلق (suspended) | set when gram limit exceeded |
| 1 | ثبت سفارش (placed) | default happy path |
| 2 | تایید مدیر سیستم (sysadmin approved) | |
| 3 | درحال تامین (sourcing) | |
| 4 | درحال آماده سازی (preparing) | |
| 5 | تحویل مسئول فروش (handed to sales rep) | |
| 6 | تحویل مشتری (delivered) | **triggers wallet cashback** |
| 7 | *(کنسل شده — commented out `:71`)* | |
Supporting statics: `$STS_TAB` (with `-1 => همگی`) `:73-104`; `$STS_COLOR` `:105-113` (0 = `#FF5252`, 1-5 = `#f5cb83`, 6 = `#4CAF50`); `$STS_IMG` `:122-130`. `$CONFIRMED = [1=>تایید شده, 2=>در انتظار تایید]` `:60-63`. `$TYPES = [1=>ثبت سفارش توسط مسئول فروش, 2=>ثبت مشتری]` `:45-48`.
**Transition mechanism (wholesale):** all 7 status rows are pre-created in `order_activity` at order creation (`ApiMobileController@setOrder:652-664`), each with `proceed` 0/1. Advancing a status = flipping `proceed=1` on that row — `Adm\MajorOrderController@orderSts:121-211`. On create: `proceed = inValidOrder ? (key==0) : (key==1)` (`:662`).
**`SingleCustomerOrders::$STS` (retail) — `app/Models/SingleCustomerOrders.php:66-74`:** `1=ثبت سفارش, 2=تایید مدیر سیستم, 3=در حال تامین, 4=در حال آماده سازی, 5=خروج از انبار, 6=ارسال, 7=تحویل مشتری`. Types `$TYPES = [1=تلفنی,2=تلگرام,3=واتس آپ,4=اینستا,5=حضوری]` `:76-82`.
Same pre-create-all-rows pattern: `Controller::createSingleOrderActivity:81-92`; `afterCreateOrder` flips the requested `sts` row (`:182-186`) and fires cashback at `sts==6` (`:202`). **Note the mismatch**: `$STS[6] = 'ارسال'` (shipped) not delivered, yet cashback fires at 6.
**`Major\Cash::$TYPES` — `app/Models/Major/Cash.php:56-62`:** `9=سفارش (order), 1=آب شده (melted/bullion), 2=حواله (transfer note), 3=چک (cheque), 4=پولی (cash)`. `$STS = [-1=>بررسی نشده, 1=>ثبت سند]` `:63-66`. `$CONFIRMED = [1=>تایید شده, 0=>تایید نشده]` `:68-71`.
**Wallet transaction types:** `1=gold, 2=credit, 3=expire` (migration `:42-46`).
**`SingleCustomerTransaction.sts`** is a free-text Persian string (e.g. `'پرداخت نشده'` — `app/Models/SingleCustomerTransaction.php:43`), **not an enum**.
### 7.7 KYC levels
**No KYC-level system exists.** What exists is a **two-step manual verification flag** on wholesale customers:
| Field | Where |
|---|---|
| `customers.sts_verify_step_one`, `customers.sts_verify_step_two` | `app/Models/Customer.php:18` fillable |
| Accessors `sts_verify_step_one_str`, `sts_verify_step_two_str` + lookups `GetStsVerifyStepOne`, `GetStsVerifyStepTwo` | `app/DB/Selector.php:101-127` |
| Document images: `business_license_image_id` (جواز کسب), `shop_image_id` (سر در فروشگاه), `ncode_image_id` (کارت ملی) | `app/Models/Customer.php:18`; relations `:170-181` |
| Admin approval | `Adm\MajorCustomerController@verifyRegister:97-144`, `@indexVerify:157`, `@archive:175` |
| Activation toggles | `Adm\AdminController@customerToggleSts:426`, `@customerToggleActive:432` |
| National code | `customers.ncode` (migration `2020_10_01_120430:22`) |
No tiered limits are derived from verification status.
### 7.8 Limits (min / max / daily)
**No daily limits. No min/max order amount. No withdrawal limit.** The only limit is a **per-customer gram ceiling** on wholesale orders:
| Concern | Where |
|---|---|
| Ceiling field | `customers.gram_level` double default 0 — `database/migrations/2020_10_19_153228_customer_add.php:31`; fillable `app/Models/Customer.php:18` |
| Running usage | `customers.remain` double default 0 — `database/migrations/2020_10_01_120430_customers_table.php:26` |
| **Check** | `app/Http/Controllers/Api/ApiMobileController.php:566-567`: `$inValidOrder = ($allGram + $remain) > $gramLevel` |
| Effect | order `sts` forced to **0 (معلق/suspended)** instead of 1 (`:577`, `:592`); activity note "تعلیق سفارش به دلیل عبور از سقف مجاز گرمی" (`:661`); a `Notify` row explains the suspension (`:669-677`) |
| Note | The `$customer->increment('remain', $allGram)` on success is **commented out** (`:604-607`) — `remain` is maintained elsewhere (cash ledger) |
| Product-level gram bounds | `product.min_gram`, `product.max_gram` — `app/Models/Product.php:22` (display/filter only) |
| Pagination limit | `Controller::addLimit` — default 10, **hard cap 50** — `app/Http/Controllers/Controller.php:31-36` |
| Rate limit | 500 req/min on `api` — `app/Providers/RouteServiceProvider.php:77` |
### 7.9 OTP / SMS
| Concern | Where | Detail |
|---|---|---|
| **SMS gateway (active)** | `app/Models/Sms.php:74-84` (`Send`), `:101-111` (`SendSms`) | `POST https://smspanel.trez.ir/SendMessageWithCode.ashx`, form fields `PhoneNumber=9998882797`, `Username=hajiabolfath`, **`Password=Aa@09195540836` (hardcoded)**, `Mobile`, `Message`; 20s timeout |
| SMS gateway (dead) | `Sms.php:63-73`, `:91-99` | `0098sms.com/sendsmslink.aspx` with `USERNAME=zsms6916` — commented out |
| Every send is logged | `Sms::set` `:125-134` → `messages` table | `customer_id`, `mobile`, `massage`(sic), `token`, `type`, `user_id` |
| **OTP — wholesale `Customer`** | `Api\ApiMobileController@userAuth:40-50` | `rand(1000,9999)` → stored in `customers.send_password` (**plain text**, no expiry, no attempt counter); SMS body includes `<#> code is …` + Android SMS Retriever hash `aMo6iFyUQaY` |
| OTP verify (wholesale) | `ApiMobileController@userVerfiy:52-81` | compares `Utility::faTOen($request->password) != $customer->send_password` → `abort(422)`; on success issues `api_token = Str::random(60)` **only if null** (`:59-61`) — token never rotates |
| **OTP — retail `SingleCustomer`** | `Api\SingleCustomerController@login:58-76` | `rand(1000,9999)` → `login.token_sms` (plain, no expiry); returns `{id}`; SMS "کد تایید ورود" |
| OTP verify (retail) | `Api\SingleCustomerController@verfiy:42-56` | compares `token_sms`; sets `is_active = true`; issues `api_token = Str::random(60)` if null |
| Legacy password login | `ApiMobileController@login:83-110` (`Hash::check`); `Adm\AdminController@login:60-78` (staff) | |
| Batch SMS | `Adm\SmsController@batch:23-76` | validates `payload`,`type` numeric,`mobiles` array,`token`; `@replaceName:91-111` does `%%NAME%%` substitution |
| Ad-hoc SMS | `Adm\SmsController@sendSmsToOther:150` | |
| **Mass-SMS to ALL customers via GET** | `routes/api.php:126-180` (`GET api/send/all`), `routes/web.php:158-186` (`GET /test2`) | unauthenticated closures that loop `Customer::get()` and SMS everyone |
| Hardcoded admin phones (order alerts) | `ApiMobileController:601-604` (`09128081392`, `09124871664`, `09124406015`, `09120363240`); `Customer/OrderController:139-144` (`09128081392`, `09124406015` ×2, `09363366849`) | |
### 7.10 Notifications (push)
| Concern | Where |
|---|---|
| FCM config | `config/fcm.php:4-9` — `FCM_PROTOCOL` (def `http`), `FCM_SERVER_KEY`, `FCM_SENDER_ID` |
| In-app notification store | `Notify` model / `notify` table — `app/Models/Notify.php:14-33` |
| Single push | `Notify::store($token,$title,$body)` `:57-86` |
| Group push | `Notify::sendToGroup($tokens,$title,$body)` `:88+` |
| Second impl | `Notification::store()` `app/Models/Notification.php:33` |
| Device token storage | `customers.token_mobile`, `users.token_mobile` (`Customer.php:18`, `User.php:29`); updated via `POST api/tokenmobile` → `ApiMobileController@updateTokenMobile:477` |
| Admin send | `Adm\NotifyController@store:27`, `@customer:67` |
| Read/unread | `Notify.read` bool; `Adm\NotifyController@read:95`, `@getUnreadNotify:86`; customer `PUT api/notify/read/{id}` → `@readNotify:784` |
### 7.11 Payment gateway + callbacks
| Concern | Where | Detail |
|---|---|---|
| **Gateway: PayPing (init)** | `app/Http/Controllers/Customer/TransactionController.php:11-27` | `POST https://api.payping.ir/v1/pay` with `clientRefId`, `amount`, `returnUrl = https://henzagold.com/oapi/site/transaction/verify`. **No `Authorization` header is sent** (the `withHeaders` call is chained *after* `post()` at `:100`, so it is a no-op). |
| Gateway: PayPing (verify, dead code) | `app/Models/SingleCustomerTransaction.php:84-100` | `POST https://api.payping.ir/v2/pay/verify` with **hardcoded bearer token** `:86`; hardcoded `amount=1000`, `payerIdentity='09134139430'`, `payerName='Azizan'`; `returnUrl = url('/bank-back')` |
| Gateway: Zarinpal (SOAP) | `SingleCustomerTransaction.php:49-82` | entirely commented out |
| **Callback route** | — | **The `returnUrl` `site/transaction/verify` is NOT registered in any route file.** `routes/site.php` has no `transaction/*` route. `/bank-back` is also not registered. **The payment callback is missing / dead.** |
| Transaction record | `transactions_transaction` via `SingleCustomerTransaction` | `gateway` = `'paypal'` (sic, hardcoded `:44`), `sts` free-text Persian |
| Route | `POST cu/transaction/gateway` — `routes/customer.php:36` | |
### 7.12 File uploads
| Concern | Where |
|---|---|
| Primary upload | `Adm\FileController@storeWithoutData:144-192` — `POST admin/file/create`, `POST api/file/create` |
| Filename | `md5(time() . original_name) . '.' . ext` (`:155-156`) |
| Destination | `public_path('storage')` — flat, **no date directory** (the `createDir()` path logic is commented out `:150`, `:162-179`) |
| Record | `App\Models\Image::create($data)` with `original_name`, `ext`, `image` (`:186-190`) |
| **No validation** | no mime/size/extension check, no auth on `admin/file/create` |
| WebP conversion / watermark | **commented out** (`:162-172`) |
| Body/caption update | `@updateBody:268` — `PUT admin/file/update/{file_id}` |
| Full CRUD (legacy) | `@store:58`, `@show:200`, `@update:215`, `@destroy:255` |
| Product image resize | `Adm\ProductController@createImage:22-29` |
| URL base | `Utility::baseURLStorage()` = `https://henzagold.com/oapi/storage/` — `app/Models/Utility.php:30-32`; `Image::getFullAttribute:35`, `getImageAttribute:58` |
| APK download | `Api\SiteController@downloadApk` — `env('APK_PATH', storage_path('app/public/henza.apk'))`, `env('APK_VERSION',0)` — `app/Http/Controllers/Api/SiteController.php:494-495` |
### 7.13 Settings / config table
**`config_statics` (`ConfigStatic`)** is the universal key/value settings table — `app/Models/ConfigStatic.php:10-33`. It doubles as a **lookup/enum table** (`users.ut` is an FK to it — `Adm\UserController:26` validates `ut` `exists:config_statics,id`; `User::user_type()` `:51`).
| Known key | Purpose | Evidence |
|---|---|---|
| `gold_price` | live price JSON (geram18/geram24/mesghal) | `WebServiceGold.php:19`,`:28`,`:110` |
| `gold_price_pwa` | PWA price JSON (10 instruments) | `GetGoldPriceForPwa.php:70`; `routes/web.php:60` |
| `product_price_extra_percent` | making-charge extra %, default 7 | `ProductVariant.php:49` |
| `percent` | wallet cashback % of order weight | `AdminController.php:994`; `Controller.php:203` |
| `expire` | cashback validity, in **days** | `AdminController.php:995`,`:1002` |
| `birthday_credit` | birthday gift amount (toman) | `birthdayCommand.php:46` |
| `birthday_expire` | birthday gift validity (days) | `birthdayCommand.php:47` |
| `brand` | product brand list (legacy, superseded by `product_brand`) | `ApiMobileController.php:75` (commented) |
| *(arbitrary)* | `Adm\WalletController@setConst:61-70` writes any key from the request body | |
Accessors: `ConfigStatic::getValue($key)` — returns scalar if exactly 1 row, collection if >1, **throws `NotFoundHttpException('Const not found')` if 0** (`:25-33`). CRUD via `Adm\ConstController` (`admin/const/*`), read via `GET site/const/{id}`, full list via `GET admin/config/statics` → `AdminController@getConfigStatics:752`.
### 7.14 What does NOT exist (explicit negatives for parity work)
| Expected in a gold-banking backend | Status in `henza_server` |
|---|---|
| Buy / sell gold orders (market/limit) | **Absent** — only product purchase orders |
| Bid/ask spread | **Absent** — one price per item |
| Deposit (cash in) | **Absent** — only admin manual wallet charge (`Adm\WalletController@charge`) |
| Withdraw (cash out / physical gold delivery) | **Absent** |
| Peer-to-peer transfer | **Absent** (the "حواله" in `Cash::$TYPES[2]` and `config/havaleh.php` is a static list of 9 broker names, not a transfer feature) |
| KYC levels / tiers | **Absent** — only 2 manual verification booleans + 3 document images |
| Daily / monthly limits | **Absent** — only a per-customer total gram ceiling |
| Payment callback / verify endpoint | **Route missing** (§7.11) |
| Idempotency keys on money ops | **Absent** |
| DB transactions around wallet ops | **Absent** — `apply_transaction` does 4 unwrapped writes (`TransactionInterface.php:154-164`) |
| Audit/event sourcing | Partial — wallet hash chain + `order_activity` + `RequestLog` |
---
## 8. Jobs / Queues / Cron
### 8.1 Jobs — `app/Jobs` (3)
| Job | Constructor args | `handle()` | Evidence |
|---|---|---|---|
| `SendSmsJob` | `$mobile, $message, $customer_id, $user_id` | `Sms::SendSms($mobile,$message,$customer_id,null,null,$user_id)` | `app/Jobs/SendSmsJob.php:25-41` |
| `SendSmsAfterCreate` | `$mobile, $message` | `Sms::Send($mobile,$message)` | `app/Jobs/SendSmsAfterCreate.php:23-37` |
| `PromotionAfterCreate` | `$customer_id` | **body fully commented out; `return;`** — "new customer 1% promo for 5 days" disabled | `app/Jobs/PromotionAfterCreate.php:33-53` |
All three implement `ShouldQueue` with `Dispatchable, InteractsWithQueue, Queueable, SerializesModels`.
**Queue driver:** `config/queue.php` `'default' => env('QUEUE_CONNECTION', 'sync')` — defaults to **sync** (jobs run inline) unless env overrides. `failed_jobs` table exists (migration #3).
### 8.2 Console commands — `app/Console/Commands` (5)
| Signature | Class | Purpose |
|---|---|---|
| `price:get` | `GetGoldPrice` `:15` | `WebServiceGold::getAllPriceFromWebService()` (ratebox.ir) — `:39-43` |
| `pwa:price` | `GetGoldPriceForPwa` `:18` | tgju.org scrape → writes `gold_price_pwa` **and** `gold_price`; then calls Tahesab `DoListMoshtari [1,510]` at `https://31.7.68.147:3030/` with **hardcoded bearer token** `:76-81` |
| `transaction:expire` | `ExpireCommand` `:18` | loops **all** `SingleCustomer`; sums expired-vs-spent gold and credit; posts negative (withdraw) `TransactionInterface` entries for the surplus — `:42-80` |
| `birthday:sms` | `birthdayCommand` `:20` | finds customers whose `birth_day` day+month = today; SMSes a gift message; deposits `birthday_credit` as **type 2 (credit)** with `expire_at = now() + birthday_expire days` — `:44-68` |
| `wallet:sms` | `SmsWalletCommand` `:17` | finds wallets with `sms_at` exactly 3 months ago; if `gold>0 \|\| credit>0` SMSes the owner the literal string `'هنزا'`; updates `sms_at`/`sent` — `:41-57` |
### 8.3 Schedule — `app/Console/Kernel.php`
| Item | Evidence |
|---|---|
| `$commands` registry | `birthdayCommand`, `ExpireCommand`, `GetGoldPriceForPwa` — `app/Console/Kernel.php:18-22` (note: `GetGoldPrice` and `SmsWalletCommand` are **not** listed, but `$this->load(__DIR__.'/Commands')` at `:42` auto-discovers them) |
| **`schedule()`** | **EMPTY** — the only line is a commented `$schedule->command('inspire')->hourly();` — `app/Console/Kernel.php:30-33` |
**⇒ There is NO Laravel cron schedule.** Price refresh, expiry sweeps and birthday SMS must be driven by external OS cron calling `artisan pwa:price` / `transaction:expire` / `birthday:sms`, or by hitting `GET /` (`routes/web.php:32`) which performs the price scrape inline.
`routes/console.php:17-19` defines only the stock `inspire` closure command.
---
## 9. Permissions / roles
**No Spatie, no Gates, no Policies.** `AuthServiceProvider::$policies` is empty (`app/Providers/AuthServiceProvider.php:15-17`); `boot()` only calls `registerPolicies()` (`:24-29`). No `Gate::define` anywhere. The `can` middleware alias is registered (`Kernel.php:60`) but **never used on any route**.
### 9.1 Model
| Piece | Detail | Evidence |
|---|---|---|
| Role = `users.ut` | integer "user type" on `User`; FK into `config_statics` | migration `2021_09_18_171700_city_table.php:28`; `User.php:27`; `User::user_type()` `:51` |
| Permission = `permission` table | composite PK `(rid, ut)` — `rid` is a **free-text permission string**, `ut` is the role id | migration `2021_09_18_115342_permissions_table.php:16-19`; `app/Models/Permission.php:14-18` |
| Validation of `ut` | `'ut' => 'required\|numeric\|exists:config_statics,id'` | `Adm\UserController.php:26`, `:64` |
### 9.2 Where permissions are read
| Operation | Where |
|---|---|
| **Returned at login** | `Adm\AdminController@login:74` — `$per = Permission::whereUt($user->ut)->get();` attached to the login response. **The server never enforces it** — enforcement is entirely client-side in the admin SPA. |
| Set (replace all for a role) | `Adm\PermissionController@create:12-40` — diffs requested `roles[]` against existing, creates missing, deletes surplus |
| Set (additive) | `@store:42-57` — `firstOrCreate` per role |
| Delete one | `@destroy:59-65` — `DELETE admin/roles/{ut}/{rid}` |
| Check one | `@has:67-74` — `GET admin/roles/{ut}/{rid}` → boolean |
| My permissions | `@get:76-81` — `Permission::where('ut', Auth::user()->ut)` |
| Permissions of a role | `@getByUt:83-88` — `GET admin/roles/{id}` |
| Attached to user detail | `Adm\UserController@show:96` — `Permission::where('ut',$user->ut)->pluck('rid')` |
### 9.3 Permission names found
**No permission (`rid`) string literals exist anywhere in the codebase.** `rid` values are created purely from client-supplied `roles[]` arrays (`PermissionController@create:21`, `@store:51`) and stored as-is. The catalogue of permission names lives only in the admin front-end and in the DB — it is **not derivable from `henza_server`**. Likewise there is no seeded list of `ut` (role) values; they are rows in `config_statics`.
### 9.4 Actual enforcement in practice
| Surface | Enforcement |
|---|---|
| `routes/admin.php` (215 routes) | **NONE** — the `auth:api` wrapper is commented out (`:12`, `:264`). Fully public. |
| `routes/api.php` public block | none |
| `routes/api.php:64-117` | `auth:api-customer` only (authentication, not authorization) |
| `routes/customer.php:10-46` | `auth:api-singleCustomer` only |
| `routes/site.php`, `routes/web.php` | none (except `version` on one route) |
| Ownership checks | ad-hoc: `Controller::checkCustomer($auth_id,$store_id)` throws `AccessDeniedHttpException` (`app/Http/Controllers/Controller.php:232-236`); scopes `Order::scopeMe:161`, `Notify::scopeMe:50`, `Major\Cash::scopeMe:74`; `where('customer_id', auth()->id())` in `Customer\CustomerController:135` etc. |
---
## 10. Broadcasting / sockets
| Item | Detail | Evidence |
|---|---|---|
| Channels file | Contains exactly **one** channel: `App.Models.User.{id}`, authorized by `(int)$user->id === (int)$id` (Laravel's stock private-notification channel) | `routes/channels.php:28-30` |
| Provider | `BroadcastServiceProvider` exists | `app/Providers/BroadcastServiceProvider.php` |
| **Driver default** | `'default' => env('BROADCAST_DRIVER', 'null')` — **null (disabled) unless env overrides** | `config/broadcasting.php:18` |
| Available connections | `pusher` (`:34`, keys `PUSHER_APP_KEY`/`PUSHER_APP_SECRET`/`PUSHER_APP_ID`/`PUSHER_APP_CLUSTER`), `redis` (`:45`), `log` (`:50`), `null` (`:54`) | `config/broadcasting.php:31-56` |
| **Events broadcast** | **NONE** — no class implements `ShouldBroadcast`; `app/Events/` does not exist; `EventServiceProvider::$listen` is stock | `app/Providers/EventServiceProvider.php` |
| **Live price over socket** | **NOT IMPLEMENTED.** Price is delivered by polling `GET api/price` / `GET site/price` / `GET admin/price`, all of which read `ConfigStatic['gold_price']` | `routes/api.php:47`, `routes/site.php:14-16`, `routes/admin.php:51-53` |
| Real-time client hint | `package.json` may include laravel-echo but no server-side publisher exists | — |
**⇒ There is no real-time/websocket layer. Live price = HTTP polling of a DB-cached JSON blob refreshed by a cron-driven command.**
---
## 11. Env / config keys
### 11.1 Custom (non-stock) config files
| File | Content |
|---|---|
| `config/havaleh.php:5-16` | Flat array of **9 hardcoded broker name+phone strings** (e.g. `'مسعود آتشی \| 33977786-021'`). Returned verbatim by `GET admin/havale/list` (`Adm\AdminController@getHavaleList:1018`) and attached to the mobile login payload as `$customer->to` (`ApiMobileController:66`, `:96`). |
| `config/version.php:2-4` | `['version' => '2.2.0']` — compared against the `version` request header by `VersionMiddleware` (`app/Http/Middleware/VersionMiddleware.php:21-27`). |
| `config/validation.php:2-9` | Single route-keyed rule set: `'admin/product/create@post' => ['cats'=>'required\|array','title'=>'required','indicator_id'=>'required','mobile_pic'=>'required']`. **Never read by any code** (no `config('validation')` usage) — dead config. |
| `config/corcel.php` | Corcel/WordPress bridge connection. |
| `config/fcm.php:4-9` | `FCM_PROTOCOL` (def `http`), `FCM_SERVER_KEY`, `FCM_SENDER_ID`. |
| `config/pdf.php`, `config/dompdf.php` | PDF rendering (invoice generation). |
### 11.2 `env()` calls inside `app/` and `routes/` (outside `config/`)
| Key | Default | Where |
|---|---|---|
| `WEBSERVICE_TOKEN` | — | `app/Models/WebServiceGold.php:85`; `app/Http/Controllers/Site/SiteController.php:261`; `app/Console/Commands/GetGoldPriceForPwa.php:170` (commented) |
| `APK_VERSION` | `0` | `app/Http/Controllers/Api/SiteController.php:494` |
| `APK_PATH` | `storage_path('app/public/henza.apk')` | `app/Http/Controllers/Api/SiteController.php:495` |
| `APP_NAME` | — | `app/DB/Te/print.blade.php:5` |
| `BASE_PATH_STORAGE` | `''` | `app/Models/Image.php:60` (**commented out**) |
> ⚠️ These are **runtime `env()` calls outside config files** — they return `null` when `php artisan config:cache` is used.
### 11.3 Stock env keys consumed via `config/*`
`APP_NAME`, `APP_ENV`, `APP_KEY`, `APP_DEBUG`, `APP_URL`, `LOG_CHANNEL`;
`DB_CONNECTION` (**default `'localhost'`** — unusual, `config/database.php:18`), `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`, `DB_SOCKET`, `DATABASE_URL`, `DB_FOREIGN_KEYS`, `MYSQL_ATTR_SSL_CA`;
`REDIS_CLIENT`, `REDIS_CLUSTER`, `REDIS_PREFIX`, `REDIS_URL`, `REDIS_HOST`, `REDIS_PASSWORD`, `REDIS_PORT`, `REDIS_DB`, `REDIS_CACHE_DB`;
`BROADCAST_DRIVER` (def `null`), `PUSHER_APP_KEY`, `PUSHER_APP_SECRET`, `PUSHER_APP_ID`, `PUSHER_APP_CLUSTER`;
`CACHE_DRIVER`, `CACHE_PREFIX`, `MEMCACHED_*`, `DYNAMODB_CACHE_TABLE`, `DYNAMODB_ENDPOINT`;
`QUEUE_CONNECTION`, `SESSION_DRIVER`, `SESSION_LIFETIME`;
`FILESYSTEM_DRIVER`, `FILESYSTEM_CLOUD`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`, `AWS_BUCKET`;
`MAIL_*`, `MAILGUN_DOMAIN`, `MAILGUN_SECRET`, `MAILGUN_ENDPOINT`, `POSTMARK_TOKEN`;
`FCM_PROTOCOL`, `FCM_SERVER_KEY`, `FCM_SENDER_ID`.
### 11.4 Hardcoded secrets / endpoints (parity + security relevant)
| Secret / endpoint | Location |
|---|---|
| SMS panel user + **password** `hajiabolfath` / `Aa@09195540836` | `app/Models/Sms.php:80-81`, `:107-108` |
| PayPing bearer `413471e0…d7b1` | `app/Models/SingleCustomerTransaction.php:86` |
| Tahesab bearer `S3F1P8Q4…U8` + host `31.7.68.147:3030` | `app/Console/Commands/GetGoldPriceForPwa.php:78-80` |
| Wallet hash pepper `"yhoenszeaf"` | `packages/rooyesh/wallet/src/interfaces/WalletInterface.php:56`; `TransactionInterface.php:112` |
| Storage base URL `https://henzagold.com/oapi/storage/` | `app/Models/Utility.php:31` |
| Payment return URL `https://henzagold.com/oapi/site/transaction/verify` | `app/Http/Controllers/Customer/TransactionController.php:99` |
| Admin alert phone numbers | `app/Http/Controllers/Api/ApiMobileController.php:601-604`; `app/Http/Controllers/Customer/OrderController.php:139-144` |
| Customer-facing URLs (`henzagold.com/customer/my-orders`, `/dashboard`, `/app`, `/download/henza.apk`) | `app/Http/Controllers/Controller.php:199`, `:213`; `routes/web.php:128`, `:132` |
---
## 12. Validation
**There are ZERO FormRequest classes** — `app/Http/Requests/` does not exist. All validation is inline, using either `$this->validate($request, [...])` (ValidatesRequests trait) or `Validator::make($request->all(), [...])`. `config/validation.php` defines one route-keyed rule set that **nothing reads**.
Distribution (rule-block counts per controller):
| Controller | blocks |
|---|---|
| `Adm/AdminController` | 17 |
| `Api/ApiMobileController` | 5 |
| `Api/SingleCustomerController` | 4 |
| `Adm/MajorOrderController`, `Adm/UserController`, `Api/CustomOrderController`, `Api/SiteController` | 3 each |
| `Adm/BranchController`, `Adm/OrderController`, `Adm/PermissionController`, `Adm/ProductController`, `Adm/SmsController`, `Adm/WorkshopController`, `Major/CustomerController` | 2 each |
| `Adm/CitiesController`, `Adm/CommentController`, `Adm/MajorCustomerController`, `Adm/SingleCustomerController`, `Customer/CommentController`, `Customer/FavController`, `Customer/OrderController`, `WebApp/AuthController` | 1 each |
### Main rule sets
| Endpoint | Rules | Evidence |
|---|---|---|
| Admin login | `phone: required`, `password: required` | `Adm/AdminController.php:62` |
| Update customer | `id: required\|exists:customers,id`, `fname: required`, `lname: required` | `Adm/AdminController.php:264-267` |
| Update customer (mobile uniqueness) | `mobile: unique:customers,mobile,{id}` | `Adm/AdminController.php:303-304` |
| **Create customer (with reagent)** | `mobile: required\|unique:customers`, `reagent: required` | `Adm/AdminController.php:321-323` |
| Create customer (reagent path) | `mobile: required\|unique:customers`, `percent: required` | `Adm/AdminController.php:352-354` |
| Confirm cash doc | `id: required` | `Adm/AdminController.php:455-456` |
| Set gram for fin-doc *(commented)* | `id: required`, `gram: required` | `Adm/AdminController.php:480-482` |
| Read SMS *(commented)* | `FROM/TO/TEXT: required` | `Adm/AdminController.php:505-508` |
| Confirm order | `cId: required` (customer), `oId: required` (order) | `Adm/AdminController.php:517-519` |
| Set fin-doc *(commented)* | `customer_id/price/value: required` | `Adm/AdminController.php:583-586` |
| Add product | `cats: required\|array`, `title: required` | `Adm/AdminController.php:600-602` |
| Get variant(s) | `id: required` | `Adm/AdminController.php:736`, `:745` |
| Add category | `title: required` | `Adm/AdminController.php:794-795` |
| **Change percent (bulk)** | `percentage_pay: required` | `Adm/AdminController.php:845-846` |
| Report order | `id: required` | `Adm/AdminController.php:900-901` |
| Branch store/update | `user_id: required` | `Adm/BranchController.php:48-49`, `:83-84` |
| **Cities store** | `title: required`, `parent: exists:cities,id`, `level: numeric\|required`, `percent: numeric` | `Adm/CitiesController.php:15-19` |
| Comment status | `comment_id: required`, `sts: required` | `Adm/CommentController.php:123-124` |
| Verify major customer | `customer_id: required` | `Adm/MajorCustomerController.php:98-99` |
| Major order sts | `id: required` | `Adm/MajorOrderController.php:123-124` |
| **Major order item edit** | `id: required`, `w: required`, `order_id: required` | `Adm/MajorOrderController.php:214-217` |
| Delete major order | `id: required` | `Adm/MajorOrderController.php:275-276` |
| Admin order store | `order_items: required` | `Adm/OrderController.php:35-36` |
| Admin order update | `order_items: required`, `shipping_address: required` | `Adm/OrderController.php:66-67` |
| **Permissions (create/store)** | `ut: required`, `roles: required\|array` | `Adm/PermissionController.php:13-15`, `:45-47` |
| Product store | `cats: required\|array`, `title: required` *(`product_code: required\|unique:product,product_code` is commented out `:45`)* | `Adm/ProductController.php:42-45` |
| Product update | *(rule array empty — `product_code` unique rule commented out)* | `Adm/ProductController.php:114-115` |
| Single customer store | `fname: required`, `lname: required`, `phone: required\|unique:login` | `Adm/SingleCustomerController.php:20` |
| **SMS batch** | `payload: required`, `type: required\|numeric`, `mobiles: array`, `token: required` | `Adm/SmsController.php:25-29` |
| SMS to other | `payload: required`, `mobile: required` | `Adm/SmsController.php:79-81` |
| **User store** | `fname: required`, `lname: required`, `mobile: required\|unique:users`, `ut: required\|numeric\|exists:config_statics,id`, `city_id: exists:cities,id`, `password: required\|min:6` | `Adm/UserController.php:22-28` |
| **User update** | `mobile: unique:users,mobile,{id}`, `ut: numeric\|exists:config_statics,id`, `city_id: exists:cities,id`; if password present: `password: required\|min:6\|confirmed` | `Adm/UserController.php:62-72` |
| **OTP request (wholesale)** | `username: required` | `Api/ApiMobileController.php:42` |
| **OTP verify (wholesale)** | `username: required`, `password: required` | `Api/ApiMobileController.php:54` |
| Legacy login (wholesale) | `username: required`, `password: required` | `Api/ApiMobileController.php:85` |
| **Retail register** | `fname: required`, `lname: required`, `phone: required\|unique:login` | `Api/SingleCustomerController.php:30` |
| **Retail OTP verify** | `token: required`, `id: required` | `Api/SingleCustomerController.php:43` |
| **Retail login (OTP request)** | `phone: required` | `Api/SingleCustomerController.php:59` |
| **Retail order store** | `order_items: required`, `shipping_address: required` | `Customer/OrderController.php:73-75` (returns 422 with `$validator->errors()`) |
### Notable validation gaps (parity-relevant)
| Gap | Evidence |
|---|---|
| **`setOrder` (wholesale order creation) has NO validation at all** — `$request->items` is consumed raw | `Api/ApiMobileController.php:528-758` |
| **Wallet charge has NO validation** — `$data['value']`, `$data['customer_id']` taken raw | `Adm/WalletController.php:19-39` |
| **`setConst` writes arbitrary ConfigStatic keys with no whitelist/validation** | `Adm/WalletController.php:61-70` |
| **File upload has no mime/size/extension validation** | `Adm/FileController.php:144-192` |
| **Payment gateway `gateway()` has no validation** — `amount`, `customer_id` raw | `Customer/TransactionController.php:11-27` |
| Wallet package's own validator is **never enforced** — calls `$validator->failed()` (returns array) instead of `fails()` (returns bool) | `packages/rooyesh/wallet/src/interfaces/TransactionInterface.php:150-153` |
| `Product::update` validation array is empty | `Adm/ProductController.php:114-116` |
| No `numeric`/`min`/`max` on any money or weight field anywhere | — |