# Technic Sale — ERP / Ombor boshqaruv tizimi

Yii2 Advanced + MySQL 8 asosidagi savdo va ombor boshqaruv tizimi.

---

## Tez boshlash

```bash
php yii migrate/up          # jadvallar + RBAC + boshlang'ich ma'lumotlar
php yii rbac/init           # rol/ruxsatlarni sinxronlash (idempotent)
```

Panel: `http://technic-sale.loc/controlp`

| Rol | Login | Parol |
|-----|-------|-------|
| Admin | `admin` | `admin` |
| Sotuvchi | `seller` | `seller123` |

> Production ga chiqishdan oldin ikkala parolni ham almashtiring.

---

## Arxitektura

Kod uch qatlamga ajratilgan. **Biznes-mantiq controller da emas, servislarda yashaydi** —
shu sababli kelajakdagi REST API va Flutter ilova aynan shu servislarni qayta ishlatadi.

```
common/
  db/          ErpActiveRecord, ActiveQuery, SoftDeleteTrait   ← umumiy AR asosi
  enums/       PaymentType, SaleStatus, StockMovementType, SmsStatus
  models/      Product, Customer, Sale, ...                    ← ma'lumot + validatsiya
  models/query/  ProductQuery, CustomerQuery, SaleQuery        ← qayta ishlatiladigan scope lar
  models/search/ *Search                                       ← grid filtrlari
  services/    StockService, SaleService, DebtService, ...      ← BIZNES MANTIQ
  services/sms/  SmsProviderInterface + provayderlar
  exceptions/  BusinessRuleException, InsufficientStockException
  rbac/        Rbac (konstantalar), RbacManager (daraxt)

backend/modules/erp/
  controllers/ ErpController (bazaviy) + har bir bo'lim uchun controller
  views/       AdminLTE3 + Bootstrap 4 + kartik GridView

console/
  controllers/ RbacController, SmsController (cron)
  migrations/  barcha jadvallar, FK, indekslar
```

### Qatlamlar orasidagi qoida

| Qatlam | Nima qiladi | Nima qilmaydi |
|--------|-------------|---------------|
| Controller | HTTP, RBAC, flash | Hech qachon SQL yozmaydi, hisob-kitob qilmaydi |
| Service | Tranzaksiya, biznes qoidalari | HTML bilmaydi |
| Model | Validatsiya, relation, scope | Ko'p-jadvalli tranzaksiya ochmaydi |

---

## Ma'lumotlar yaxlitligi — muhim qoidalar

Uchta ustun **denormalizatsiya qilingan** (tezlik uchun) va ularni qo'lda o'zgartirish taqiqlanadi:

| Ustun | Yagona egasi |
|-------|--------------|
| `product.current_quantity` | `StockService` |
| `customer.current_debt` | `DebtService` |
| `sale.debt_amount` / `sale.status` | `SaleService`, `DebtService` |

Bu ustunlar model `rules()` ida **safe emas**, ya'ni `load()` orqali tashqaridan yozib bo'lmaydi.

### Poyga holatidan himoya (race condition)

* **Ombor.** `StockService` har bir o'zgarishdan avval mahsulot satrini
  `SELECT ... FOR UPDATE` bilan qulflaydi. Ikki sotuvchi bir vaqtda oxirgi mahsulotni
  sotmoqchi bo'lsa, ikkinchisi kutadi va yangilangan qoldiqni ko'radi → manfiy qoldiq bo'lmaydi.
* **Qarz.** `DebtService` qarzni `UPDATE ... WHERE current_debt >= :amount` shartli
  so'rovi bilan kamaytiradi. Ikki parallel to'lov qarzni manfiyga tushira olmaydi.
* **Hujjat raqami.** `DocumentNumberService` `MAX(...) FOR UPDATE` (locking read) ishlatadi —
  oddiy `MAX()` snapshot o'qish bo'lgani uchun ikki tranzaksiya bir xil raqam olardi.

`StockService`, `DebtService` va `DocumentNumberService` tranzaksiyadan tashqarida
chaqirilsa **ataylab exception tashlaydi** — bu xatoni jimgina o'tkazib yubormaslik uchun.

### Audit izi

Har bir ombor o'zgarishi `stock_movement` jadvaliga tushadi (`type`, `quantity`,
`balance_after`, `ref_type`, `ref_id`). Shu sababli `current_quantity` ni istalgan
vaqtga qayta hisoblash va tekshirish mumkin. Har bir yozuvda `created_by`,
`updated_by`, `created_at`, `updated_at` bor.

---

## RBAC

RBAC to'liq **DB da** (`yii\rbac\DbManager`, keshlangan). Daraxt kodda deklarativ
tavsiflanadi va DB ga sinxronlanadi:

```php
common/rbac/Rbac.php         // barcha rol/ruxsat nomlari — "sehrli satr" yozmang
common/rbac/RbacManager.php  // daraxt ta'rifi + idempotent init()
```

```bash
php yii rbac/init            # sinxronlash (mavjud tayinlovlarga tegmaydi)
php yii rbac/show            # joriy daraxtni ko'rish
php yii rbac/assign 5 seller # foydalanuvchiga rol berish
```

Yangi ruxsat qo'shish: `Rbac.php` ga konstanta → `RbacManager::permissions()` ga tavsif
→ kerak bo'lsa `sellerPermissions()` ga qo'shing → `php yii rbac/init`.

### Rollar

* **admin** — barcha 30 ruxsat.
* **seller** — 9 ruxsat: mijoz qo'shish/ko'rish, sotuv qilish, o'z sotuvlarini ko'rish,
  ombor qoldig'ini ko'rish, o'z pul qoldig'ini ko'rish. Mahsulot qo'sha/tahrirlay/o'chira
  olmaydi, omborga kirim qila olmaydi.

### Controller da himoya

`ErpController` **deny-by-default**: `permissions()` da e'lon qilinmagan action
hech kimga ochilmaydi. Yangi action qo'shib, ruxsatini yozishni unutsangiz —
u ishlamaydi, teshik ochilmaydi.

Ko'rinishdagi cheklov yetarli emas: `SaleController` sotuvchiga `sale.view` bo'lmasa
so'rovga `seller_id = <o'zi>` shartini **majburan** qo'shadi va ID ni taxmin qilib
boshqaning sotuvini ochishga yo'l qo'ymaydi.

---

## SMS

Provayder almashtiriladigan qilib yozilgan:

```
common/services/sms/SmsProviderInterface.php   ← shartnoma
common/services/sms/EskizSmsProvider.php       ← Eskiz.uz (notify.eskiz.uz)
common/services/sms/LogSmsProvider.php         ← dev rejimi: faqat logga yozadi
common/services/SmsService.php                 ← jurnal + dedupe + shablon
```

Provayder `setting` jadvalidagi `sms.provider` qiymati bilan tanlanadi
(`eskiz` yoki `log`). Yangi provayder qo'shish uchun interfeysni implement qiling va
`common/config/main.php` dagi `container.singletons` ga qo'shing — qolgan kod o'zgarmaydi.

Sozlamalar: **Tizim → Sozlamalar → SMS** (login, parol, `from`, xabar shabloni).
Shablonda `{name}`, `{amount}`, `{date}` o'rinbosarlari ishlaydi.

### Cron

```bash
# Har kuni 09:00 — qarz muddati bugun tugaydiganlarga SMS
0 9 * * * php /path/to/project/yii sms/debt-reminders >> /var/log/erp-sms.log 2>&1

# Har kuni 09:30 — muddati o'tganlar uchun panel bildirishnomasi
30 9 * * * php /path/to/project/yii sms/overdue-notifications >> /var/log/erp-sms.log 2>&1
```

Windows Task Scheduler uchun: `php D:\OSPanel\domains\technic-sale.loc\yii sms/debt-reminders`

Buyruq **idempotent**: kuniga necha marta ishga tushsa ham bir mijozga bitta SMS ketadi
(`sms_log.dedupe_key` ustunidagi unikal indeks kafolatlaydi).

Sinov: `php yii sms/test 998901234567 "Salom"`

---

## Sotuvchi kassasi

```
balans = (sotuvda olingan naqd)  +  (qarz evaziga qabul qilingan to'lovlar)
         -  (admin sotuvchidan olib qo'ygan summa)
```

Muhim nuqta: `sale.paid_amount` — bu **sotuv paytida** olingan pul va u keyin
o'zgarmaydi. Keyingi to'lovlar `customer_payment` jadvalida yashaydi va
`sale.debt_amount` ni kamaytiradi. Aks holda bir to'lov ikki marta hisoblanardi.

Bekor qilingan sotuvlar hech qaysi hisobotga kirmaydi.

---

## Ishga tushirishdan oldin (production checklist)

- [ ] `admin` va `seller` parollarini almashtiring
- [ ] `common/config/main-local.php` da DB parolini qo'ying
- [ ] `setting` → `sms.provider` ni `eskiz` ga o'zgartiring va login/parolni kiriting
- [ ] `YII_DEBUG` ni `false` qiling (`backend/web/index.php`)
- [ ] `common/config/main.php` dagi `db.attributes[PDO::ATTR_PERSISTENT]` ni yuk ostida tekshiring
- [ ] Cron ni sozlang (yuqoriga qarang)
- [ ] `php yii cache/flush-all` — RBAC keshi yangilanishi uchun

---

## Kelajakdagi REST API / Flutter

Butun biznes-mantiq `common/services/*` da, `Yii::$app->request` ga bog'lanmagan holda
yozilgan. `api/` ilovasida controller yozib, ayni servislarni chaqirish kifoya:

```php
// api/modules/v1/controllers/SaleController.php
public function actionCreate()
{
    $sale = new Sale();
    $sale->load(Yii::$app->request->post(), '');
    $sale->seller_id = Yii::$app->user->id;

    try {
        $sale = Yii::createObject(SaleService::class)->create($sale, Yii::$app->request->post('items', []));
    } catch (BusinessRuleException $e) {
        throw new BadRequestHttpException($e->getMessage());
    }

    return $sale;
}
```

`ErpActiveRecord` `fields()` ni qo'llab-quvvatlaydi — JSON serializatsiya tayyor.
