Files
s.esmaeili ea86636027 Initial commit: Hedena Gold monorepo.
First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
2026-10-15 17:19:10 +03:30

1314 lines
121 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | — |