# Analiza: Bezpośredni zapis zamówienia bez requisition

Data: 2026-04-07

---

## 1. Zidentyfikowany brakujący endpoint

### Problem
Panel wysyła `POST /shops/{idShop}/suppliers/{idSupplier}/order` (przez `MaterialOrderRepository::insert()`), a ten endpoint **nie istnieje** w API.

### Ścieżka wywołania (strona panelu):

```
Widok:        new_by_supplier.twig
               ↓ fetch POST /ajax/material-suppliers/{id}/orders/new
Router panel: POST /ajax/material-suppliers/{id:\d+}/orders/new
               ↓
Controller:   OrderController::ajaxSubmitOrder($id)
               ↓
Service:      MaterialOrderService::insert($idSupplier, $data)
              + dodaje id_employee, id_shop z GlobalRegistry
               ↓
Repository:   MaterialOrderRepository::insert($idShop, $idSupplier, $data)
               ↓
ApiClient:    POST /shops/{idShop}/suppliers/{idSupplier}/order   ← NIE ISTNIEJE
```

---

## 2. Istniejący (działający) flow — oparty o requisition

```
Widok:        convert_by_supplier.view.php
               ↓ fetch POST /ajax/materials/requisitions/{id}/convert
Router panel: POST /ajax/materials/requisitions/{idRequisition:\d+}/convert
               ↓
Controller:   RequisitionController::convertToOrder($idRequisition)
               ↓
Service (p.): MaterialRequisitionService::convertToOrder($idRequisition, $data)
              + dodaje id_employee, id_shop, id_requisition
               ↓
Repository:   MaterialRequisitionRepository::convertToOrder($id, $data)
               ↓
ApiClient:    POST /material-requisitions/{id}/convert
               ↓
API Route:    POST /material-requisitions/(\d+)/convert  ← ISTNIEJE
               ↓
Controller:   MaterialRequisitionController::convertToOrder($id)
               ↓
Service (a.): MaterialRequisitionService::convertToOrder($id, $data)
               ↓
              MaterialOrderService::convertFromRequisition($idRequisition, $data)
               ↓
              MaterialOrderService::insertFromRequisition($idRequisition, $data)
```

---

## 3. Dekompozycja `insertFromRequisition()` — co robi

### A. Transformacja danych
- Generuje `order_key`: `'ORD-' . $data['id_shop'] . "-" . strtoupper(bin2hex(random_bytes(8)))`
- Obsługuje split: `order_batch_key`, `batch_index` (opcjonalne)
- Oblicza `orderValue` = suma `pack_quantity * pack_price_net` z items

### B. Logika biznesowa (walidacja)
- Pobiera konfigurację dostawcy: `supplierOrderConfigService->getConfig($id_supplier)`
- Sprawdza minimum zamówienia: jeśli `orderValue < min_order_value` → rzuca wyjątek
- Oblicza opłatę dostawy: `supplierOrderConfigService->calculateFee()`

### C. Zapis zamówienia
- **INSERT** do `material_order` (tabela: `material_order`):
  - `order_key`, `order_batch_key`, `batch_index`
  - `id_shop`, `id_supplier`, **`id_requisition`**, `id_employee`
  - `order_date`, `expected_date`
  - `rabbitmq_send_status`, `rabbitmq_send_attempts=0`
  - `status='NEW'`

### D. Zapis opłat
- **INSERT** do `material_order_fee`:
  - `id_order`, `fee_type='delivery'`, `label`, `fee_amount`, `currency_code`

### E. Zapis pozycji zamówienia
- **INSERT** do `material_order_item` (pętla po items):
  - `id_order`, `id_material`, `id_supplier`, `supplier_sku`
  - **`material_name`**, `pack_quantity`, **`pack_size_in_base_unit`**, **`id_unit`**, `pack_price_net`
  - `status='PENDING'`

### F. Aktualizacja requisition ← SPECYFICZNE DLA REQUISITION (do pominięcia)
```php
$resUpdateRequisition = $this->requisitionItemRepository->updateFromOrder($idRequisition, $idOrder);
```
To aktualizuje `requisition_item.id_order` → oznacza pozycje zapotrzebowania jako zamówione.

---

## 4. Dekompozycja `convertFromRequisition()` — co robi ponad `insertFromRequisition`

### A. Weryfikacja dostawcy
```php
$supplier = $this->supplierRepository->first(['id' => $data['id_supplier']]);
$data['rabbitmq_send_status'] = $supplier['integrated_supplier'] ? 'pending' : null;
```

### B. Budowanie payloadu outbox (dla zintegrowanych dostawców)
```php
$payload = [
    'version'         => 1,
    'order_key'       => $order['order_key'],
    'order_batch_key' => $order['order_batch_key'] ?? null,
    'batch_index'     => $order['batch_index'] !== null ? (int) $order['batch_index'] : null,
    'shop_uid'        => $integrationData['shop_uid'],
    'order_date'      => $order['order_date'],
    'expected_date'   => $order['expected_date'],
    'order_value_net' => $orderValueNet,
    'fees'            => [...],
    'total_with_fees' => round($orderValueNet + $totalFees, 2),
    'products'        => [
        ['sku', 'name', 'pack_quantity', 'pack_price_net']
    ],
];
```

### C. Zapis do outbox
```php
$this->orderOutboxRepository->insertOnDuplicate([
    'id_shop'      => $order['id_shop'],
    'order_key'    => $order['order_key'],
    'payload_json' => $json
]);
```

---

## 5. Kontrakt wejściowy — co panel wysyła do NOWEGO endpointu

```json
{
  "delivery_date":  "2026-01-10",
  "delivery_date2": null,           // tylko przy split (delivery_mode=2)
  "delivery_mode":  1,              // 1=single, 2=split
  "split_pct":      null,           // procent przy split
  "note":           null,
  "items": [
    {
      "id_material":        5,
      "catalog_position_id": null,
      "supplier_sku":       "SKU123",
      "qty":               10,
      "qty_delivery_1":    10,       // rozdzielone przy split
      "qty_delivery_2":     0,       // rozdzielone przy split
      "price_net":          3.90
    }
  ],
  "order_params": {
    "beginning_of_period":      "2026-01-01",
    "requisition_period_days":  7,
    "sales_forecast":           100
  },
  "id_employee": 123,               // dodane przez panel service
  "id_shop":     456                // dodane przez panel service
}
```

---

## 6. Luka: dane wymagane przez `insertFromRequisition` a brakujące w payloadzie

| Pole wymagane przez `insertFromRequisition` | Dostępne w payloadzie panelu | Uwagi |
|---|---|---|
| `material_name` | ❌ NIE | Musi być pobrane z DB (`material.name`) |
| `pack_size_in_base_unit` | ❌ NIE | Musi być pobrane z DB (`material_packaging`) |
| `id_unit` | ❌ NIE | Musi być pobrane z DB lub default=3 |
| `supplier_sku` | ✅ TAK | Bezpośrednio w `items` |
| `id_material` | ✅ TAK | Bezpośrednio w `items` |
| `pack_quantity` (= `qty`) | ✅ TAK | Zmiana nazwy |
| `pack_price_net` (= `price_net`) | ✅ TAK | Zmiana nazwy |

---

## 7. Różnica w obsłudze split

| Aspekt | Flow requisition | Nowy wizard |
|---|---|---|
| Ile requestów | 2 × `/convert` (batch 1 i 2) | 1 × `/order` |
| Przekazanie split | `order_batch_key`, `batch_index` w każdym requeście | `delivery_mode=2`, `qty_delivery_1`, `qty_delivery_2` na pozycję |
| Kto tworzy 2 zamówienia | Klient (panel) | API (musi stworzyć 2 zamówienia wewnętrznie) |

**Wniosek**: nowy endpoint musi wewnętrznie obsłużyć podział na 2 zamówienia przy `delivery_mode=2`.

---

## 8. Proponowany endpoint

```
POST /shops/{idShop}/suppliers/{idSupplier}/order
```

**Kontrakt requestu** (taki jak wysyła panel — patrz sekcja 5).

**Kontrakt odpowiedzi (sukces)**:
```json
{
  "ok": true,
  "code": 201,
  "data": {
    "id_order": 123
  }
}
```
lub dla split:
```json
{
  "ok": true,
  "code": 201,
  "data": {
    "orders": [123, 124]
  }
}
```

---

## 9. Plan implementacji — `api`

### 9.1 Nowa metoda w `MaterialOrderService`

```
public function createDirect(int $idShop, int $idSupplier, array $data): array
```

Logika:
1. Weryfikacja dostawcy (jak w `convertFromRequisition`)
2. Ustawienie `rabbitmq_send_status` na podstawie `integrated_supplier`
3. Wzbogacenie items o `material_name`, `pack_size_in_base_unit`, `id_unit` (query do DB)
4. Mapowanie: `qty → pack_quantity`, `price_net → pack_price_net`
5. Jeśli `delivery_mode=1` → wywołaj `insertDirect($idShop, $idSupplier, $data)` raz
6. Jeśli `delivery_mode=2` → generuj `order_batch_key` (UUID), wywołaj `insertDirect` DWIE RAZY:
   - batch 1: items z `qty_delivery_1`, `batch_index=1`, `expected_date=delivery_date`
   - batch 2: items z `qty_delivery_2` (>0), `batch_index=2`, `expected_date=delivery_date2`
7. Dla zintegrowanych: outbox payload (identyczny z `convertFromRequisition`)
8. Wszystko w jednej transakcji

### 9.2 Nowa metoda `insertDirect()` — kopia `insertFromRequisition()` bez:
```php
$resUpdateRequisition = $this->requisitionItemRepository->updateFromOrder($idRequisition, $idOrder);
```
I z `id_requisition = null` przy INSERT do `material_order`.

### 9.3 Nowy route w `api/v1/Routes/Shop/materialSupplierRoutes.php` lub nowy plik

```php
$router->post('/shops/(\d+)/suppliers/(\d+)/order', function($idShop, $idSupplier) use ($container) {
    AuthMiddleware::checkAuth();
    $controller = $container->get(\App\Api\Controllers\Material\MaterialOrderController::class);
    $controller->createDirect($idShop, $idSupplier);
});
```

### 9.4 Nowa metoda w `MaterialOrderController`

```php
public function createDirect($idShop, $idSupplier)
{
    $data = $this->getValueFromPostRequest();
    $result = $this->materialOrderService->createDirect((int) $idShop, (int) $idSupplier, $data);
    ResponseHandler::sendCreated($result);
}
```

---

## 10. Ryzyka i luki

### RYZYKO 1: Brak `material_name` w payloadzie
Nowy endpoint musi pobierać `material_name` z tabeli `material` po `id_material`. Oznacza to dodatkowe zapytanie dla każdej pozycji (lub batch query). Trzeba sprawdzić, czy `BaseRepository::first(['id' => $idMaterial])` wystarczy.

### RYZYKO 2: Brak `pack_size_in_base_unit` i `id_unit`
Trzeba pobrać z tabeli `material_packaging` (lub `material_packaging_price_list`) po `id_material` i `id_supplier`. Pytanie: które opakowanie wybrać jeśli jest kilka? Logika powinna pobrać to powiązane z `supplier_sku` lub domyślne.

### RYZYKO 3: Kolumna `id_requisition` — czy może być NULL
Baza danych musi pozwalać na `id_requisition = NULL` w tabeli `material_order`. Nie mamy dostępu do schematu, by to potwierdzić. Należy to sprawdzić przed implementacją.

### RYZYKO 4: Obsługa split przy niepełnych danych
Jeśli `delivery_mode=2` a wszystkie `qty_delivery_2=0` → nie tworzyć drugiego zamówienia. Należy zdefiniować tę logikę.

### RYZYKO 5: Atomiczność split
Jeśli tworzymy 2 zamówienia w jednej transakcji i drugie się nie powiedzie, rollback cofnie oba. Czy to właściwe zachowanie? (vs. flow requisition, gdzie każde zamówienie jest osobnym requestem)

### RYZYKO 6: `catalog_position_id` — gdzie zapisać
Panel wysyła `catalog_position_id` w items. `insertFromRequisition` go nie używa i nie ma go w `material_order_item`. Należy zdecydować, czy ignorować, czy dodać kolumnę.

### RYZYKO 7: `order_params` — dokumentacja vs. zapis
Panel wysyła `order_params` jako metadane pomocnicze. Nie są zapisywane przez `insertFromRequisition`. Można je ignorować w nowym endpoincie.

### BRAKUJĄCE INFO: Skąd pobierać `pack_size_in_base_unit` i `id_unit`
Nie jest jasne, który PackRepository/query powinienem użyć, żeby pobrać właściwy `pack_size_in_base_unit` i `id_unit` dla danego `(id_material, id_supplier, supplier_sku)`. Potrzebna jest analiza zapytania w `PackRepository` lub `RequisitionItemRepository`.

---

## 11. Potwierdzenie kontraktu panelu — czy wymaga korekty

Panel (`MaterialOrderRepository::insert()`) wywołuje:
```php
$this->apiClient->post("/shops/{$idShop}/suppliers/{$idSupplier}/order", $data);
```

Po stronie panelu **nie ma potrzeby zmian** — endpoint URL jest poprawny, kontrakt danych jest poprawny. Wystarczy stworzyć odpowiadający endpoint w API.

Jedyna potencjalna korekta: jeśli zdecydujemy, że panel powinien wysyłać `material_name`, `pack_size_in_base_unit`, `id_unit` bezpośrednio (unikając lookup w API), wtedy widok `new_by_supplier.twig` musiałby rozszerzyć payload `submitOrder()`.

