<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use GuzzleHttp\Promise;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\Utils;
use Illuminate\Support\Str;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Bus;
class BusinessCentralService
{
    public function getAccessToken()
    {
        $client = new Client();
        $response = $client->post("https://login.microsoftonline.com/".env('AZURE_TENANT_ID')."/oauth2/v2.0/token", [
            'form_params' => [
                'grant_type' => 'client_credentials',
                'client_id' => env('AZURE_CLIENT_ID'),
                'client_secret' => env('AZURE_CLIENT_SECRET'),
                'scope' => 'https://api.businesscentral.dynamics.com/.default'
            ]
        ]);
        $body = json_decode((string)$response->getBody(), true);

        return $body['access_token'];
    }

    public function getCompanyId($token)
    {
        $client = new Client();
        $response = $client->get("https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/api/v2.0/companies", [
            'headers' => [
                'Authorization' => "Bearer {$token}",
                'Accept'        => 'application/json'
            ]
        ]);
        $companies = json_decode((string)$response->getBody(), true);
        $role = session('user')['role'] ?? null; 
        $companyId1 = $companies['value'][0]['id'];
        $companyId2 = $companies['value'][1]['id'];
        // For Production
        // $companyId1 = $companies['value'][0]['id'];
        // $companyId2 = $companies['value'][1]['id'];
        return [
            'companyId1' => $companyId1,
            'companyId2' => $companyId2
        ];
    }

    public function __construct()
    {
     $this->token = $this->getAccessToken();
     $companyIds = $this->getCompanyId($this->token);
     $this->companyRegent = $companyIds['companyId1'];
     $this->companyHIN    = $companyIds['companyId2'];
     $this->companyId = $this->companyRegent;

     $this->client = new Client();
    }

    
    function getMetadata() {
        
        $customersResponse = $this->client->get("https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/api/citbi/sku/v1.0/\$metadata", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer' => 'odata.maxpagesize=1000'
            ]
        ]);
        $xml = simplexml_load_string((string)$customersResponse->getBody());
        $xml->registerXPathNamespace('edmx', 'http://docs.oasis-open.org/odata/ns/edmx'); 
        $xml->registerXPathNamespace('edm', 'http://docs.oasis-open.org/odata/ns/edm');
        $schemas = $xml->xpath('//edm:Schema');

        $entities = [];

        foreach ($schemas as $schema) {
            foreach ($schema->EntityType as $entity) {
                $entityName = (string) $entity['Name'];
                $properties = [];

                foreach ($entity->Property as $prop) {
                    $properties[] = [
                        'name' => (string) $prop['Name'],
                        'type' => (string) $prop['Type'],
                        'nullable' => isset($prop['Nullable']) ? (string) $prop['Nullable'] : 'true',
                    ];
                }

                $entities[$entityName] = $properties;
            }
        }
        
        if (isset($entities['purchaseOrderLine'])) {
        $logData = [];

        foreach ($entities['purchaseOrderLine'] as $field) {
            $logData[] = [
                'name'     => $field['name'],
                'type'     => $field['type'],
                'nullable' => $field['nullable'],
            ];
        }

        Log::info('📦 Entity: purchaseOrderLine', $logData);
    } else {
        Log::warning('Entity purchaseOrderLine tidak ditemukan di metadata');
    }

    return $schemas;

        echo "<pre>";
        echo "ðŸ“¦ Entity: purchaseOrderLine\n\n";

        foreach ($entities['purchaseOrderLine'] as $field) {
            echo " â–¸ {$field['name']} ({$field['type']})";
            echo $field['nullable'] === 'false' ? " [NOT NULL]" : "";
            echo "\n";
        }
        echo "</pre>";
        exit;
    }

      public function getData(string $endpoint, array $queryParams = [])
{
    $url = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/v2.0/companies({$this->companyId})/{$endpoint}";

    if (!empty($queryParams)) {
        $url .= '?' . http_build_query($queryParams);
    }

    try {
        $response = $this->client->get($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ]
        ]);

        $data = json_decode((string)$response->getBody(), true);
        $items = $data['value'] ?? [];

        // === Group dan sort otomatis hanya jika endpoint = 'items' ===
        if ($endpoint === 'items') {
            $groupedItems = [];
            $itemsWithoutPrefix = [];

            foreach ($items as $item) {
                $number = $item['number'] ?? '';
                if (preg_match('/^[A-Z]+/i', $number, $matches)) {
                    $prefix = strtoupper($matches[0]);
                    if (!isset($groupedItems[$prefix])) {
                        $groupedItems[$prefix] = [];
                    }
                    $groupedItems[$prefix][] = $item;
                } else {
                    $itemsWithoutPrefix[] = $item;
                }
            }

            if (!empty($itemsWithoutPrefix)) {
                $groupedItems['OTHER'] = $itemsWithoutPrefix;
            }

            // Sort setiap grup berdasarkan angka di number
            foreach ($groupedItems as $prefix => &$group) {
                usort($group, function($a, $b) {
                    $numA = (int) preg_replace('/\D/', '', $a['number'] ?? 0);
                    $numB = (int) preg_replace('/\D/', '', $b['number'] ?? 0);
                    return $numA <=> $numB;
                });
            }
            unset($group);

            // Ganti data['value'] menjadi grouped & sorted
            $data['value'] = $groupedItems;
        }

        return [
            'status' => 'success',
            'data' => $data
        ];
    } catch (\Exception $e) {
        return [
            'status' => 'error',
            'message' => $e->getMessage()
        ];
    }
}



    public function getAllPurchaseQtys()
    {
        $start = microtime(true);
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/Purchase_Lines_Excel?\$filter=Outstanding_Quantity gt 0 and (Status eq 'Released' or Status eq 'Pending Approval' or Status eq 'Pending Prepayment')  and (Location_Code eq 'CI.1010') &\$select=No,Location_Code,Outstanding_Quantity";

        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json'
            ]
        ];

        $allData = [];
        do {
            $response = $this->client->get($url, $headers);
            $decoded = json_decode((string)$response->getBody(), true);
            if (isset($decoded['value'])) {
                $allData = array_merge($allData, $decoded['value']);
            }
            $url = $decoded['@odata.nextLink'] ?? null;
        } while ($url);

        $result = [];
        foreach ($allData as $line) {
            $key = $line['No'] . '_' . $line['Location_Code'];
            $result[$key] = ($result[$key] ?? 0) + ($line['Outstanding_Quantity'] ?? 0);
        }
        $elapsed = microtime(true) - $start;
        Log::info("Fetch runtime Purchaseqty: {$elapsed} seconds");
        

        return $result;
    }

    public function getAllTransferQtys()
    {
        $start = microtime(true);
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/Transfer_Order_Line_Excel?\$filter=Status eq 'Released' and Quantity_Shipped lt Quantity and (Transfer_From_Code eq 'CI.1010'
            ) &\$select=Document_No,Item_No,Transfer_From_Code,OutstandingShipped";

        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer'        => 'odata.maxpagesize=1000'
            ]
        ];

        $allData = [];
        do {
            $response = $this->client->get($url, $headers);
            $decoded = json_decode((string)$response->getBody(), true);
            if (isset($decoded['value'])) {
                $allData = array_merge($allData, $decoded['value']);
            }
            $url = $decoded['@odata.nextLink'] ?? null;
        } while ($url);

        $result = [];
        foreach ($allData as $line) {
            $key = $line['Item_No'] . '_' . $line['Transfer_from_Code'];
            $result[$key] = ($result[$key] ?? 0) + ($line['OutstandingShipped'] ?? 0);
        }

        $elapsed = microtime(true) - $start;
        Log::info("Fetch runtime Transfer Qty: {$elapsed} seconds");
        return $result;
    }

    public function getTransferLinesFromBC()
    {
        $start = microtime(true);
        $baseUrl = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/Transfer_Order_Line_Excel?\$filter=Status eq 'Released' and Transfer_from_Code eq 'CI.1010'";

        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer'        => 'odata.maxpagesize=5000',
            ]
        ];

        $allData = [];
        $url = $baseUrl;

        do {
            $response = $this->client->get($url, $headers);
            $decoded = json_decode((string)$response->getBody(), true);

            if (isset($decoded['value'])) {
                $allData = array_merge($allData, $decoded['value']);
            }

            $url = $decoded['@odata.nextLink'] ?? null;
        } while ($url);
        $elapsed = microtime(true) - $start;
        Log::info("Fetch runtime Transfer Qty: {$elapsed} seconds");
        return $allData;
    }


    function getPurchaseLineFromBC() {
    $url = "https://api.businesscentral.dynamics.com/v2.0/"
         . env('AZURE_TENANT_ID') . "/"
         . env('BC_ENVIRONMENT')
         . "/api/v2.0/companies(" . $this->companyId . ")/purchaseOrders(83ea2b56-d008-ee11-8f70-00224857ecae)/purchaseOrderLines";

    try {
        $resp = $this->client->get($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer'        => 'odata.maxpagesize=1000',
            ],
            // Optional: let 4xx/5xx come back without throwing
            // 'http_errors' => false,
        ]);

        return [
            'ok'      => true,
            'status'  => $resp->getStatusCode(),
            'reason'  => $resp->getReasonPhrase(),
            'headers' => $resp->getHeaders(),
            'json'    => json_decode((string)$resp->getBody(), true),
            'raw'     => (string)$resp->getBody(),
        ];

    } catch (RequestException $e) {
        $res     = $e->getResponse();
        $status  = $res ? $res->getStatusCode() : null;
        $reason  = $res ? $res->getReasonPhrase() : null;
        $headers = $res ? $res->getHeaders() : [];
        $raw     = $res ? (string)$res->getBody() : $e->getMessage();
        $json    = json_decode($raw, true);

        // Log everything (no truncation in logs)
        Log::error('BC GET purchaseOrderLines failed', [
            'url'      => $url,
            'status'   => $status,
            'reason'   => $reason,
            'headers'  => $headers,
            'raw'      => $raw,
            'exception'=> $e->getMessage(),
        ]);

        // Return a rich error object to caller
        return [
            'ok'       => false,
            'status'   => $status,
            'reason'   => $reason,
            'headers'  => $headers,
            'errorRaw' => $raw,
            'error'    => $json ?? $raw,  // parsed JSON if possible
            'message'  => $e->getMessage(),
        ];
    } catch (\Throwable $e) {
        Log::error('Unexpected error calling BC', [
            'url'      => $url,
            'exception'=> $e->getMessage(),
        ]);

        return [
            'ok'      => false,
            'status'  => 500,
            'reason'  => 'Unexpected Error',
            'error'   => $e->getMessage(),
        ];
    }
}

public function billOfMaterial()
{
    try {
        set_time_limit(300);

        $baseOdata = "https://api.businesscentral.dynamics.com/v2.0/"
            . env('AZURE_TENANT_ID') . "/"
            . env('BC_ENVIRONMENT')
            . "/ODataV4/Company('" . $this->companyId . "')";

        $endpoint = "$baseOdata/BillOfMaterial";

        $headers = [
            'Authorization' => "Bearer {$this->token}",
            'Accept'        => 'application/json',
            'Prefer'        => 'odata.maxpagesize=50000'
        ];

        $options = [
            'headers' => $headers,
            'timeout' => 300,
            'connect_timeout' => 60
        ];

        $response = $this->client->get($endpoint, $options);
        $body = json_decode($response->getBody()->getContents(), true);
        $rawData = $body['value'] ?? [];

        // 🔹 Filter hanya yang Business_Unit == 'RBC'
        $filteredData = array_filter($rawData, function ($row) {
            return isset($row['Business_Unit']) && strtoupper(trim($row['Business_Unit'])) === 'CI';
        });

        // 🔧 Kelompokkan berdasarkan BOM_Parent
        $grouped = [];

        foreach ($filteredData as $row) {
            $parent = $row['BOM_Parent'] ?? null;
            if (!$parent) continue;

            if (!isset($grouped[$parent])) {
                $grouped[$parent] = [
                    'BOM_Parent'      => $parent,
                    'Description_BOM' => $row['Description_BOM'] ?? '',
                    'Uom_BOM_Parent'  => $row['Uom_BOM_Parent'] ?? '',
                    'Standart_Cost'   => $row['Standart_Cost'] ?? 0,
                    'Business_Unit'   => $row['Business_Unit'] ?? '',
                    'Item_Operation'  => []
                ];
            }

            // Tambahkan child item ke dalam Item_Operation
            $grouped[$parent]['Item_Operation'][] = [
                '@odata.etag'      => $row['@odata.etag'] ?? null,
                'Row_No'           => $row['Row_No'] ?? null,
                'Operation'        => $row['Operation'] ?? null,
                'BOM'              => $row['BOM'] ?? null,
                'BOM_Description'  => $row['BOM_Description'] ?? '',
                'Quantity'         => $row['Quantity'] ?? 0,
                'Uom_BOM'          => $row['Uom_BOM'] ?? '',
                'Price'            => $row['Price'] ?? 0
            ];
        }

        $finalData = array_values($grouped); // reset index numerik

        return [
            'status' => 'success',
            'count'  => count($finalData),
            'items'  => $finalData
        ];

    } catch (\Exception $e) {
        return [
            'status'  => 'error',
            'message' => $e->getMessage()
        ];
    }
}
    

    public function getStockV3()
{
    try {
        set_time_limit(300);

        $baseOdata = "https://api.businesscentral.dynamics.com/v2.0/"
            . env('AZURE_TENANT_ID') . "/"
            . env('BC_ENVIRONMENT')
            . "/ODataV4/Company('" . $this->companyId . "')";

        $filter = urlencode(
            "Qty gt 0 and (" .
                "LocationCode eq 'RBC.2100'" .
                // "LocationCode eq 'RBC.2110' or " .
                // "LocationCode eq 'RBC.2120' or " .
                // "LocationCode eq 'RBC.2130' or " .
                // "LocationCode eq 'RBC.2140'" .
            ")"
        );

        $endpoint = "$baseOdata/StockV3?\$filter={$filter}";

        $options = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
            ],
            'timeout' => 300,
            'connect_timeout' => 60
        ];

        $response = $this->client->get($endpoint, $options);
        $body = json_decode($response->getBody()->getContents(), true);
        $rows = $body['value'] ?? [];
        $grouped = [];

        foreach ($rows as $row) {
            $location = $row['LocationCode'] ?? 'UNKNOWN';
            $itemNo   = $row['ItemNo'] ?? 'UNKNOWN';

            if (!isset($grouped[$location])) {
                $grouped[$location] = [];
            }

            $grouped[$location][$itemNo] = $row;
        }

        return [
            'status'        => 'success',
            'total_items'   => count($rows),
            'locations'     => array_keys($grouped),
            'items_by_loc'  => $grouped,
        ];

    } catch (\Exception $e) {
        return [
            'status'  => 'error',
            'message' => $e->getMessage()
        ];
    }
}



public function getCombinedBOMWithStock()
{
    $cacheKey = 'bc_bom_with_stock_final';
    $cacheTtl = now()->addMinutes(2); // cache 2 menit

    // === Jika cache final tersedia, kembalikan dulu ===
    if (Cache::has($cacheKey)) {
        $cachedData = Cache::get($cacheKey);

        // Jalankan refresh di background tanpa blocking
        dispatch(function () use ($cacheKey, $cacheTtl) {
            try {
                $fresh = $this->generateFinalBOMWithStock();
                Cache::put($cacheKey, $fresh, $cacheTtl);
                Log::info('[BC] BOM+Stock cache refreshed in background');
            } catch (\Throwable $e) {
                Log::error('[BC] Failed to refresh BOM+Stock cache in background: ' . $e->getMessage());
            }
        })->afterResponse();

        return [
            'status'  => 'success',
            'source'  => 'cache',
            'message' => 'Loaded from cache (background refresh running)',
            'count'   => count($cachedData),
            'items'   => $cachedData,
        ];
    }

    // === Jika cache belum ada, generate langsung ===
    $final = $this->generateFinalBOMWithStock();

    Cache::put($cacheKey, $final, $cacheTtl);

    return [
        'status'  => 'success',
        'source'  => 'fresh',
        'message' => 'Generated and cached final data',
        'count'   => count($final),
        'items'   => $final,
    ];
}

// ===========================
// Function generate final data
// ===========================
protected function generateFinalBOMWithStock()
{
    $bomData        = $this->billOfMaterial()['items'] ?? [];
    $stockData      = $this->getCombinedStock()['items'] ?? [];
    $transferData   = $this->getAllTransferLinesData() ?? [];
    $assemblyData   = $this->getAllAssemblyOrder()['data'] ?? []; // <- ambil dari 'data'

    // Filter Business Unit RBC
    $bomData = array_filter($bomData, fn($item) =>
        isset($item['Business_Unit']) && strtoupper(trim($item['Business_Unit'])) === 'CI'
    );

    // =========================
    // PETA STOK
    // =========================
    $stockMap = [];
    foreach ($stockData as $s) {
        if (!empty($s['Item_No'])) {
            $stockMap[strtoupper(trim($s['Item_No']))] = [
                'Inventory'          => (float)($s['Inventory'] ?? 0),
                'Minimum_Stock'      => (float)($s['Minimum_Stock'] ?? 0),
                'Maximum_Inventory'  => (float)($s['Maximum_Inventory'] ?? 0),
                'Base_UoM'           => strtoupper(trim($s['Base_UoM'] ?? '')),
                'UoM'                => $s['UoM'] ?? [],
            ];
        }
    }

    // =========================
    // PETA TRANSFER ORDER (TO)
    // =========================
    $transferMapOpen     = [];
    $transferCountRel    = [];
    $transferQtyRelTotal = [];
    $transferListByItem  = [];

    foreach ($transferData as $t) {
        if (empty($t['itemNo']) || !isset($t['status'])) continue;

        $normalizedItem = strtoupper(trim($t['itemNo']));
        $status         = strtolower(trim($t['status']));
        $qty            = (float)($t['quantity'] ?? 0);
        $doc            = $t['documentNo'] ?? null;
        $ship           = $t['shipmentDate'] ?? null;

        // TO Released → simpan ke list
        if ($status === 'released' && $doc && $ship) {
            $transferListByItem[$normalizedItem][] = [
                'documentNo'   => $doc,
                'shipmentDate' => $ship,
                'quantity'     => $qty,
            ];
        }

        // Hitung total TO released
        if ($status === 'released') {
            $transferCountRel[$normalizedItem]    = ($transferCountRel[$normalizedItem] ?? 0) + 1;
            $transferQtyRelTotal[$normalizedItem] = ($transferQtyRelTotal[$normalizedItem] ?? 0) + $qty;
            $transferMapOpen[$normalizedItem]     = true;
        }

        if (!isset($transferMapOpen[$normalizedItem])) {
            $transferMapOpen[$normalizedItem] = false;
        }
    }

// =========================
// PETA ASSEMBLY ORDER (AO)
// =========================
$aoCountByItem = [];
$aoQtyByItem   = [];

foreach ($assemblyData as $ao) {
    if (empty($ao['itemNo'])) continue;

    $normalizedItem = strtoupper(trim($ao['itemNo']));
    $qty = (float)($ao['Quantity'] ?? 0);

    // Hitung jumlah dokumen AO per item
    $aoCountByItem[$normalizedItem] = ($aoCountByItem[$normalizedItem] ?? 0) + 1;

    // Hitung total Quantity AO per item
    $aoQtyByItem[$normalizedItem] = ($aoQtyByItem[$normalizedItem] ?? 0) + $qty;
}

    // =========================
    // GABUNGKAN DATA FINAL
    // =========================
    $final = [];
    foreach ($bomData as $parent) {
        $parentNo = $parent['BOM_Parent'] ?? null;
        if (!$parentNo) continue;

        $normalizedParent = strtoupper(trim($parentNo));
        $stockInfo = $stockMap[$normalizedParent] ?? [];

        $parent['Inventory']         = $stockInfo['Inventory'] ?? 0;
        $parent['Minimum_Stock']     = $stockInfo['Minimum_Stock'] ?? 0;
        $parent['Maximum_Inventory'] = $stockInfo['Maximum_Inventory'] ?? 0;

        if (!empty($parent['Item_Operation'])) {
            foreach ($parent['Item_Operation'] as &$child) {
                $childNo = strtoupper(trim($child['BOM'] ?? ''));
                $childStock = $stockMap[$childNo] ?? [];

                $child['Inventory'] = $childStock['Inventory'] ?? 0;
                $child['Base_UoM']  = $childStock['Base_UoM'] ?? null;

                $qty = (float)($child['Quantity'] ?? 0);
                $uomRecipe = strtoupper(trim($child['Uom_BOM'] ?? ''));

                $conversionRate = 1;
                if ($childStock && !empty($childStock['UoM'])) {
                    foreach ($childStock['UoM'] as $uomItem) {
                        $code = strtoupper(trim($uomItem['Code'] ?? ''));
                        if ($code === $uomRecipe) {
                            $conversionRate = (float)($uomItem['Qty_Unit'] ?? 1);
                            break;
                        }
                    }
                }

                $child['Quantity_Base']       = $qty * $conversionRate;
                $child['Inventory_Remaining'] = ($child['Inventory'] ?? 0) - $child['Quantity_Base'];
            }
            unset($child);
        }

        // === Tambahan AO Counter ===
        $parent['Outstanding_AO'] = $aoCountByItem[$normalizedParent] ?? 0;
        $parent['Outstanding_AO_Qty']  = $aoQtyByItem[$normalizedParent] ?? 0;
        // === Transfer Data ===
        $parent['statusTO']     = $transferMapOpen[$normalizedParent] ?? false;
        $parent['to_count']     = $transferCountRel[$normalizedParent] ?? 0;
        $parent['to_total_qty'] = $transferQtyRelTotal[$normalizedParent] ?? 0;
        $parent['to_list']      = $transferListByItem[$normalizedParent] ?? [];

        $final[] = $parent;
    }

    Log::info('[BC] BOM+Stock final generated: ' . count($final) . ' items');

    return array_values($final);
}

public function getAllTransferLinesData()
{
    // ✅ Versi ringan dari getAllTransferLines(), tanpa response()->json() dan overhead HTTP
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/transferLineCustom?\$filter = status eq 'Released' and fromCode eq 'CI.1011'";

    $client = new \GuzzleHttp\Client([
        'timeout'         => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->get($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
            ],
        ]);

        $result = json_decode((string) $response->getBody(), true);
        $rows = $result['value'] ?? [];

        // ============================================================
        // ⛔ FILTER BARU:
        //    Jika qtyShip == quantity → JANGAN IKUTKAN DATA
        // ============================================================
        $filtered = array_filter($rows, function ($row) {
            // pastikan key ada
            $qtyShip   = $row['qtyShip']   ?? null;
            $quantity  = $row['quantity']  ?? null;

            // Jika salah satu null → tetap tampilkan
            if ($qtyShip === null || $quantity === null) {
                return true;
            }

            // Jika sama → HILANGKAN dari hasil
            if ((float)$qtyShip == (float)$quantity) {
                return false;
            }

            return true;
        });

        // array_filter menjaga key asli → reset agar clean
        return array_values($filtered);

    } catch (\Throwable $e) {
        Log::error('BC getAllTransferLinesData error: ' . $e->getMessage());
        return [];
    }
}

public function getAllBillOfMaterialData()
{
    // ✅ Versi ringan dari getAllBillOfMaterial(), tanpa response()->json() dan overhead HTTP
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/billOfMaterial";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->get($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
        ]);

        $result = json_decode((string) $response->getBody(), true);
        return $result['value'] ?? [];

    } catch (\Throwable $e) {
        Log::error('BC getAllBillOfMaterialData error: ' . $e->getMessage());
        return [];
    }
}


public function getCombinedStock(bool $forceRefresh = false)
{
    $cacheKey = 'combined_stock_v3';

    if ($forceRefresh) {
        Cache::forget($cacheKey);
    }

    $data = Cache::remember($cacheKey, now()->addMinutes(30), function () {
        return $this->generateCombinedStockByLocation();
    });

    return [
        'status' => 'success',
        'source' => Cache::has($cacheKey) ? 'cache' : 'fresh',
        'count'  => collect($data)->flatten(1)->count(),
        'items'  => $data,
    ];
}


protected function generateCombinedStockByLocation(): array
{
    $stockBC   = $this->getStockkeepingFromBC()['locations'] ?? [];
    $stockV3   = $this->getStockV3()['items_by_loc'] ?? [];
    $unitCost  = $this->getStockUnitCost()['items_by_location'] ?? [];

    // ===============================
    // Index Unit Cost: [loc][item]
    // ===============================
    $unitCostIndex = [];
    foreach ($unitCost as $loc => $rows) {
        foreach ($rows as $row) {
            $itemNo = $row['Item_No'] ?? null;
            if ($itemNo) {
                $unitCostIndex[$loc][$itemNo] = floatval($row['Unit_Cost'] ?? 0);
            }
        }
    }

    $result = [];

    // ===============================
    // PROCESS PER LOCATION
    // ===============================
    $allLocations = array_unique(array_merge(
        array_keys($stockBC),
        array_keys($stockV3)
    ));

    foreach ($allLocations as $location) {

        $bcItems = $stockBC[$location] ?? [];
        $v3Items = $stockV3[$location] ?? [];

        // Index V3 by ItemNo
        $v3Index = [];
        foreach ($v3Items as $row) {
            if (!empty($row['ItemNo'])) {
                $v3Index[$row['ItemNo']] = $row;
            }
        }

        $usedItems = [];

        // ===============================
        // 1️⃣ PRIORITY: stockFromBC
        // ===============================
        foreach ($bcItems as $item) {
            $itemNo = $item['Item_No'] ?? null;
            if (!$itemNo) continue;

            $inventory = floatval($item['Inventory'] ?? 0);
            $unitCost  = $unitCostIndex[$location][$itemNo] ?? 0;

            $result[$location][] = [
                'Item_No' => $itemNo,
                'Description' => $item['Description'] ?? '',
                'Inventory' => $inventory,
                'Minimum_Stock' => floatval($item['Safety_Stock_Quantity'] ?? 0),
                'Maximum_Inventory' => floatval($item['Maximum_Inventory'] ?? 0),
                'Replenishment_System' => $item['Replenishment_System'] ?? null,
                'Base_UoM' => $item['UoMCode'] ?? null,
                'UoM' => [],

                'Unit_Cost' => $unitCost,
                'Total_Cost' => round($inventory * $unitCost, 2),
            ];

            $usedItems[$itemNo] = true;
        }

        // ===============================
        // 2️⃣ FALLBACK: stockV3
        // ===============================
        foreach ($v3Index as $itemNo => $item) {
            if (isset($usedItems[$itemNo])) continue;

            $inventory = floatval($item['Qty'] ?? 0);
            $unitCost  = $unitCostIndex[$location][$itemNo] ?? 0;

            $result[$location][] = [
                'Item_No' => $itemNo,
                'Description' => $item['ItemDescription'] ?? '',
                'Inventory' => $inventory,
                'Minimum_Stock' => 0,
                'Maximum_Inventory' => 0,
                'Replenishment_System' => $item['Replenishment_System'] ?? null,
                'Base_UoM' => $item['UoMCode'] ?? null,
                'UoM' => [],

                'Unit_Cost' => $unitCost,
                'Total_Cost' => round($inventory * $unitCost, 2),
            ];
        }
    }

    // ===============================
    // Attach UOM
    // ===============================
    $allItemNos = collect($result)->flatten(1)->pluck('Item_No')->unique()->values()->all();
    $uomIndex = $this->fetchUoMForItems($allItemNos);

    foreach ($result as $loc => &$items) {
        foreach ($items as &$row) {
            $row['UoM'] = $uomIndex[$row['Item_No']] ?? [];
        }
    }

    return $result;
}

/**
 * 🚀 Ambil Item_UOM hanya untuk item tertentu (dalam batch kecil)
 */
protected function fetchUoMForItems(array $itemNos)
{
    $uomIndex = [];
    $chunkSize = 50; // aman untuk panjang URL BC
    $headers = [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept'        => 'application/json',
            'Prefer'        => 'odata.maxpagesize=1000',
        ]
    ];

    $baseUrl = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/ODataV4/Company('{$this->companyId}')/Item_UOM";

    $client = new \GuzzleHttp\Client(['timeout' => 60]);

    // 🔁 Bagi item menjadi beberapa batch agar tetap ringan
    foreach (array_chunk($itemNos, $chunkSize) as $chunk) {
        $filterList = array_map(fn($no) => "Item_No eq '$no'", $chunk);
        $filterQuery = '$filter=' . implode(' or ', $filterList);
        $url = "{$baseUrl}?{$filterQuery}";

        try {
            $response = $client->get($url, $headers);
            $decoded  = json_decode((string)$response->getBody(), true);
            $values   = $decoded['value'] ?? [];

            foreach ($values as $uom) {
                $itemNo   = $uom['Item_No'] ?? null;
                $code     = $uom['Code'] ?? null;
                $qtyPer   = isset($uom['Qty_per_Unit_of_Measure'])
                            ? (float)$uom['Qty_per_Unit_of_Measure']
                            : null;

                if (!$itemNo || !$code) continue;

                // Inisialisasi array jika belum ada
                if (!isset($uomIndex[$itemNo])) {
                    $uomIndex[$itemNo] = [];
                }

                // Hindari duplikasi Code
                $alreadyExists = false;
                foreach ($uomIndex[$itemNo] as $entry) {
                    if ($entry['Code'] === $code) {
                        $alreadyExists = true;
                        break;
                    }
                }

                if (!$alreadyExists) {
                    $uomIndex[$itemNo][] = [
                        'Code'      => $code,
                        'Qty_Unit'  => $qtyPer,
                    ];
                }
            }

        } catch (\Exception $e) {
            Log::warning('[BC] Failed fetching UOM batch: ' . $e->getMessage());
        }
    }

    Log::info('[BC] UOM fetched for ' . count($uomIndex) . ' items (with Qty_Unit)');
    return $uomIndex;
}

    function getStockkeepingFromBC()
{
    // =========================================
    // MULTI LOCATION FILTER (RBC)
    // =========================================
    $filter = urlencode(
        "(Location_Code eq 'RBC.2100'
          or Location_Code eq 'RBC.2110'
          or Location_Code eq 'RBC.2120'
          or Location_Code eq 'RBC.2130'
          or Location_Code eq 'RBC.2140')"
    );

    $url =
        "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/ODataV4/Company('{$this->companyId}')"
        . "/StockkeepingUnit?\$filter={$filter}";

    $headers = [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept'        => 'application/json',
            'Prefer'        => 'odata.maxpagesize=50000',
        ]
    ];

    $allData = [];

    // =========================================
    // PAGINATION LOOP (TETAP)
    // =========================================
    do {
        $response = $this->client->get($url, $headers);
        $decoded  = json_decode((string) $response->getBody(), true);

        if (isset($decoded['value'])) {
            $allData = array_merge($allData, $decoded['value']);
        }

        $url = $decoded['@odata.nextLink'] ?? null;

    } while ($url);

    // =========================================
    // GROUP BY Location_Code
    // =========================================
    $groupedByLocation = [];

    foreach ($allData as $row) {
        $location = $row['Location_Code'] ?? 'UNKNOWN';

        if (!isset($groupedByLocation[$location])) {
            $groupedByLocation[$location] = [];
        }

        $groupedByLocation[$location][] = $row;
    }

    // =========================================
    // RETURN TERSTRUKTUR
    // =========================================
    return [
        'total'     => count($allData),
        'locations' => $groupedByLocation,
    ];
}


    function itemUoM()
    {
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/Item_UOM";
        
        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer' => 'odata.maxpagesize=1000'
            ]
        ];
        $allData = [];
        do {
            $response = $this->client->get($url, $headers);
            $decoded = json_decode((string)$response->getBody(), true);

            if (isset($decoded['value'])) {
                $allData = array_merge($allData, $decoded['value']);
            }

            $url = $decoded['@odata.nextLink'] ?? null;
        } while ($url);

        return ['value' => $allData];
    }

    public function getItemCardExcelRaw()
{
    try {
        // ⏱️ Perpanjang waktu eksekusi PHP
        set_time_limit(300);

        $baseOdata = "https://api.businesscentral.dynamics.com/v2.0/"
            . env('AZURE_TENANT_ID') . "/"
            . env('BC_ENVIRONMENT')
            . "/ODataV4/Company('PT.CI%20LIVE')";

        // 🔹 Ambil hanya field No, Description, dan AssemblyBOM
        // 🔹 Filter langsung di API: AssemblyBOM eq true
        // 🔹 Batasi hanya 100 hasil
        $endpoint = "$baseOdata/Item_Card_Excel?"
            . "\$filter=Business_Unit eq 'RBC'";

        $headers = [
            'Authorization' => "Bearer {$this->token}",
            'Accept'        => 'application/json',
            'Prefer'        => 'odata.maxpagesize=50000'
        ];

        $options = [
            'headers' => $headers,
            'timeout' => 300, // waktu maksimum request 5 menit
            'connect_timeout' => 60
        ];

        // 🔹 Kirim request
        $response = $this->client->get($endpoint, $options);
        $body = json_decode($response->getBody()->getContents(), true);

        $data = $body['value'] ?? [];

        return [
            'status' => 'success',
            'count'  => count($data),
            'items'  => $data
        ];

    } catch (\Exception $e) {
        return [
            'status'  => 'error',
            'message' => $e->getMessage()
        ];
    }
}


    public function getAllVendorsMap()
    {
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/api/v2.0/companies({$this->companyId})/vendors?\$select=number,displayName";
        $vendors = [];
        do {
            $response = $this->client->get($url, [
                'headers' => [
                    'Authorization' => "Bearer {$this->token}",
                    'Accept' => 'application/json',
                    'Prefer' => 'odata.maxpagesize=1000'
                ]
            ]);
            $data = json_decode($response->getBody(), true);

            foreach ($data['value'] as $vendor) {
                $vendors[$vendor['number']] = $vendor['displayName'];
            }

            $url = $data['@odata.nextLink'] ?? null; 

        } while ($url);

        return $vendors;
    }


    public function getVendor() {
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/api/v2.0/companies({$this->companyId})/vendors";

        $response = $this->client->get($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json'
            ]
        ]);
        return json_decode($response->getBody(), true);
    }

    
public function createItem(array $itemData)
{
    $url = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/v2.0/companies({$this->companyId})/items";

    // Ambil unitPrice
    $unitPrice = 0;
    if (isset($itemData['unitPrice'])) {
        $unitPrice = $itemData['unitPrice'];
    } elseif (!empty($itemData['vendorQuotes']) && is_array($itemData['vendorQuotes']) && isset($itemData['vendorQuotes'][0]['price'])) {
        $unitPrice = $itemData['vendorQuotes'][0]['price'];
    }
    $unitPrice = is_numeric($unitPrice) ? (float) $unitPrice : 0.0;

    // Siapkan payload
    $payload = [
        'number' => $itemData['number'] ?? null,
        'displayName' => $itemData['displayName'] ?? null,
        'type' => $itemData['type'] ?? 'Inventory',
        'baseUnitOfMeasureCode' => $itemData['baseUnitOfMeasureCode'] ?? 'PTN',
        'generalProductPostingGroupCode' => $itemData['generalProductPostingGroupCode'] ?? 'GP.FOD075',
        'inventoryPostingGroupCode' => $itemData['inventoryPostingGroupCode'] ?? 'INV_FOOD',
        'unitPrice' => $unitPrice,
    ];

    // 🧩 Client Guzzle dengan timeout panjang
    $client = new \GuzzleHttp\Client([
        'timeout' => 180,          // Maksimal 3 menit per request
        'connect_timeout' => 20,   // Maksimal waktu membuka koneksi
    ]);

    try {
        $response = $client->post($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
            'json' => $payload,
        ]);

        return [
            'status' => 'success',
            'data' => json_decode((string) $response->getBody(), true)
        ];
    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $responseBody = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : null;

        \Log::error('BC createItem RequestException: ' . $e->getMessage(), [
            'payload' => $payload,
            'response' => $responseBody
        ]);

        return [
            'status' => 'error',
            'message' => $e->getMessage(),
            'response' => $responseBody
        ];
    } catch (\Exception $e) {
        \Log::error('BC createItem general error: ' . $e->getMessage(), [
            'payload' => $payload
        ]);

        return [
            'status' => 'error',
            'message' => $e->getMessage()
        ];
    }
}



public function getAssemblyServiceData()
{
    $url = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/ODataV4/ServiceButcher_AssignLot?Company=PT.CI%20LIVE";

    try {
        // Contoh body JSON kosong jika action tidak pakai parameter
        $body = new \stdClass(); 
        
        $body->assemblyNo = 'SO.CI.25120005';
        $body->qtyBase    = 1;
        $body->type       = 'STOCK';
        $response = Http::withHeaders([
            'Authorization' => 'Bearer ' . $this->token,
            'Accept'        => 'application/json',
            'Content-Type'  => 'application/json',
        ])->post($url, $body);

        if ($response->failed()) {
            Log::error('[BC] AssemblyService POST failed', [
                'url' => $url,
                'status' => $response->status(),
                'body' => $response->body(),
            ]);

            return response()->json([
                'success' => false,
                'status'  => $response->status(),
                'message' => 'POST request to AssemblyService failed',
                'response' => $response->body(),
            ], 500);
        }

        return response()->json([
            'success' => true,
            'data' => $response->json(),
        ]);

    } catch (\Throwable $e) {
        Log::error('[BC] Error posting AssemblyService', [
            'message' => $e->getMessage(),
        ]);

        return response()->json([
            'success' => false,
            'message' => $e->getMessage(),
        ], 500);
    }
}



public function postAssemblyOrder($AssemblyNo, $qtyBase)
{
    // URL unbound action ODataV4
    $QtyBase = (float)$qtyBase;
    $url = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/" 
        . env('BC_ENVIRONMENT') . "/" 
        . "ODataV4/ServiceButcher_AssignLot?Company=PT.CI%20LIVE";

    try {
        // Beberapa versi BC membutuhkan parameter via body sebagai object, atau nested
        $payload = new \stdClass(); 
        $payload->assemblyNo = $AssemblyNo;
        $payload->qtyBase = $QtyBase;
        $payload->type       = 'STOCK';
        // POST request menggunakan Laravel HTTP Client
        $response = Http::withHeaders([
            'Authorization' => 'Bearer ' . $this->token,
            'Content-Type'  => 'application/json',
            'Accept'        => 'application/json',
        ])->timeout(180)
          ->post($url, $payload);

        if ($response->failed()) {
            \Log::error('[BC] AssemblyService POST failed', [
                'url' => $url,
                'status' => $response->status(),
                'body' => $response->body(),
            ]);

            return [
                'success' => false,
                'status'  => $response->status(),
                'message' => 'POST request to AssemblyService failed',
                'response' => $response->body(),
            ];
        }

        // Kembalikan sebagai array langsung, bukan JsonResponse
        return [
            'success' => true,
            'message' => 'Assembly Order posted successfully',
            'data'    => $response->json(),
        ];

    } catch (\Throwable $e) {
        \Log::error('[BC] Error posting AssemblyService', [
            'message' => $e->getMessage(),
        ]);

        return [
            'success' => false,
            'message' => $e->getMessage(),
        ];
    }
}



public function getAllAssemblyOrder()
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/assemblyHeaderCustom?\$filter = locCode eq 'CI.1011'";
    // $base = "https://api.businesscentral.dynamics.com/v2.0/"
    //     . env('AZURE_TENANT_ID') . "/"
    //     . env('BC_ENVIRONMENT')
    //     . "/api/citbi/sku/v1.0/companies({$this->companyId})/assemblyHeaderCustom?\$filter = locCode eq 'CI.1012'";
    // $base = "https://api.businesscentral.dynamics.com/v2.0/"
    //     . env('AZURE_TENANT_ID') . "/"
    //     . env('BC_ENVIRONMENT')
    //     . "/api/citbi/sku/v1.0/companies({$this->companyId})/assemblyHeaderCustom?\$filter = locCode eq 'RBC.2040'";
    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->get($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
        ]);

        // Decode hasil response JSON ke array
        $result = json_decode((string) $response->getBody(), true);

        // Ambil hanya bagian "value" jika tersedia (karena struktur BC selalu "value" array)
        $data = $result['value'] ?? $result ?? [];

        return [
            'status' => 'success',
            'data'  => $data,
            'count'  => count($data),
        ];

    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $body = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : $e->getMessage();

        \Log::error('BC Get Assemble Order error (full): ' . $body);

        return [
            'status'  => 'error',
            'message' => $body,
            'items'   => [],
        ];

    } catch (\Exception $e) {
        \Log::error('BC Get Assemble Order general error: ' . $e->getMessage());

        return [
            'status'  => 'error',
            'message' => $e->getMessage(),
            'items'   => [],
        ];
    }
}


public function getAllTransferHeaders()
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/transferHeaderCustom";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->get($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
        ]);

        $result = json_decode((string) $response->getBody(), true);

        return response()->json([
            'status' => 'success',
            'data' => $result,
        ], 200);

    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $body = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : $e->getMessage();

        \Log::error('BC getAllTransferHeaders error (full): ' . $body);

        return response()->json([
            'status' => 'error',
            'message' => $body,
        ], 500);

    } catch (\Exception $e) {
        \Log::error('BC getAllTransferHeaders general error: ' . $e->getMessage());

        return response()->json([
            'status' => 'error',
            'message' => $e->getMessage(),
        ], 500);
    }
}

// Controller
public function getAllTransferLines()
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/transferLineCustom";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->get($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
        ]);

        $result = json_decode((string) $response->getBody(), true);

        return response()->json([
            'status' => 'success',
            'data' => $result,
        ], 200);

    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $body = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : $e->getMessage();

        \Log::error('BC getAllTransferLines error (full): ' . $body);

        return response()->json([
            'status' => 'error',
            'message' => $body,
        ], 500);

    } catch (\Exception $e) {
        \Log::error('BC getAllTransferLines general error: ' . $e->getMessage());

        return response()->json([
            'status' => 'error',
            'message' => $e->getMessage(),
        ], 500);
    }
}

public function getAllTransferWithLines()
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        // =========================================================
        // STEP 1: Ambil semua Transfer Header
        // =========================================================
        $headerUrl = $base . "/transferHeaderCustom";
        $responseHeader = $client->get($headerUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
            ],
        ]);

        $headers = json_decode((string) $responseHeader->getBody(), true);
        $headersData = $headers['value'] ?? $headers ?? [];

        // =========================================================
        // STEP 2: Ambil semua Transfer Line
        // =========================================================
        $lineUrl = $base . "/transferLineCustom";
        $responseLine = $client->get($lineUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
            ],
        ]);

        $lines = json_decode((string) $responseLine->getBody(), true);
        $linesData = $lines['value'] ?? $lines ?? [];

        // =========================================================
        // STEP 3: Filter transSpec → hanya TO.EXT BU & ST. ORDER
        // =========================================================
        $validTransSpec = ['TO.EXT BU', 'ST. ORDER'];

        $filteredByTransSpec = collect($headersData)
            ->filter(fn($h) => in_array(($h['transSpec'] ?? ''), $validTransSpec, true))
            ->values()
            ->all();

        // =========================================================
        // STEP 4: Filter transfer-to ke semua RBC location (2100–2140)
        // =========================================================
        $validLocations = ['RBC.2100', 'RBC.2110', 'RBC.2120', 'RBC.2130', 'RBC.2140'];

        // ❗Status tidak difilter lagi
        $filteredHeaders = collect($filteredByTransSpec)
            ->filter(fn($h) => in_array(($h['transfertoCode'] ?? ''), $validLocations, true))
            ->values()
            ->all();

        // =========================================================
        // STEP 5: Extract header no
        // =========================================================
        $filteredHeaderNos = collect($filteredHeaders)
            ->pluck('no')
            ->values()
            ->all();

        // =========================================================
        // STEP 6: Filter only lines belonging to these headers
        // =========================================================
        $filteredLines = collect($linesData)
            ->filter(fn($line) => in_array(($line['documentNo'] ?? ''), $filteredHeaderNos))
            ->values()
            ->all();

        // =========================================================
        // STEP 7: Inject filtered lines into header
        // =========================================================
        $headersWithLines = collect($filteredHeaders)
            ->map(function ($header) use ($filteredLines) {

                $headerNo = $header['no'] ?? null;

                $linesForHeader = collect($filteredLines)
                    ->filter(fn($l) => ($l['documentNo'] ?? '') === $headerNo)
                    ->values()
                    ->all();

                $header['Lines'] = $linesForHeader;
                return $header;
            })
            ->values()
            ->all();

        // =========================================================
        // STEP 8: Hanya header yg punya line dengan qty != qtyShip
        // =========================================================
        $filteredIncomplete = collect($headersWithLines)
            ->map(function ($header) {

                $remaining = collect($header['Lines'] ?? [])
                    ->filter(function ($l) {
                        $qty = (float) ($l['quantity'] ?? 0);
                        $qtyShip = (float) ($l['qtyShip'] ?? 0);
                        return $qty !== $qtyShip;
                    })
                    ->values()
                    ->all();

                $header['Lines'] = $remaining;
                return $header;
            })
            ->filter(fn($h) => count($h['Lines']) > 0)
            ->values()
            ->all();

        // =========================================================
        // STEP 9: Return JSON
        // =========================================================
        return response()->json([
            'status' => 'success',
            'count'  => count($filteredIncomplete),
            'data'   => $filteredIncomplete,
        ], 200);

    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $body = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : $e->getMessage();

        \Log::error('❌ BC getAllTransferWithLines RequestException: ' . $body);

        return response()->json([
            'status'  => 'error',
            'message' => $body,
        ], 500);

    } catch (\Exception $e) {
        \Log::error('❌ BC getAllTransferWithLines General error: ' . $e->getMessage());

        return response()->json([
            'status'  => 'error',
            'message' => $e->getMessage(),
        ], 500);
    }
}



public function getAllTransferWithLinesV2()
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        // =========================================================
        // STEP 1: Ambil semua Transfer Header
        // =========================================================
        $headerUrl = $base . "/transferHeaderCustom";
        $responseHeader = $client->get($headerUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
            ],
        ]);

        $headers = json_decode((string) $responseHeader->getBody(), true);
        $headersData = $headers['value'] ?? $headers ?? [];

        // =========================================================
        // STEP 2: Ambil semua Transfer Line
        // =========================================================
        $lineUrl = $base . "/transferLineCustom";
        $responseLine = $client->get($lineUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
            ],
        ]);

        $lines = json_decode((string) $responseLine->getBody(), true);
        $linesData = $lines['value'] ?? $lines ?? [];

        // =========================================================
        // STEP 3: Filter header hanya yang transferFromCode = RBC.2040
        // =========================================================
        $filteredHeaders = collect($headersData)
            ->filter(fn($h) => 
                ($h['transfertoCode'] ?? '') === 'CI.1011' 
               
            )
            ->values()
            ->all();
        // $filteredHeaders = collect($headersData)
        //     ->filter(fn($h) => 
        //         ($h['transfertoCode'] ?? '') === 'CI.1012' 
        //         && ($h['status'] ?? '') === 'Released'
        //     )
        //     ->values()
        //     ->all();
        // $filteredHeaders = collect($headersData)
        //     ->filter(fn($h) => 
        //         ($h['transfertoCode'] ?? '') === 'RBC.2040' 
        //         && ($h['status'] ?? '') === 'Released'
        //     )
        //     ->values()
        //     ->all();
        $filteredHeaderNos = collect($filteredHeaders)
            ->pluck('no')
            ->values()
            ->all();

        // =========================================================
        // STEP 4: Ambil hanya line yang documentNo ada di header RBC.2040
        // =========================================================
        $filteredLines = collect($linesData)
            ->filter(fn($line) => in_array($line['documentNo'] ?? '', $filteredHeaderNos))
            ->values()
            ->all();

        // =========================================================
        // STEP 5: Bungkus line ke masing-masing header sebagai field "Lines"
        // =========================================================
        $headersWithLines = collect($filteredHeaders)
            ->map(function ($header) use ($filteredLines) {
                $headerNo = $header['no'] ?? null;
                $linesForHeader = collect($filteredLines)
                    ->filter(fn($l) => ($l['documentNo'] ?? '') === $headerNo)
                    ->values()
                    ->all();

                $header['Lines'] = $linesForHeader;
                return $header;
            })
            ->values()
            ->all();

        // =========================================================
        // STEP 6: Hanya tampilkan header yang punya minimal 1 line dengan quantity != qtyShip
        // Note: pengecekan menggunakan `qtyShip` (bukan qtyToShip)
        // =========================================================
        $filteredIncomplete = collect($headersWithLines)
            ->map(function ($header) {
                // Sisakan hanya line yang quantity != qtyShip
                $remainingLines = collect($header['Lines'] ?? [])
                    ->filter(function ($line) {
                        $qty = (float) ($line['quantity'] ?? 0);
                        $qtyShip = (float) ($line['qtyShip'] ?? $line['qtyShip'] ?? 0);
                        // tampilkan line jika belum setara (quantity != qtyShip)
                        return $qty !== $qtyShip;
                    })
                    ->values()
                    ->all();

                $header['Lines'] = $remainingLines;
                return $header;
            })
            // buang header yang semua line sudah setara (jadi Lines menjadi kosong)
            ->filter(fn($h) => count($h['Lines']) > 0)
            ->values()
            ->all();

        // =========================================================
        // STEP 7: Return hasil final
        // =========================================================
        return response()->json([
            'status' => 'success',
            'count'  => count($filteredIncomplete),
            'data'   => $filteredIncomplete,
        ], 200);

    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $body = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : $e->getMessage();

        \Log::error('❌ BC getAllTransferWithLines RequestException: ' . $body);

        return response()->json([
            'status'  => 'error',
            'message' => $body,
        ], 500);

    } catch (\Exception $e) {
        \Log::error('❌ BC getAllTransferWithLines General error: ' . $e->getMessage());

        return response()->json([
            'status'  => 'error',
            'message' => $e->getMessage(),
        ], 500);
    }
}



public function shipSelectedService(array $headers)
{
    $patched = [];
    $posted  = [];
    $errors  = [];

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    foreach ($headers as $header) {
        $headerNo = $header['no'] ?? null;
        $lines    = $header['Lines'] ?? [];

        foreach ($lines as $line) {
            try {
                $documentNo = $line['documentNo'] ?? null;
                $lineNo     = $line['lineNo'] ?? null;
                $etag       = $line['@odata.etag'] ?? null;
                $quantityShip = $line['quantityShip'] ?? null;

                if (!$documentNo || !$lineNo || !$etag) {
                    throw new \Exception("Missing documentNo / lineNo / etag for header {$headerNo}");
                }

                $patchUrl = "https://api.businesscentral.dynamics.com/v2.0/"
                    . env('AZURE_TENANT_ID') . "/"
                    . env('BC_ENVIRONMENT') . "/api/citbi/sku/v1.0/companies({$this->companyId})/transferLineCustom(documentNo='{$documentNo}',lineNo={$lineNo})";

                $patchPayload = [
                    'qtyToShip' => (float) $quantityShip
                ];
                $response = $client->patch($patchUrl, [
                    'headers' => [
                        'Authorization' => "Bearer {$this->token}",
                        'Accept'        => 'application/json',
                        'If-Match'      => $etag,
                        'Content-Type'  => 'application/json',
                    ],
                    'json' => $patchPayload,
                ]);

                $patchResult = json_decode((string) $response->getBody(), true);
                $patched[] = [
                    'headerNo' => $headerNo,
                    'documentNo' => $documentNo,
                    'result'   => $patchResult,
                ];

            } catch (\Throwable $e) {
                $errors[] = [
                    'headerNo' => $headerNo,
                    'documentNo' => $line['documentNo'] ?? null,
                    'step'     => 'PATCH',
                    'message'  => $e->getMessage(),
                ];
                Log::error($errors);
            }
        }

        // Setelah semua line di header di PATCH, lakukan POST
        try {
            $transferNo = $header['no'] ?? null;
            if (!$transferNo) throw new \Exception("Missing TransferNo for POST");

            $postUrl = "https://api.businesscentral.dynamics.com/v2.0/"
                . env('AZURE_TENANT_ID') . "/"
                . env('BC_ENVIRONMENT') . "/ODataV4/TOService_PostTransferShipmentByNo?Company=PT.CI%20LIVE";

            $payload = new \stdClass();
            $payload->transferNo = $transferNo;
            
            $response = \Illuminate\Support\Facades\Http::withHeaders([
                'Authorization' => 'Bearer ' . $this->token,
                'Content-Type'  => 'application/json',
                'Accept'        => 'application/json',
            ])->timeout(180)
              ->post($postUrl, $payload);
            Log::error($response);
            if ($response->failed()) {
                throw new \Exception("POST shipment failed, status {$response->status()}: " . $response->body());
            }

            $posted[] = [
                'headerNo' => $headerNo,
                'transferNo' => $transferNo,
                'response' => $response->json(),
            ];

        } catch (\Throwable $e) {
            $errors[] = [
                'headerNo' => $headerNo,
                'step'     => 'POST',
                'message'  => $e->getMessage(),
            ];
        }
    }

    return [
        'status'  => empty($errors) ? 'success' : 'partial',
        'patched' => $patched,
        'posted'  => $posted,
        'errors'  => $errors,
    ];
}


public function createAssemblyHeader(array $data)
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/assemblyHeaderCustom";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    // =========================================================
    // STEP 1: POST HEADER
    // =========================================================
    try {
        $postPayload = [
            'docType' => 'Order',
            'docNo'   => '',
        ];

        $response = $client->post($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Content-Type'  => 'application/json',
            ],
            'json' => $postPayload,
        ]);

        $created = json_decode((string) $response->getBody(), true);
        Log::info('✅ STEP 1: Assembly Header Created', $created);

        $docType = $created['docType'] ?? 'Order';
        $docNo   = $created['docNo'] ?? null;
        $etag    = $created['@odata.etag'] ?? null;

        if (!$docNo || !$etag) {
            throw new \Exception('docNo atau ETag tidak ditemukan di response POST.');
        }

    } catch (\Exception $e) {
        Log::error('❌ STEP 1 Failed: POST Assembly Header', ['error' => $e->getMessage()]);
        return [
            'status'  => 'error',
            'step'    => 'POST Header',
            'message' => $e->getMessage(),
        ];
    }

    // PATCH URL
    $patchUrl = $base . "(docType='" . rawurlencode($docType) . "',docNo='" . rawurlencode($docNo) . "')";

    // =========================================================
    // STEP 2: PATCH itemNo
    // =========================================================
    try {
        $itemPayload = [
            'itemNo' => $data['itemNo'] ?? null,
        ];

        $patch1 = $client->patch($patchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'If-Match'      => $etag,
                'Content-Type'  => 'application/json',
            ],
            'json' => $itemPayload,
        ]);

        $patch1Data = json_decode((string) $patch1->getBody(), true);
        Log::info("✅ STEP 2: itemNo patched", $itemPayload);

        $etag = $patch1Data['@odata.etag'] ?? $etag;

    } catch (\Exception $e) {
        Log::error("❌ STEP 2 Failed: PATCH itemNo", ['error' => $e->getMessage()]);
        return [
            'status'  => 'error',
            'step'    => 'PATCH itemNo',
            'message' => $e->getMessage(),
        ];
    }

    // =========================================================
    // STEP 3: PATCH postingDate
    // =========================================================
    try {
        $postingDate = $data['postingDate'] ?? now()->format('Y-m-d');

        $postingPayload = [
            'postingDate' => $postingDate,
        ];

        $patch2 = $client->patch($patchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'If-Match'      => $etag,
                'Content-Type'  => 'application/json',
            ],
            'json' => $postingPayload,
        ]);

        $patch2Data = json_decode((string) $patch2->getBody(), true);
        Log::info("✅ STEP 3: postingDate patched", $postingPayload);

        $etag = $patch2Data['@odata.etag'] ?? $etag;

    } catch (\Exception $e) {
        Log::error("❌ STEP 3 Failed: PATCH postingDate", ['error' => $e->getMessage()]);
        return [
            'status'  => 'error',
            'step'    => 'PATCH postingDate',
            'message' => $e->getMessage(),
        ];
    }

    // =========================================================
    // STEP 4: PATCH quantity
    // =========================================================
    try {
        $qtyPayload = [
            'quantity' => (float) ($data['quantity'] ?? 0),
        ];

        $patch3 = $client->patch($patchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'If-Match'      => $etag,
                'Content-Type'  => 'application/json',
            ],
            'json' => $qtyPayload,
        ]);

        $patch3Data = json_decode((string) $patch3->getBody(), true);
        Log::info("✅ STEP 4: quantity patched", $qtyPayload);

        $etag = $patch3Data['@odata.etag'] ?? $etag;

    } catch (\Exception $e) {
        Log::error("❌ STEP 4 Failed: PATCH quantity", ['error' => $e->getMessage()]);
        return [
            'status'  => 'error',
            'step'    => 'PATCH quantity',
            'message' => $e->getMessage(),
        ];
    }


    // =========================================================
    // STEP 5: PATCH locCode
    // =========================================================
    try {
        $locPayload = [
            'locCode' => $data['locCode'] ?? 'CI.1011'
        ];

        $patch4 = $client->patch($patchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'If-Match'      => $etag,
                'Content-Type'  => 'application/json',
            ],
            'json' => $locPayload,
        ]);

        $finalData = json_decode((string) $patch4->getBody(), true);
        Log::info("✅ STEP 5: locCode patched", $locPayload);

        $etag = $finalData['@odata.etag'] ?? $etag;

    } catch (\Exception $e) {
        Log::error("❌ STEP 5 Failed: PATCH locCode", ['error' => $e->getMessage()]);
        return [
            'status'  => 'error',
            'step'    => 'PATCH locCode',
            'message' => $e->getMessage(),
        ];
    }


    // =========================================================
    // STEP 6 (NEW): PATCH quantityToAssemble
    // =========================================================
    try {
        $qtyAssemblePayload = [
            'quantityToAssemble' => (float) ($data['quantity'] ?? 0),
        ];

        $patch6 = $client->patch($patchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'If-Match'      => $etag,
                'Content-Type'  => 'application/json',
            ],
            'json' => $qtyAssemblePayload,
        ]);

        $patch6Data = json_decode((string) $patch6->getBody(), true);
        Log::info("✅ STEP 6: quantityToAssemble patched", $qtyAssemblePayload);

        $etag = $patch6Data['@odata.etag'] ?? $etag;

    } catch (\Exception $e) {
        Log::error("❌ STEP 6 Failed: PATCH quantityToAssemble", ['error' => $e->getMessage()]);
        return [
            'status'  => 'error',
            'step'    => 'PATCH quantityToAssemble',
            'message' => $e->getMessage(),
        ];
    }


    // =========================================================
    // DONE
    // =========================================================
    return [
        'status'  => 'success',
        'data'    => $finalData,
        'message' => "Assembly Header {$docNo} berhasil dibuat & semua field berhasil dipatch.",
    ];
}



public function createTransferHeader(array $data)
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/transferHeaderCustomHotel";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    // =========================================================
    // STEP 1: POST → Create Header
    // =========================================================
    \Log::error('STEP 1: Creating BC Transfer Header', $data);

    $createPayload = [
        'noSeries' => $data['noSeries'] ?? null,
        'no' => ''
    ];

    $response = $client->post($base, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'Content-Type' => 'application/json',
        ],
        'json' => $createPayload,
    ]);

    $created = json_decode((string)$response->getBody(), true);
    \Log::info('STEP 1 RESULT:', $created);

    $no   = $created['no'] ?? null;
    $etag = $created['@odata.etag'] ?? null;

    if (!$no) {
        \Log::error('STEP 1 ERROR: Tidak ada nomor header pada response.', ['response' => $created]);
        throw new \Exception('Nomor transfer header tidak ditemukan di response.');
    }

    if (!$etag) {
        \Log::error('STEP 1 ERROR: Tidak ada ETag pada response.', ['response' => $created]);
        throw new \Exception('ETag tidak ditemukan di response.');
    }

    $patchUrl = $base . "(no='" . rawurlencode($no) . "')";

    // =========================================================
    // STEP 2: PATCH transSpec
    // =========================================================
    \Log::info("STEP 2: PATCH transSpec untuk {$no}");

    $transSpecPayload = [
        'transSpec' => $data['transSpec'] ?? null,
    ];

    $patch1 = $client->patch($patchUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag,
            'Content-Type' => 'application/json',
        ],
        'json' => $transSpecPayload,
    ]);

    $patch1Data = json_decode((string)$patch1->getBody(), true);
    $etag = $patch1Data['@odata.etag'] ?? $etag;
    \Log::info('STEP 2 RESULT:', $patch1Data);

    // =========================================================
    // STEP 3: PATCH transferFromCode + transfertoCode
    // =========================================================
    \Log::info("STEP 3: PATCH transferFromCode & transfertoCode untuk {$no}");

    $transferPayload = [
        'transferFromCode' => $data['transferFromCode'] ?? null,
        'transfertoCode'   => $data['transfertoCode'] ?? null,
    ];

    $patch2 = $client->patch($patchUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag,
            'Content-Type' => 'application/json',
        ],
        'json' => $transferPayload,
    ]);

    $patch2Data = json_decode((string)$patch2->getBody(), true);
    $etag = $patch2Data['@odata.etag'] ?? $etag;
    \Log::info('STEP 3 RESULT:', $patch2Data);

    // =========================================================
    // STEP 4: PATCH postingDate (tanggal hari ini)
    // =========================================================
    \Log::info("STEP 4: PATCH postingDate untuk {$no}");

    $postingDate = now()->format('Y-m-d');
    $postingPayload = [
        'postingDate' => $postingDate,
    ];

    $patch3 = $client->patch($patchUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag,
            'Content-Type' => 'application/json',
        ],
        'json' => $postingPayload,
    ]);

    $patch3Data = json_decode((string)$patch3->getBody(), true);
    $etag = $patch3Data['@odata.etag'] ?? $etag;
    \Log::info('STEP 4 RESULT:', $patch3Data);

    // =========================================================
    // STEP 5: PATCH businessUnit
    // =========================================================
    \Log::info("STEP 5: PATCH businessUnit untuk {$no}");

    $businessUnitPayload = [
        'businessUnit' => $data['businessUnit'] ?? null,
    ];

    $patch4 = $client->patch($patchUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag,
            'Content-Type' => 'application/json',
        ],
        'json' => $businessUnitPayload,
    ]);

    $patch4Data = json_decode((string)$patch4->getBody(), true);
    $etag = $patch4Data['@odata.etag'] ?? $etag;
    \Log::info('STEP 5 RESULT:', $patch4Data);

    // =========================================================
    // STEP 6: PATCH transferType  (BARU)
    // =========================================================
    \Log::info("STEP 6: PATCH transferType untuk {$no}");

    $transferTypePayload = [
        'transType' => $data['transType'] ?? null,
        // 'transType' => $data['transType'] ?? null,
    ];
    Log::error($data);
    $patch6 = $client->patch($patchUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag,
            'Content-Type' => 'application/json',
        ],
        'json' => $transferTypePayload,
    ]);

    $patch6Data = json_decode((string)$patch6->getBody(), true);
    $etag = $patch6Data['@odata.etag'] ?? $etag;
    \Log::info('STEP 6 RESULT:', $patch6Data);

    // =========================================================
    // STEP 7: PATCH department (TERAKHIR sebelum buat LINE)
    // =========================================================
    \Log::info("STEP 7: PATCH department untuk {$no}");

    $departmentPayload = [
        'department' => $data['department'] ?? '',
    ];

    $patch7 = $client->patch($patchUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag,
            'Content-Type' => 'application/json',
        ],
        'json' => $departmentPayload,
    ]);

    $finalData = json_decode((string)$patch7->getBody(), true);
    $etag = $finalData['@odata.etag'] ?? $etag;
    \Log::error('STEP 7 RESULT:', $finalData);

    // =========================================================
    // STEP 8: CREATE TRANSFER LINES (tetap bagian terakhir)
    // =========================================================
    $lineBase = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/transferLineCustom";

    $lines = $data['lines'] ?? [];
    $lineCounter = 10000;
    $createdLines = [];

    foreach ($lines as $idx => $line) {

        \Log::info("CREATE LINE {$idx} untuk DocumentNo {$no}");

        // STEP 8.1 POST line
        $linePayload = [
            'documentNo' => $no,
            'lineNo' => $lineCounter,
        ];

        $res = $client->post($lineBase, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'Content-Type' => 'application/json',
            ],
            'json' => $linePayload,
        ]);

        $createdLine = json_decode((string)$res->getBody(), true);
        $lineEtag = $createdLine['@odata.etag'] ?? null;

        if (!isset($createdLine['documentNo'])) {
            \Log::error("Gagal membuat line {$idx}", ['response' => $createdLine]);
            continue;
        }

        $linePatchUrl = $lineBase . "(documentNo='" . rawurlencode($no) . "',lineNo=" . $lineCounter . ")";

        // STEP 8.2 PATCH itemNo
        $patchItem = $client->patch($linePatchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'If-Match' => $lineEtag,
                'Content-Type' => 'application/json',
            ],
            'json' => ['itemNo' => $line['itemNo'] ?? null],
        ]);

        $patched1 = json_decode((string)$patchItem->getBody(), true);
        $lineEtag = $patched1['@odata.etag'] ?? $lineEtag;

        // STEP 8.3 PATCH quantity
        $patchQty = $client->patch($linePatchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'If-Match' => $lineEtag,
                'Content-Type' => 'application/json',
            ],
            'json' => ['quantity' => $line['quantity'] ?? 0],
        ]);

        $patched2 = json_decode((string)$patchQty->getBody(), true);
        $lineEtag = $patched2['@odata.etag'] ?? $lineEtag;

        // STEP 8.4 PATCH uomCode
        try {
            $patchUom = $client->patch($linePatchUrl, [
                'headers' => [
                    'Authorization' => "Bearer {$this->token}",
                    'Accept' => 'application/json',
                    'If-Match' => $lineEtag,
                    'Content-Type' => 'application/json',
                ],
                'json' => ['uomCode' => $line['uomCode'] ?? null],
            ]);

            $patched3 = json_decode((string)$patchUom->getBody(), true);
            $createdLines[] = $patched3;

        } catch (RequestException $e) {

            $errorBody = null;

            if ($e->hasResponse()) {
                $errorBody = (string) $e->getResponse()->getBody();
            }

            \Log::error("BC ERROR PATCH UOM", [
                'documentNo' => $no,
                'lineNo'     => $lineCounter,
                'itemNo'     => $line['itemNo'] ?? null,
                'uomCode'    => $line['uomCode'] ?? null,
                'status'     => $e->getCode(),
                'response'   => $errorBody,
            ]);

            throw $e; // optional: kalau mau stop proses
        }

        $lineCounter += 10000;
    }

    // =========================================================
    // DONE
    // =========================================================
    return [
        'status' => 'success',
        'header' => $finalData,
        'lines' => $createdLines,
        'message' => "Transfer Header {$no} dan " . count($createdLines) . " line berhasil dibuat.",
    ];
}




public function createVendor(array $vendorData): array
{
    // =====================
    // 1️⃣ Buat Vendor
    // =====================
    $url = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT') . "/api/v2.0/companies({$this->companyId})/vendors";

    $payload = [];
    if (!empty($vendorData['number'])) $payload['number'] = $vendorData['number'];
    if (!empty($vendorData['displayName'])) $payload['displayName'] = $vendorData['displayName'];
    if (!empty($vendorData['addressLine1'])) $payload['addressLine1'] = $vendorData['addressLine1'];
    if (!empty($vendorData['addressLine2'])) $payload['addressLine2'] = $vendorData['addressLine2'];
    if (!empty($vendorData['city'])) $payload['city'] = $vendorData['city'];
    if (!empty($vendorData['state'])) $payload['state'] = $vendorData['state'];
    if (!empty($vendorData['country'])) $payload['country'] = $vendorData['country'];
    if (!empty($vendorData['postalCode'])) $payload['postalCode'] = $vendorData['postalCode'];
    if (!empty($vendorData['phoneNumber'])) $payload['phoneNumber'] = $vendorData['phoneNumber'];
    if (!empty($vendorData['email'])) $payload['email'] = $vendorData['email'];
    if (!empty($vendorData['website'])) $payload['website'] = $vendorData['website'];
    if (!empty($vendorData['taxRegistrationNumber'])) $payload['taxRegistrationNumber'] = $vendorData['taxRegistrationNumber'];
    if (!empty($vendorData['currencyId']) && $vendorData['currencyId'] !== '00000000-0000-0000-0000-000000000000') $payload['currencyId'] = $vendorData['currencyId'];
    if (!empty($vendorData['paymentTermsId']) && $vendorData['paymentTermsId'] !== '00000000-0000-0000-0000-000000000000') $payload['paymentTermsId'] = $vendorData['paymentTermsId'];
    if (!empty($vendorData['paymentMethodId']) && $vendorData['paymentMethodId'] !== '00000000-0000-0000-0000-000000000000') $payload['paymentMethodId'] = $vendorData['paymentMethodId'];
    if (isset($vendorData['taxLiable'])) $payload['taxLiable'] = (bool)$vendorData['taxLiable'];
    if (isset($vendorData['blocked'])) $payload['blocked'] = $vendorData['blocked'];

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->post($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
            'json' => $payload,
        ]);

        $vendorCreated = json_decode((string)$response->getBody(), true);
    } catch (\Exception $e) {
        \Log::error('BC createVendor error: ' . $e->getMessage(), ['payload' => $payload]);
        return [
            'status' => 'error',
            'message' => $e->getMessage(),
        ];
    }

    // =====================
    // 2️⃣ Buat PriceListHeaderCustom
    // =====================
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT');
$root = $base . "/api/citbi/sku/v1.0";
$priceUrl = "{$root}/companies({$this->companyId})/priceListHeaderCustom";

$client = new \GuzzleHttp\Client([
    'timeout' => 180,
    'connect_timeout' => 20,
]);
$etag = $priceCreated['@odata.etag'] ?? null;
$priceCode = $vendorCreated['number'] ?? $vendorData['number'];
// ===================
// Tahap 1: Create PriceListHeaderCustom (priceCode + Description)
// ===================
$createPayload = [
    'priceCode' => $vendorCreated['number'] ?? $vendorData['number'],
    'Description' => $vendorCreated['displayName'] ?? $vendorData['displayName'],
];

try {
    $response = $client->post($priceUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag ?? '*',
        ],
        'json' => $createPayload,
    ]);

    $priceCreated = json_decode((string)$response->getBody(), true);
    $priceCode = $priceCreated['priceCode'] ?? $createPayload['priceCode'];
} catch (\Exception $e) {
    \Log::error('BC createVendorPrice (create) error: ' . $e->getMessage(), [
        'payload' => $createPayload
    ]);
    $priceCreated = null;
}

// ===================
// Tahap 2: Patch sourceType
// ===================
if ($priceCode) {
    try {
        $client->patch("{$priceUrl}('{$priceCode}')", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'If-Match' => $etag ?? '*',
            ],
            'json' => [
                'sourceType' => 'Vendor'
            ]
        ]);
    } catch (\Exception $e) {
        \Log::error('BC createVendorPrice (patch sourceType) error: ' . $e->getMessage());
    }
}

// ===================
// Tahap 3: Patch sourceNo
// ===================
if ($priceCode) {
    try {
        $client->patch("{$priceUrl}('{$priceCode}')", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'If-Match' => $etag ?? '*',
            ],
            'json' => [
                'sourceNo' => $vendorCreated['number'] ?? $vendorData['number']
            ]
        ]);
    } catch (\Exception $e) {
        \Log::error('BC createVendorPrice (patch sourceNo) error: ' . $e->getMessage());
    }
}

// ===================
// Tahap 4: Patch status
// ===================
if ($priceCode) {
    try {
        $client->patch("{$priceUrl}('{$priceCode}')", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'If-Match' => $etag ?? '*',
            ],
            'json' => [
                'status' => 'Active'
            ]
        ]);
    } catch (\Exception $e) {
        \Log::error('BC createVendorPrice (patch status) error: ' . $e->getMessage());
    }
}

    return [
        'status' => 'success',
        'data' => [
            'vendor' => $vendorCreated,
            'priceListHeaderCustom' => $priceCreated
        ]
    ];
}


    public function getNoSeries()
{
    $url = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/ODataV4/Company('" . $this->companyId . "')/seriesNo";

    try {
        $response = $this->client->get($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ]
        ]);
        \Log::error($this->companyId);

        $data = json_decode((string)$response->getBody(), true);
        return [
            'status' => 'success',
            'data' => $data
        ];
    } catch (\Exception $e) {
        \Log::error('BC getNoSeries error: ' . $e->getMessage(), [
            'url' => $url,
            'exception' => $e
        ]);

        return [
            'status' => 'error',
            'message' => $e->getMessage()
        ];
    }
}
        


    function getPOFromBC() {

        $response = $this->client->get(
            "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/api/v2.0/companies({$this->companyId})/purchaseOrders?\$filter=number eq '30858'",
            [
                'headers' => [
                    'Authorization' => "Bearer {$this->token}",
                    'Accept'        => 'application/json'
                ]
            ]
        );

        return json_decode((string)$response->getBody(), true);
    }

    public function getTransferOrderSuggestions($userName)
        {
            $start = microtime(true);
            $stockkeeping = $purchaseRaw = $transferRaw = $priceRaw = $vendorRaw = [];
            $baseOdata = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/ODataV4/Company('" . $this->companyId . "')";
            $baseApi = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/api/v2.0/companies({$this->companyId})";
            $activeCompany = session('user')['role'];
            if ($activeCompany === 'TARU') {
                $transferLocationCode = "(Transfer_to_Code eq 'RBC.2100')";
            } else if ($activeCompany === 'BEACH HOUSE') {
                $transferLocationCode = "(Transfer_from_Code eq 'HIN.1000' or Transfer_from_Code eq 'HIN.1200' or Transfer_from_Code eq 'HIN.1300' or Transfer_from_Code eq 'HIN.3000')";
            }
            $headers = [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer'        => 'odata.maxpagesize=10000'
            ];
            
            $prefixes = ['RBC.2100', 'RBC.2110', 'RBC.2120','RBC.2130','RBC.2140', 'HIC.1000', 'HIC.1200'];
            $itemInvenLocPromises = [];

           $locationFilter = "Location_Code eq 'RBC.2100'";
           $hotelStockFitler = "LocationCode eq 'RBC.2100'";

            // $url = "https://api.businesscentral.dynamics.com/v2.0/" 
            // . env('AZURE_TENANT_ID') . "/" 
            // . env('BC_ENVIRONMENT')
            // . "/ODataV4/Company('" . $this->companyId . "')/BudgetLost?\$filter={$filter}";

            $TARU = $this->getStockV3();

            // $itemInvenLocPromises['TARU'] = $this->client->getAsync(
            //     "$baseOdata/HotelStock",
            //     ['headers' => $headers]
            // );


            // foreach ($prefixes as $prefix) {
            //     $itemInvenLocPromises[$prefix] = $this->client->getAsync(
            //         "$baseOdata/HotelStock?\$filter=LocationCode eq '$prefix'",
            //         ['headers' => $headers]
            //     );
            // }

            $purchaseFilter = "
                Outstanding_Quantity gt 0
                and (Status eq 'Released' or Status eq 'Pending Approval' or Status eq 'Pending Prepayment')
                and Location_Code eq 'RBC.2100'
            ";

            $openFilter = "
                Outstanding_Quantity gt 0
                and Status eq 'Open'
                and Location_Code eq 'RBC.2100'
            ";

            $promises = [
                'stockkeeping' => $this->client->getAsync(
                    "$baseOdata/APIStockkeeping?\$filter=" . urlencode($locationFilter),
                    ['headers' => $headers]
                ),
                'transferQtys' => $this->client->getAsync(
                    "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
                    "/ODataV4/Company('" . $this->companyId . "')/Transfer_Order_Line_Excel?" .
                    "\$filter=" . urlencode("Status eq 'Released' and Quantity_Shipped lt Quantity and (Transfer_to_Code eq 'RBC.2100')") .
                    "&\$select=Document_No,Item_No,Transfer_TO_Code,Quantity_Shipped,Quantity,OutstandingShipped,Shipment_Date,Description,Unit_of_Measure_Code,Status",
                    ['headers' => $headers]
                ),
                'priceLists' => $this->client->getAsync(
                    "$baseOdata/Price_List_Lines?" .
                    '$filter=' . urlencode("SourceNo ne null and StartingDate le " . now()->toDateString() . " and EndingDate ge " . now()->toDateString()) .
                    '&$select=Asset_No,Product_No,Unit_of_Measure_Code,DirectUnitCost,SourceNo',
                    ['headers' => $headers]
                ),
                'vendors' => $this->client->getAsync(
                    "$baseApi/vendors?\$select=number,displayName",
                    ['headers' => $headers]
                ),
                'itemUoM' => $this->client->getAsync(
                    "$baseOdata/Item_UOM?\$select=Item_No,Code,Qty_per_Unit_of_Measure",
                    ['headers' => $headers]
                )
            ];
            $promises = array_merge($promises);
            $responses = Utils::settle($promises)->wait();
            $elapsed = microtime(true) - $start;
            Log::debug('Fetch Runtime:', ['seconds' => $elapsed]);
            if ($responses['stockkeeping']['state'] === 'fulfilled') {
                $stockkeeping = json_decode($responses['stockkeeping']['value']->getBody()->getContents(), true)['value'] ?? [];
            }
            if ($responses['transferQtys']['state'] === 'fulfilled') {
                $transferRaw = json_decode($responses['transferQtys']['value']->getBody()->getContents(), true)['value'] ?? [];
            }
            if ($responses['priceLists']['state'] === 'fulfilled') {
                $priceRaw = json_decode($responses['priceLists']['value']->getBody()->getContents(), true)['value'] ?? [];
            }
            $itemUoMMap = [];
            if ($responses['itemUoM']['state'] === 'fulfilled') {
                $itemUoMRaw = json_decode($responses['itemUoM']['value']->getBody()->getContents(), true)['value'] ?? [];
                foreach ($itemUoMRaw as $uom) {
                    $itemNo = $uom['Item_No'];
                    $uomCode = $uom['Code'];
                    $qtyPerUOM = $uom['Qty_per_Unit_of_Measure'] ?? 1;
                    $itemUoMMap[$itemNo][$uomCode] = $qtyPerUOM;
                }
            }
            $TARU = $TARU["items_by_loc"];
            // foreach (['FO', 'BV', 'HO', 'MT', 'BG'] as $prefix) {
            //     if (isset($responses[$prefix]['state']) && $responses[$prefix]['state'] === 'fulfilled') {
            //         $body = json_decode($responses[$prefix]['value']->getBody()->getContents(), true);
            //         $ItemInvenLoc = array_merge($ItemInvenLoc, $body['value']);
            //     }
            // }
            if ($responses['vendors']['state'] === 'fulfilled') {
                $vendorRaw = json_decode($responses['vendors']['value']->getBody()->getContents(), true)['value'] ?? [];
            }
            $purchaseQtys = [];
            $purchaseDetailsMap = [];   
            foreach ($purchaseRaw as $line) {
                if (strpos($line['Document_No'], 'PRO') !== false) {
                    continue;
                }
                $uomCode = $line['Unit_of_Measure_Code'];
                $multiplier = $itemUoMMap[$line['No']][$uomCode] ?? 1;
                $qtyBaseUoM = $line['Outstanding_Quantity'] * $multiplier;
                $key = $line['No'] . '_' . $line['Location_Code'];
                $purchaseQtys[$key] = ($purchaseQtys[$key] ?? 0) + $qtyBaseUoM;

                $purchaseDetailsMap[$key][] = [
                    'document_no' => $line['Document_No'],
                    'approved_date' => isset($line['Approved_Date']) ? date('d-m-Y', strtotime($line['Approved_Date'])) : null,
                    'status' => $line['Status'],
                    'outstanding_qty' => $qtyBaseUoM, 
                    'uom'             => $uomCode,
                    'multiplier'      => $multiplier,
                    'vendor' => $line['Buy_from_Vendor_Name']
                ];
            }
            $transferQtys = [];
            $transferDates = [];
            $transferDetailsMap = [];
            foreach ($transferRaw as $line) {
                $itemNo = $line['Item_No'];
                $location = $line['Transfer_to_Code'];
                $multiplier = $itemUoMMap[$line['Item_No']][$uomCode] ?? 1;
                if (!in_array($location, ['RBC.2100'])) {
                    continue;
                }
                $key = $line['Item_No'] . '_' . $line['Transfer_to_Code'];
                $outstanding = $line['OutstandingShipped'] ?? 0;
                $outstandingBaseUoM = ($line['OutstandingShipped'] ?? 0) * $multiplier;


                if ($outstanding > 0) {
                    $transferQtys[$key] = ($transferQtys[$key] ?? 0) + $outstandingBaseUoM;
                    $transferDetailsMap[$itemNo . '_' . $location][] = [
                        'document_no' => $line['Document_No'],
                        'shipment_date' => $line['Shipment_Date'],
                        'quantity'      => $outstandingBaseUoM,
                        'uom'           => $uomCode,
                        'multiplier'    => $multiplier
                    ];
                    if (!empty($line['Shipment_Date'])) {
                        $date = $line['Shipment_Date'];
                        if (!isset($transferDates[$key]) || $date < $transferDates[$key]) {
                            $transferDates[$key] = $date;
                        }
                    }
                }
            }
            $priceMap = [];
            $unitCostsByItem = []; 
            foreach ($priceRaw as $line) {
                $itemNo   = $line['Asset_No'] ?? null;
                $vendorNo = $line['SourceNo'] ?? null;
                $uomCode  = $line['Unit_of_Measure_Code'] ?? null;
                $unitCost = $line['DirectUnitCost'] ?? null;

                if (!empty($itemNo) && !empty($vendorNo)) {
                    $priceMap[$itemNo][$vendorNo] = true;
                    if ($unitCost !== null) {
                        $multiplier = $uomMap[$itemNo][$uomCode] ?? 1; 
                        $costPerBaseUoM = $unitCost / $multiplier;
                        $unitCostsByItem[$itemNo][$vendorNo] = $costPerBaseUoM;
                    }
                }
            }
            foreach ($priceMap as $itemNo => $vendorList) {
                $priceMap[$itemNo] = array_keys($vendorList);
            }

            $vendorMap = [];
            foreach ($vendorRaw as $v) {
                $vendorMap[$v['number']] = $v['displayName'];
            }
            $skuKeys = [];
            foreach ($stockkeeping as $sku) {
                $skuKeys[$sku['Item_No'] . '_' . $sku['Location_Code']] = true;
            }
            // $itemInvenKey = [];
            // foreach ($ItemInvenLoc as $i => $item) {
            //     $itemInvenKey[$item['ItemNo'] . '_' . $item['Location']] = $i;
            // }
            $results = [];
            foreach ($stockkeeping as $sku) {
                $itemNo = $sku['Item_No'];
                $location = $sku['Location_Code'];
                $stock = $sku['Inventory'];
                $minQty = $sku['MinInven'];
                $maxQty = $sku['MaxInven'];
                $description = $sku['Description'];
                $active = $sku['Active'];
                $comment = $sku['Comment'];
                $dbStatus = $sku['Status'];
                $uomCode = $sku['UoMCode'];
                $key = $itemNo . '_' . $location;
                $normalizedDescription = strtoupper(preg_replace('/[^A-Z0-9]/', '', $description));

                if ($location === 'CI.1010' && str_contains($normalizedDescription, 'FRUITVEGETABLE')) {
                    continue;
                }
                $onPO = $purchaseQtys[$key] ?? 0;
                $inTransfer = $transferQtys[$key] ?? 0;
                $qtyToOrder = max(0, ceil($maxQty - ($stock + $inTransfer)));
                $transferDate = $transferDates[$key] ?? null;
                
                if ($qtyToOrder <= 0) {
                    $status = "No Need Order";
                } else {
                    if (($stock + $inTransfer) < $minQty) {
                        $status = "Need Order";
                    } else {
                        $status = "Order For Stock";
                    }
                }
                if ($inTransfer > 0 && ($stock+$inTransfer) > ($minQty)){
                    $status = "Follow Up TO";
                }

                if (empty($vendorNos)) {
                $results[] = [  
                    'item_no'       => $itemNo,
                    'description'   => $description,
                    'stock'         => $stock,
                    'min_qty'       => $minQty,
                    'max_qty'       => $maxQty,
                    'in_transfer'   => $inTransfer,
                    'qty_to_order'  => $qtyToOrder,
                    'vendor_name'   => 'No Vendor',
                    'vendor_no'     => '0',
                    'location_code' => $location,
                    'unit_cost' => '0',
                    'status'        => $status,
                    'transfer_lines'=> $transferDetailsMap[$key] ?? [],
                    'po_lines' => $purchaseDetailsMap[$key] ?? [],
                    'openPo' => $openPurchaseMap[$key] ?? [],
                    'Active' => $active,
                    'Comment' => $comment,
                    'Status' => $dbStatus,
                    'uomCode' => $uomCode,
                    'need_shipment' => ($stock > 0 && $inTransfer > 0)
                ];
            } else {
                foreach ($vendorNos as $vendorNo) {
                    $results[] = [
                        'item_no'       => $itemNo,
                        'description'   => $description,
                        'stock'         => $stock,
                        'min_qty'       => $minQty,
                        'max_qty'       => $maxQty,
                        'in_transfer'   => $inTransfer,
                        'qty_to_order'  => $qtyToOrder,
                        'vendor_name'   => $vendorMap[$vendorNo] ?? null,
                        'vendor_no'     => $vendorNo,
                        'location_code' => $location,
                        'unit_cost' => $unitCostsByItem[$itemNo][$vendorNo],
                        'status'        => $status,
                        'transfer_lines'=> $transferDetailsMap[$key] ?? [],
                        'po_lines' => $purchaseDetailsMap[$key] ?? [],
                        'openPo' => $openPurchaseMap[$key] ?? [],
                        'Active' => $active,
                        'Comment' => $comment,
                        'Status' => $dbStatus,
                        'uomCode' => $uomCode,
                        'need_shipment' => ($stock > 0 && $inTransfer > 0)
                    ];
                }
                }

            }


            foreach ($transferRaw as $line) {
                $itemNo = $line['Item_No'];
                $location = $line['Transfer_to_Code'];
                $key = $itemNo . '_' . $location;
                if (!in_array($location, ['RBC.2100'])) {
                    continue;
                }
                if (isset($skuKeys[$key])) {
                    continue;
                }
                $onPO = $purchaseQtys[$key] ?? 0;
                $inTransfer = $transferQtys[$key] ?? 0;
                $transferDate = $line['Shipment_Date'] ?? null;
                $uomCode = $line['Unit_of_Measure_Code'];

                $description = $line['Description'] ?? '';
                $normalizedDescription = strtoupper(preg_replace('/[^A-Z0-9]/', '', $description));
                if ($location === 'CI.1010' && str_contains($normalizedDescription, 'FRUITVEGETABLE')) {
                    continue;
                }
                
                $stock = $TARU[$itemNo] ?? 0;
                $active = isset($itemInvenKey[$key]) ? $ItemInvenLoc[$itemInvenKey[$key]]['Active'] : true;
                $Comment = isset($itemInvenKey[$key]) ? $ItemInvenLoc[$itemInvenKey[$key]]['Comment'] : '';
                $dbStatus = isset($itemInvenKey[$key]) ? $ItemInvenLoc[$itemInvenKey[$key]]['Status'] : '__';
                $minQty = 0;
                $maxQty = 0;
                $qtyToOrder = max(0, ceil($maxQty - ($stock + $inTransfer)));

                if ($qtyToOrder <= 0) {
                    $status = "No Need Order";
                } else {
                    if (($stock + $inTransfer) < $minQty) {
                        $status = "Need Order";
                    } else {
                        $status = "Order For Stock";
                    }
                }
                if ($inTransfer > 0 && ($stock+$inTransfer) < ($minQty) && ($stock+$inTransfer) > ($minQty)){
                    $status = "Follow Up TO";
                }

                $vendorNos = $priceMap[$itemNo] ?? [];

                if (empty($vendorNos)) {
                    $results[] = [
                        'item_no'       => $itemNo,
                        'description'   => $description,
                        'stock'         => $stock,
                        'min_qty'       => $minQty,
                        'max_qty'       => $maxQty,
                        'in_transfer'   => $inTransfer,
                        'qty_to_order'  => $qtyToOrder,
                        'vendor_name'   => 'No Vendor',
                        'vendor_no'     => '0',
                        'location_code' => $location,
                        'unit_cost'     => '0',
                        'status'        => $status,
                        'transfer_lines' => $transferDetailsMap[$key] ?? [],
                        'po_lines' => $purchaseDetailsMap[$key] ?? [],
                        'openPo' => $openPurchaseMap[$key] ?? [],
                        'Active' => $active,
                        'Comment' => $Comment,
                        'Status' => $dbStatus,
                        'uomCode' => $uomCode,
                        'need_shipment' => ($stock > 0 && $inTransfer > 0)
                        
                    ];
                } else {
                    foreach ($vendorNos as $vendorNo) {
                        $results[] = [
                            'item_no'       => $itemNo,
                            'description'   => $description,
                            'stock'         => $stock,
                            'min_qty'       => $minQty,
                            'max_qty'       => $maxQty,
                            'in_transfer'   => $inTransfer,
                            'qty_to_order'  => $qtyToOrder,
                            'vendor_name'   => $vendorMap[$vendorNo] ?? null,
                            'vendor_no'     => $vendorNo,
                            'location_code' => $location,
                            'unit_cost'     => $unitCostsByItem[$itemNo][$vendorNo] ?? 0,
                            'status'        => $status,
                            'transfer_lines' => $transferDetailsMap[$key] ?? [],
                            'po_lines' => $purchaseDetailsMap[$key] ?? [],
                            'openPo' => $openPurchaseMap[$key] ?? [],
                            'Active' => $active,
                            'Comment' => $Comment,
                            'Status' => $dbStatus,
                            'uomCode' => $uomCode,
                            'need_shipment' => ($stock > 0 && $inTransfer > 0)
                        ];
                    }
                }
            }

            $results = array_filter($results, function ($item) {
                $desc = strtoupper(preg_replace('/[^A-Z0-9]/', '', $item['description'] ?? ''));
                return !(($item['location_code'] === 'CI.1010') && str_contains($desc, 'FRUITVEGETABLE'));
            });
            return [
                'items' => $results,
                'vendors' => $vendorMap
            ];
        }
public function getallitemdata()
{
    $start = microtime(true);
    $priceRaw = $vendorRaw = $uomRaw = [];

    $baseOdata = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/ODataV4/Company('" . $this->companyId . "')";
    $baseApi   = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/api/v2.0/companies({$this->companyId})";

    $headers = [
        'Authorization' => "Bearer {$this->token}",
        'Accept'        => 'application/json',
        'Prefer'        => 'odata.maxpagesize=50000'
    ];

    // ============================================================
    // PREFIX LIST
    // ============================================================
    $prefixes = ['FO', 'BV', 'HO', 'MT', 'BG'];

    $hasValidPrefix = function ($itemNo) use ($prefixes) {
        foreach ($prefixes as $p) {
            if (str_starts_with($itemNo, $p)) return true;
        }
        return false;
    };

    // ============================================================
    // 1. Ambil Price List + Vendor + UoM via async
    // ============================================================
    $promises = [
        'priceLists' => $this->client->getAsync(
            "$baseOdata/Price_List_Lines?\$select=Description,Asset_No,Product_No,Unit_of_Measure_Code,DirectUnitCost,SourceNo,StartingDate,EndingDate,Status",
            ['headers' => $headers]
        ),
        'vendors' => $this->client->getAsync("$baseApi/vendors?\$select=number,displayName", ['headers' => $headers]),
        'itemUom' => $this->client->getAsync("$baseOdata/Item_UOM?\$select=Item_No,Code", ['headers' => $headers])
    ];

    $responses = Utils::settle($promises)->wait();

    $priceRaw = $responses['priceLists']['state'] === 'fulfilled'
        ? json_decode($responses['priceLists']['value']->getBody()->getContents(), true)['value'] ?? []
        : [];

    $vendorRaw = $responses['vendors']['state'] === 'fulfilled'
        ? json_decode($responses['vendors']['value']->getBody()->getContents(), true)['value'] ?? []
        : [];

    $uomRaw = $responses['itemUom']['state'] === 'fulfilled'
        ? json_decode($responses['itemUom']['value']->getBody()->getContents(), true)['value'] ?? []
        : [];

    // ============================================================
    // 2. FILTER PRICE LIST BERDASARKAN PREFIX
    // ============================================================
    $priceRaw = array_values(array_filter($priceRaw, function ($line) use ($hasValidPrefix) {
        $itemNo = $line['Asset_No'] ?? null;
        return $itemNo && $hasValidPrefix($itemNo);
    }));

    // ============================================================
    // 3. Proses Price List → build mapping
    // ============================================================
    $priceMap = [];
    $unitCostsByItem = [];
    $priceDateMap = [];
    $statusPriceMap = [];
    $uomMap = [];
    $descMap = [];

    foreach ($priceRaw as $line) {
        $itemNo   = $line['Asset_No'] ?? null;
        $vendorNo = $line['SourceNo'] ?? null;
        $uomCode  = $line['Unit_of_Measure_Code'] ?? null;
        $unitCost = $line['DirectUnitCost'] ?? null;
        $desc     = $line['Description'] ?? null;

        $startDate = isset($line['StartingDate']) ? strtotime($line['StartingDate']) : 0;
        $endDate   = isset($line['EndingDate']) ? strtotime($line['EndingDate']) : 0;

        if (!$itemNo || !$vendorNo) continue;

        $priceMap[$itemNo][$vendorNo][] = [
            'unit_cost'     => $unitCost,
            'uom'           => $uomCode,
            'description'   => $desc,
            'start'         => $startDate,
            'end'           => $endDate,
            'starting_date' => $line['StartingDate'] ?? null,
            'ending_date'   => $line['EndingDate'] ?? null
        ];

        $statusPriceMap[$itemNo][$vendorNo] = $line['Status'] ?? null;
    }

    // ============================================================
    // 4. Select best price per item/vendor (latest end date)
    // ============================================================
    foreach ($priceMap as $itemNo => $vendors) {
        foreach ($vendors as $vendorNo => $prices) {
            usort($prices, function ($a, $b) {
                if ($a['end'] !== $b['end']) return $b['end'] - $a['end'];
                return $b['start'] - $a['start'];
            });

            $selected = $prices[0];

            $unitCostsByItem[$itemNo][$vendorNo] = $selected['unit_cost'];
            $priceDateMap[$itemNo][$vendorNo] = [
                'starting_date' => $selected['starting_date'],
                'ending_date'   => $selected['ending_date']
            ];
            $uomMap[$itemNo][$vendorNo] = $selected['uom'];
            $descMap[$itemNo][$vendorNo] = $selected['description'];
            $priceMap[$itemNo][$vendorNo] = true;
        }
    }

    foreach ($priceMap as $itemNo => $vendorList) {
        $priceMap[$itemNo] = array_keys($vendorList);
    }

    // ============================================================
    // 5. Vendor Map
    // ============================================================
    $vendorMap = [];
    foreach ($vendorRaw as $v) {
        $vendorMap[$v['number']] = $v['displayName'];
    }

    // ============================================================
    // 6. Item UoM Map
    // ============================================================
    $itemUomMap = [];
    foreach ($uomRaw as $row) {
        $item = $row['Item_No'] ?? null;
        $code = $row['Code'] ?? null;
        if (!$item || !$code) continue;
        $itemUomMap[$item][] = $code;
    }

    // ============================================================
    // 7. Build final results
    // ============================================================
    $results = [];
    $seenKeys = [];

    foreach ($priceMap as $itemNo => $vendorNos) {

        // prefix filter juga di double-check di sini
        if (!$hasValidPrefix($itemNo)) continue;

        foreach ($vendorNos as $vendorNo) {

            $uniqueKey = $itemNo . '_' . $vendorNo;
            if (isset($seenKeys[$uniqueKey])) continue;
            $seenKeys[$uniqueKey] = true;

            $dates = $priceDateMap[$itemNo][$vendorNo] ?? [
                'starting_date' => null,
                'ending_date' => null
            ];

            $results[] = [
                'item_no'       => $itemNo,
                'description'   => $descMap[$itemNo][$vendorNo] ?? null,
                'vendor_name'   => $vendorMap[$vendorNo] ?? null,
                'vendor_no'     => $vendorNo,
                'unit_cost'     => $unitCostsByItem[$itemNo][$vendorNo] ?? 0,
                'uom'           => $uomMap[$itemNo][$vendorNo] ?? null,
                'item_uom'      => $itemUomMap[$itemNo] ?? [],
                'starting_date' => $dates['starting_date'] ? date('d/m/Y', strtotime($dates['starting_date'])) : null,
                'ending_date'   => $dates['ending_date'] ? date('d/m/Y', strtotime($dates['ending_date'])) : null,
                'status_price'  => $statusPriceMap[$itemNo][$vendorNo] ?? null
            ];
        }
    }

    $duration = round(microtime(true) - $start, 2);
    Log::info("Fetched BC Item Data in {$duration}s with " . count($results) . " records.");

    return [
        'items'   => $results,
        'vendors' => $vendorMap,
    ];
}


    public function getItemIdByNo(string $itemNo)
    {
        $filter = urlencode("number eq '$itemNo'");
        $response = $this->client->get("https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
       "/api/v2.0/companies({$this->companyId})/items?\$filter=$filter", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Content-Type'  => 'application/json',
            ]
        ]);

        $items = json_decode($response->getBody(), true);
        return $items['value'][0]['id'] ?? null;

        //$url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/api/citbi/sku/v1.0/companies(" . rawurlencode($this->companyId) . ")/GeneralJournalLines";
    }


    public function createPurchaseOrderForVendor($vendorNo, $userName, $location)
    {
        if ($location == 'CI.1010') {
            $purchaser = 'TN01';
        } 
        else if ($location == 'CI.1020'){
            $purchaser = 'UT01';
        } else if ($location == 'HIN.1200' || $location == 'HIN.1300') {
            $purchaser = 'PE01';
        } else if ($location == 'HIN.1000') {
            $purchaser = 'PW01';
        } else if ($location == 'HIN.3000') {
            $purchaser = 'FB01';
        }

        $payload = [
            'vendorNumber' => $vendorNo,
            'orderDate' => now()->toDateString(),
            'purchaser' => $purchaser,
            'shortcutDimension2Code' => "100000",
            'requestedReceiptDate' => today()->toDateString()
        ];

        $response = $this->client->post("https://api.businesscentral.dynamics.com/v2.0/".env('AZURE_TENANT_ID')."/".env('BC_ENVIRONMENT')."/api/v2.0/companies({$this->companyId})/purchaseOrders", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Content-Type'  => 'application/json',
            ],
            'body' => json_encode($payload)
        ]);
        
        $body = json_decode($response->getBody(), true);
        return json_decode($response->getBody(), true);
    }

    public function getLocationIdByCode($locationCode)
    {
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/api/v2.0/companies({$this->companyId})/locations?\$filter=code eq '{$locationCode}'";

        $response = $this->client->get($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json'
            ]
        ]);

        $data = json_decode((string)$response->getBody(), true);
        
        return $data['value'][0]['id'] ?? null;
    }


    // public function addPurchaseLineToPO($poId, $itemNo, $quantity, $cost, $location, $userName)
    // {
    //     $start = microtime(true);
    
    //     $itemId = $this->getItemIdByNo($itemNo);
    //     $locationCode = $location;
    //     if (str_contains($itemNo, 'BV') && $cost > 1000000) $locationCode = 'CI.1051';
    //     $locationId = $this->getLocationIdByCode($locationCode);
    
    //     $payload = json_encode([
    //         '@odata.type'       => '#Microsoft.NAV.purchaseOrderLine',
    //         'lineType'  => 'Item',
    //         'itemId'     => $itemId,
    //         'quantity'   => (float)$quantity
    //     ]);
    
    //     $url = "https://api.businesscentral.dynamics.com/v2.0/"
    //          . env('AZURE_TENANT_ID') . "/"
    //          . env('BC_ENVIRONMENT')
    //          . "/api/v2.0/companies({$this->companyId})/purchaseOrders({$poId})/purchaseOrderLines";

    //     try {
    //         $response = $this->client->post($url, [
    //             'headers' => [
    //                 'Authorization' => "Bearer {$this->token}",
    //                 'Accept'        => 'application/json',
    //                 'Content-Type'  => 'application/json',
    //                 'OData-Version'     => '4.0',
    //                 'OData-MaxVersion'  => '4.0',
    //             ],
    //             'body' => $payload,
    //         ]);
    
    //         Log::info("Create POLine runtime: " . (microtime(true) - $start) . " seconds");
    //         return json_decode((string)$response->getBody(), true);
    
    //     } catch (RequestException $e) {
    //         $res     = $e->getResponse();
    //         $status  = $res?->getStatusCode();
    //         $reason  = $res?->getReasonPhrase();
    //         $headers = $res?->getHeaders() ?? [];
    //         $raw     = $res ? (string)$res->getBody() : $e->getMessage();
    
    //         // Optional telemetry IDs (if present)
    //         $corrId = $headers['x-correlation-id'][0] ?? ($headers['MS-CorrelationId'][0] ?? null);
    //         $reqId  = $headers['request-id'][0] ?? null;
    
    //         Log::error('BC addPurchaseLineToPO failed', [
    //             'url'           => $url,
    //             'status'        => $status,
    //             'reason'        => $reason,
    //             'headers'       => $headers,
    //             'payload_json'  => json_decode($payload, true),
    //             'error_raw'     => $raw,      // ← full upstream body (no truncation)
    //             'requestId'     => $reqId,
    //             'correlationId' => $corrId,
    //             'exception'     => $e->getMessage(),
    //         ]);
    //         throw $e; // or return a normalized error if you prefer
    //     }
    // }
    
    
    public function getAsyncAddPurchaseLinePromise($poId, $itemNo, $quantity, $userName)
    {   
        $start = microtime(true);
        $itemId     = $this->getItemIdByNo($itemNo);
        $locationId = $this->getLocationIdByCode(in_array($userName, [
                'titania@citbi.onmicrosoft.com',
                'adminbc@citbi.onmicrosoft.com'
            ]) ? 'CI.1010' : 'CI.1020');

        $payload = [
            'itemId'     => $itemId,
            'quantity'   => (float) $quantity,
            'locationId' => $locationId,
        ];
        $elapsed = microtime(true) - $start;
        Log::info("Create POLine runtime: {$elapsed} seconds");
        return $this->client->postAsync(
            "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/api/v2.0/companies({$this->companyId})/purchaseOrders({$poId})/purchaseOrderLines",
            [
                'headers' => [
                    'Authorization' => "Bearer {$this->token}",
                    'Accept'        => 'application/json',
                    'Content-Type'  => 'application/json',
                ],
                'body' => json_encode($payload),
            ]
        );
    }

    public function addPurchaseLineToPO(string $poNo, string $itemNo, float $quantity, $cost, string $locationCode = '', $userName, string $poId)
    {
        $base = "https://api.businesscentral.dynamics.com/v2.0/"
          . env('AZURE_TENANT_ID') . "/"
          . env('BC_ENVIRONMENT');

        $root = $base . "/api/citbi/sku/v1.0";
        $linesUrl = "{$root}/companies({$this->companyId})/purchaseOrderLinesCustom"; // <-- exact casing!

        $headersJson = [
            'Authorization'    => "Bearer {$this->token}",
            'Accept'           => 'application/json',
            'Content-Type'     => 'application/json',
        ];

        try {
            $flt = urlencode("documentType eq 'Order' and documentNo eq '{$poNo}'");
            $lastUrl = $linesUrl . "?\$filter={$flt}&\$orderby=lineNo desc&\$top=1&\$select=lineNo";
            $lastResp = $this->client->get($lastUrl, ['headers' => $headersJson]);
            $lastJson = json_decode((string)$lastResp->getBody(), true);
            $lastLineNo = (int)($lastJson['value'][0]['lineNo'] ?? 0);
            $nextLineNo = max(10000, $lastLineNo + 10000);

            $payload = [
                'documentType' => 'Order',
                'documentNo'   => (string)$poNo,
                'lineNo'       => $nextLineNo,
                'itemNo'       => $itemNo,
                'type'         => 'Item',
                'quantity'     => (float)$quantity
            ];
            if ($locationCode !== '') {
                $payload['locationCode'] = $locationCode;
            }
            $resp = $this->client->post($linesUrl, [ 'headers' => $headersJson, 'body' => json_encode($payload, JSON_UNESCAPED_SLASHES), ]); 
            $data = json_decode((string)$resp->getBody(), true); 
            $etag  = $data['@odata.etag'] ?? '*';
            $idUrl = $data['@odata.id']   ?? null;
            \Log::info('API line created (doc keys)', [ 'url' => $linesUrl, 'payload' => $payload, 'response' => $data ]);

            return $data;

        } catch (\GuzzleHttp\Exception\RequestException $e) {
            $res = $e->getResponse();
            $raw = $res ? (string)$res->getBody() : $e->getMessage();
            \Log::error('Custom API purchase line POST failed', [
                'url'       => $linesUrl,
                'status'    => $res?->getStatusCode(),
                'reason'    => $res?->getReasonPhrase(),
                'headers'   => $res?->getHeaders(),
                'payload'   => $payload ?? null,
                'error_raw' => $raw,
                'requestId' => $res?->getHeader('request-id')[0] ?? null,
            ]);
            throw $e;
        }
    }

    public function getAllPurchasePrices()
    {
        $start = microtime(true);
        $today = now()->toDateString(); // e.g. "2025-06-17"
        $filter = "SourceNo ne null and StartingDate le $today and EndingDate ge $today";
        $encodedFilter = urlencode($filter);

        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/Price_List_Lines?\$filter={$encodedFilter}";

        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer'        => 'odata.maxpagesize=1000'
            ]
        ];

        $allData = [];

        try {
            do {
                $response = $this->client->get($url, $headers);
                $decoded = json_decode((string)$response->getBody(), true);

                if (isset($decoded['value'])) {
                    $allData = array_merge($allData, $decoded['value']);
                }

                $url = $decoded['@odata.nextLink'] ?? null;
            } while ($url);

            return collect($allData)->groupBy('Asset_No');
        } catch (\Exception $e) {
            Log::error("Failed to fetch Price List Lines: " . $e->getMessage());
            return collect(); 
        }
        $elapsed = microtime(true) - $start;
        Log::info("Fetch runtime Priccss: {$elapsed} seconds");
        
    }
public function createReqWorksheet(array $data)
{
    $baseUrl = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/reqWorksheet";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    $createdLines = [];
    $today = now()->format('Y-m-d');

    try {
        // =========================================================
        // STEP 1: GET LAST lineNo untuk batch & template tertentu
        // =========================================================
        $filterUrl = $baseUrl . "?\$filter=templateName eq 'REQ.' and journalBatch eq 'PASTRY'";
        $getRes = $client->get($filterUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
        ]);

        $existing = json_decode((string)$getRes->getBody(), true);
        $lastLineNo = collect($existing['value'] ?? [])->max('lineNo') ?? 0;
        $lineCounter = $lastLineNo + 10000;

        // =========================================================
        // STEP 2: LOOP tiap line input
        // =========================================================
        foreach ($data['lines'] as $index => $line) {
            $logPrefix = "Line {$lineCounter}";
            try {
                // ---------- POST Line Baru ----------
                $postPayload = [
                    'templateName' => 'REQ.',
                    'journalBatch' => 'PASTRY',
                    'lineNo'       => $lineCounter,
                ];

                $postRes = $client->post($baseUrl, [
                    'headers' => [
                        'Authorization' => "Bearer {$this->token}",
                        'Accept' => 'application/json',
                        'Content-Type' => 'application/json',
                    ],
                    'json' => $postPayload,
                ]);

                $posted = json_decode((string)$postRes->getBody(), true);
                $etag   = $posted['@odata.etag'] ?? null;
                if (!$etag) throw new \Exception("No ETag returned on POST {$logPrefix}");

                // URL PATCH harus lengkap dengan key kombinasi
                $patchUrl = $baseUrl . "(templateName='REQ.',journalBatch='PASTRY',lineNo={$lineCounter})";

                // ---------- PATCH Steps (utama, tanpa uomCode & businessUnit) ----------
                $patchSteps = [
                    1 => ['tipe'         => 'Item'], // PATCH pertama: Type
                    2 => ['itemNo'       => $line['itemNo']],
                    3 => ['quantity'     => $line['quantity']],
                    4 => ['department'   => $data['department'] ?? ''],
                    5 => ['transferFrom' => $data['transferFromCode'] ?? ''],
                    6 => ['locCode'      => $data['transfertoCode'] ?? ''],
                    7 => ['dueDate'      => $today],
                ];

                foreach ($patchSteps as $step => $payload) {
                    try {
                        $patchRes = $client->patch($patchUrl, [
                            'headers' => [
                                'Authorization' => "Bearer {$this->token}",
                                'Accept' => 'application/json',
                                'If-Match' => $etag,
                                'Content-Type' => 'application/json',
                            ],
                            'json' => $payload,
                        ]);

                        $patched = json_decode((string)$patchRes->getBody(), true);
                        $etag = $patched['@odata.etag'] ?? $etag;

                    } catch (\GuzzleHttp\Exception\RequestException $e) {
                        $errorBody = '';
                        if ($e->hasResponse()) {
                            $body = json_decode((string)$e->getResponse()->getBody(), true);
                            $errorBody = $body['error']['message'] ?? '';
                        }
                        $msg = "PATCH STEP {$step} gagal untuk {$logPrefix}: " . ($errorBody ?: $e->getMessage());
                        \Log::error($msg, ['payload' => $payload]);
                        throw new \Exception($msg);
                    }
                }

                // ---------- PATCH STEP TERPISAH UNTUK uomCode ----------
                if (!empty($line['uomCode'])) {
                    try {
                        $uomPayload = ['uomCode' => $line['uomCode']];
                        $uomRes = $client->patch($patchUrl, [
                            'headers' => [
                                'Authorization' => "Bearer {$this->token}",
                                'Accept' => 'application/json',
                                'If-Match' => $etag,
                                'Content-Type' => 'application/json',
                            ],
                            'json' => $uomPayload,
                        ]);

                        $uomPatched = json_decode((string)$uomRes->getBody(), true);
                        $etag = $uomPatched['@odata.etag'] ?? $etag;

                        \Log::info("PATCH UOM sukses untuk {$logPrefix}", ['uomCode' => $line['uomCode']]);
                    } catch (\GuzzleHttp\Exception\RequestException $e) {
                        $body = $e->hasResponse() ? json_decode((string)$e->getResponse()->getBody(), true) : [];
                        $errorMsg = $body['error']['message'] ?? $e->getMessage();
                        \Log::error("PATCH UOM gagal untuk {$logPrefix}: {$errorMsg}");
                        throw new \Exception("PATCH UOM gagal untuk {$logPrefix}: {$errorMsg}");
                    }
                }

                // ---------- PATCH STEP TERPISAH UNTUK businessUnit ----------
                if (!empty($data['businessUnit'])) {
                    try {
                        $buPayload = ['businessUnit' => $data['businessUnit']];
                        $buRes = $client->patch($patchUrl, [
                            'headers' => [
                                'Authorization' => "Bearer {$this->token}",
                                'Accept' => 'application/json',
                                'If-Match' => $etag,
                                'Content-Type' => 'application/json',
                            ],
                            'json' => $buPayload,
                        ]);

                        $buPatched = json_decode((string)$buRes->getBody(), true);
                        $etag = $buPatched['@odata.etag'] ?? $etag;

                        \Log::info("PATCH Business Unit sukses untuk {$logPrefix}", ['businessUnit' => $data['businessUnit']]);
                    } catch (\GuzzleHttp\Exception\RequestException $e) {
                        $body = $e->hasResponse() ? json_decode((string)$e->getResponse()->getBody(), true) : [];
                        $errorMsg = $body['error']['message'] ?? $e->getMessage();
                        \Log::error("PATCH Business Unit gagal untuk {$logPrefix}: {$errorMsg}");
                        throw new \Exception("PATCH Business Unit gagal untuk {$logPrefix}: {$errorMsg}");
                    }
                }

                // ---------- SUCCESS SUMMARY ----------
                $createdLines[] = [
                    'lineNo'  => $lineCounter,
                    'status'  => 'success',
                    'message' => "Berhasil POST & PATCH semua step untuk {$logPrefix}.",
                ];

            } catch (\Throwable $e) {
                \Log::error("{$logPrefix} gagal: " . $e->getMessage());
                $createdLines[] = [
                    'lineNo'  => $lineCounter,
                    'status'  => 'error',
                    'message' => $e->getMessage(),
                ];
            }

            $lineCounter += 10000;
        }

        return [
            'status'  => 'success',
            'message' => 'Req Worksheet berhasil dibuat & di-update.',
            'lines'   => $createdLines,
        ];

    } catch (\Throwable $e) {
        \Log::error('BC createReqWorksheet FAILED', ['error' => $e->getMessage()]);
        return [
            'status'  => 'error',
            'message' => $e->getMessage(),
        ];
    }
}

public function getAllItemDataWithUoM()
{
    $cacheKey     = 'bc_item_data_all';
    $hashKey      = 'bc_item_data_hash';
    $cacheTtl     = now()->addHours(1);
    $prefixes     = ['FO', 'BV', 'HO', 'MT', 'BG'];

    if (Cache::has($cacheKey)) {
        $cachedData = Cache::get($cacheKey);
        $oldHash = Cache::get($hashKey);

        dispatch(function () use ($cacheKey, $hashKey, $prefixes, $cacheTtl, $oldHash) {
            try {
                $fresh = $this->fetchAllItemDataWithUoM($prefixes);

                $newHash = md5(json_encode([
                    'count' => count($fresh['items']),
                    'head'  => array_slice($fresh['items'], 0, 10),
                    'tail'  => array_slice($fresh['items'], -10),
                ]));

                if ($newHash !== $oldHash) {
                    Cache::put($cacheKey, $fresh, $cacheTtl);
                    Cache::put($hashKey, $newHash, $cacheTtl);
                    Log::info("🔄 BC cache updated — hash changed.");
                } else {
                    Log::info("✅ BC cache valid — no change.");
                }

            } catch (\Throwable $e) {
                Log::error("❌ Cache refresh failed: " . $e->getMessage());
            }
        })->afterResponse();

        return $cachedData;
    }

    try {
        $data = $this->fetchAllItemDataWithUoM($prefixes);

        $hash = md5(json_encode([
            'count' => count($data['items']),
            'head'  => array_slice($data['items'], 0, 10),
            'tail'  => array_slice($data['items'], -10),
        ]));

        Cache::put($cacheKey, $data, $cacheTtl);
        Cache::put($hashKey, $hash, $cacheTtl);

        Log::info("🆕 Fresh BC data fetched & cached.");
        return $data;

    } catch (\Throwable $e) {
        Log::error("❌ Failed initial BC fetch: " . $e->getMessage());
        return [];
    }
}


public function fetchAllItemDataWithUoM(array $prefixes = ['FO','BV','HO','MT','BG'])
{
    $start = microtime(true);

    $baseOdata = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/ODataV4/Company('" . $this->companyId . "')";
    $baseApi   = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/api/v2.0/companies({$this->companyId})";

    $headers = [
        'Authorization' => "Bearer {$this->token}",
        'Accept'        => 'application/json',
        'Prefer'        => 'odata.maxpagesize=5000'
    ];

    // ====================================================================
    // HELPERS
    // ====================================================================
    $hasValidPrefix = function ($itemNo) use ($prefixes) {
        foreach ($prefixes as $p) {
            if (str_starts_with($itemNo, $p)) return true;
        }
        return false;
    };

    // ====================================================================
    // STREAMING FETCH FUNCTION
    // ====================================================================
    $streamFetch = function ($url) use ($headers) {
        $skip = 0;
        $top  = 5000;

        while (true) {
            $pagedUrl = $url . "&%24top={$top}&%24skip={$skip}";
            $res = Http::withHeaders($headers)->get($pagedUrl);

            if (!$res->successful()) break;

            $rows = $res->json('value') ?? [];

            if (!count($rows)) break;

            foreach ($rows as $r) {
                yield $r;
            }

            $skip += $top;
        }
    };

    // ====================================================================
    // 1. STREAM PRICE LIST
    // ====================================================================
    $priceMap = [];
    $unitCostsByItem = [];
    $priceDateMap = [];
    $statusPriceMap = [];
    $uomMap = [];
    $descMap = [];

    foreach ($streamFetch("$baseOdata/Price_List_Lines?\$select=Description,Asset_No,Product_No,Unit_of_Measure_Code,DirectUnitCost,SourceNo,StartingDate,EndingDate,Status") as $line) {

        $itemNo = $line['Asset_No'] ?? null;
        if (!$itemNo || !$hasValidPrefix($itemNo)) continue;

        $vendorNo = $line['SourceNo'] ?? null;
        if (!$vendorNo) continue;

        $priceMap[$itemNo][$vendorNo][] = [
            'unit_cost'     => $line['DirectUnitCost'] ?? null,
            'uom'           => $line['Unit_of_Measure_Code'] ?? null,
            'description'   => $line['Description'] ?? null,
            'start'         => strtotime($line['StartingDate'] ?? '') ?: 0,
            'end'           => strtotime($line['EndingDate'] ?? '') ?: 0,
            'starting_date' => $line['StartingDate'] ?? null,
            'ending_date'   => $line['EndingDate'] ?? null,
        ];

        $statusPriceMap[$itemNo][$vendorNo] = $line['Status'] ?? null;
    }

    // ====================================================================
    // 2. SELECT BEST PRICE PER ITEM/VENDOR
    // ====================================================================
    foreach ($priceMap as $itemNo => $vendorGroup) {
        foreach ($vendorGroup as $vendorNo => $prices) {

            usort($prices, fn($a,$b) =>
                ($b['end'] <=> $a['end']) ?: ($b['start'] <=> $a['start'])
            );

            $best = $prices[0];

            $unitCostsByItem[$itemNo][$vendorNo] = $best['unit_cost'];
            $priceDateMap[$itemNo][$vendorNo] = [
                'starting_date' => $best['starting_date'],
                'ending_date'   => $best['ending_date'],
            ];
            $uomMap[$itemNo][$vendorNo] = $best['uom'];
            $descMap[$itemNo][$vendorNo] = $best['description'];

            $priceMap[$itemNo][$vendorNo] = true;
        }

        $priceMap[$itemNo] = array_keys($priceMap[$itemNo]);
    }

    // ====================================================================
    // 3. STREAM VENDORS
    // ====================================================================
    $vendorMap = [];
    foreach ($streamFetch("$baseApi/vendors?\$select=number,displayName") as $v) {
        $vendorMap[$v['number']] = $v['displayName'];
    }

    // ====================================================================
    // 4. STREAM ITEM UOM
    // ====================================================================
    $itemUomMap = [];
    foreach ($streamFetch("$baseOdata/Item_UOM?\$select=Item_No,Code") as $row) {
        $item = $row['Item_No'] ?? null;
        if (!$item) continue;
        $itemUomMap[$item][] = $row['Code'];
    }

    // ====================================================================
    // 5. BUILD FINAL RESULT (SAMA PERSIS SEPERTI VERSI LAMA)
    // ====================================================================
    $results = [];
    $seenKeys = [];

    foreach ($priceMap as $itemNo => $vendorNos) {
        if (!$hasValidPrefix($itemNo)) continue;

        foreach ($vendorNos as $vendorNo) {

            $uk = $itemNo . '_' . $vendorNo;
            if (isset($seenKeys[$uk])) continue;
            $seenKeys[$uk] = true;

            $dates = $priceDateMap[$itemNo][$vendorNo] ?? [
                'starting_date' => null,
                'ending_date'   => null
            ];

            $results[] = [
                'item_no'       => $itemNo,
                'description'   => $descMap[$itemNo][$vendorNo] ?? null,
                'vendor_name'   => $vendorMap[$vendorNo] ?? null,
                'vendor_no'     => $vendorNo,
                'unit_cost'     => $unitCostsByItem[$itemNo][$vendorNo] ?? 0,
                'uom'           => $uomMap[$itemNo][$vendorNo] ?? null,
                'item_uom'      => $itemUomMap[$itemNo] ?? [],
                'starting_date' => $dates['starting_date'] ? date('d/m/Y', strtotime($dates['starting_date'])) : null,
                'ending_date'   => $dates['ending_date'] ? date('d/m/Y', strtotime($dates['ending_date'])) : null,
                'status_price'  => $statusPriceMap[$itemNo][$vendorNo] ?? null
            ];
        }
    }

    Log::info("STREAMED BC fetch in ".round(microtime(true)-$start,2)."s — ".count($results)." rows");

    return [
        'items'   => $results,
        'vendors' => $vendorMap,
    ];
}

public function updateBillOfMaterialItems(array $data)
{
    $baseUrl = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/billOfMaterial";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    $results = [];

    try {
        foreach ($data as $batchIndex => $recipe) {
            $bomParent = $recipe['parent_recipe']['bom_parent'];
            $bomDesc   = $recipe['parent_recipe']['description'] ?? '';
            $materials = $recipe['materials'] ?? [];

            \Log::info("🔹 Starting BOM Update for {$bomParent} ({$bomDesc})");

            // =======================================================
            // STEP 1: GET ALL LINES of This BOM Parent
            // =======================================================
            $filterUrl = $baseUrl . "?\$filter=parentNo eq '{$bomParent}'";
            $res = $client->get($filterUrl, [
                'headers' => [
                    'Authorization' => "Bearer {$this->token}",
                    'Accept' => 'application/json',
                ],
            ]);
            $existingLines = json_decode((string) $res->getBody(), true);
            $bomLines = collect($existingLines['value'] ?? []);

            if ($bomLines->isEmpty()) {
                \Log::warning("⚠️ No lines found for BOM {$bomParent}");
                $results[] = [
                    'bom_parent' => $bomParent,
                    'status' => 'warning',
                    'message' => "No BOM lines found for parent {$bomParent}",
                ];
                continue;
            }

            // =======================================================
            // STEP 2: LOOP setiap MATERIAL yang akan diubah
            // =======================================================
            foreach ($materials as $matIndex => $mat) {
                $oldNo  = $mat['old_material']['item_no'];
                $newMat = $mat['new_material'];

                // cari baris yang cocok
                $targetLine = $bomLines->first(function ($line) use ($bomParent, $oldNo) {
                    return isset($line['parentNo'], $line['itemNo'])
                        && $line['parentNo'] === $bomParent
                        && $line['itemNo'] === $oldNo;
                });

                if (!$targetLine) {
                    \Log::warning("⚠️ No matching line found for {$oldNo} in BOM {$bomParent}");
                    $results[] = [
                        'bom_parent' => $bomParent,
                        'old_item'   => $oldNo,
                        'status'     => 'not_found',
                        'message'    => "No line found for {$oldNo}",
                    ];
                    continue;
                }

                $lineNo = $targetLine['lineNo'];
                $etag   = $targetLine['@odata.etag'] ?? null;
                $patchUrl = $baseUrl . "(parentNo='{$bomParent}',lineNo={$lineNo})";

                \Log::info("🧩 Updating BOM line {$lineNo} ({$oldNo}) → {$newMat['item_no']}");

                // =======================================================
                // STEP 3: PATCH itemNo
                // =======================================================
                try {
                    $payload = ['itemNo' => $newMat['item_no']];
                    $patchRes = $client->patch($patchUrl, [
                        'headers' => [
                            'Authorization' => "Bearer {$this->token}",
                            'Accept' => 'application/json',
                            'If-Match' => $etag,
                            'Content-Type' => 'application/json',
                        ],
                        'json' => $payload,
                    ]);
                    $patched = json_decode((string)$patchRes->getBody(), true);
                    $etag = $patched['@odata.etag'] ?? $etag;
                    \Log::info("✅ PATCH itemNo success for {$bomParent} line {$lineNo}");
                } catch (\GuzzleHttp\Exception\RequestException $e) {
                    $body = $e->hasResponse() ? json_decode((string)$e->getResponse()->getBody(), true) : [];
                    $msg = $body['error']['message'] ?? $e->getMessage();
                    \Log::error("❌ PATCH itemNo failed for {$bomParent}-{$lineNo}: {$msg}");
                    $results[] = [
                        'bom_parent' => $bomParent,
                        'line_no'    => $lineNo,
                        'old_item'   => $oldNo,
                        'new_item'   => $newMat['item_no'],
                        'status'     => 'error',
                        'message'    => "PATCH itemNo failed: {$msg}",
                    ];
                    continue;
                }

                // =======================================================
                // STEP 4: PATCH uomCode (jika ada)
                // =======================================================
                if (!empty($newMat['chosen_uom'])) {
                    try {
                        $payload = ['uomCode' => $newMat['chosen_uom']];
                        $patchRes = $client->patch($patchUrl, [
                            'headers' => [
                                'Authorization' => "Bearer {$this->token}",
                                'Accept' => 'application/json',
                                'If-Match' => $etag,
                                'Content-Type' => 'application/json',
                            ],
                            'json' => $payload,
                        ]);
                        $patched = json_decode((string)$patchRes->getBody(), true);
                        $etag = $patched['@odata.etag'] ?? $etag;
                        \Log::info("✅ PATCH uomCode success for {$bomParent}-{$lineNo}");
                    } catch (\GuzzleHttp\Exception\RequestException $e) {
                        $body = $e->hasResponse() ? json_decode((string)$e->getResponse()->getBody(), true) : [];
                        $msg = $body['error']['message'] ?? $e->getMessage();
                        \Log::error("❌ PATCH uomCode failed for {$bomParent}-{$lineNo}: {$msg}");
                        $results[] = [
                            'bom_parent' => $bomParent,
                            'line_no'    => $lineNo,
                            'new_item'   => $newMat['item_no'],
                            'status'     => 'error',
                            'message'    => "PATCH uomCode failed: {$msg}",
                        ];
                    }
                }

                // =======================================================
                // STEP 5: PATCH qtyPer (jika ada)
                // =======================================================
                if (!empty($newMat['chosen_quantity'])) {
                    try {
                        $payload = ['qtyPer' => (float)$newMat['chosen_quantity']];
                        $patchRes = $client->patch($patchUrl, [
                            'headers' => [
                                'Authorization' => "Bearer {$this->token}",
                                'Accept' => 'application/json',
                                'If-Match' => $etag,
                                'Content-Type' => 'application/json',
                            ],
                            'json' => $payload,
                        ]);
                        $patched = json_decode((string)$patchRes->getBody(), true);
                        $etag = $patched['@odata.etag'] ?? $etag;
                        \Log::info("✅ PATCH qtyPer success for {$bomParent}-{$lineNo}");
                    } catch (\GuzzleHttp\Exception\RequestException $e) {
                        $body = $e->hasResponse() ? json_decode((string)$e->getResponse()->getBody(), true) : [];
                        $msg = $body['error']['message'] ?? $e->getMessage();
                        \Log::error("❌ PATCH qtyPer failed for {$bomParent}-{$lineNo}: {$msg}");
                        $results[] = [
                            'bom_parent' => $bomParent,
                            'line_no'    => $lineNo,
                            'new_item'   => $newMat['item_no'],
                            'status'     => 'error',
                            'message'    => "PATCH qtyPer failed: {$msg}",
                        ];
                    }
                }

                // =======================================================
                // ✅ STEP 6: PATCH assemblyBom = true
                // =======================================================
                try {
                    $payload = ['assemblyBom' => true];
                    $patchRes = $client->patch($patchUrl, [
                        'headers' => [
                            'Authorization' => "Bearer {$this->token}",
                            'Accept' => 'application/json',
                            'If-Match' => $etag,
                            'Content-Type' => 'application/json',
                        ],
                        'json' => $payload,
                    ]);
                    $patched = json_decode((string)$patchRes->getBody(), true);
                    $etag = $patched['@odata.etag'] ?? $etag;
                    \Log::info("✅ PATCH assemblyBom=true success for {$bomParent}-{$lineNo}");
                } catch (\GuzzleHttp\Exception\RequestException $e) {
                    $body = $e->hasResponse() ? json_decode((string)$e->getResponse()->getBody(), true) : [];
                    $msg = $body['error']['message'] ?? $e->getMessage();
                    \Log::error("❌ PATCH assemblyBom failed for {$bomParent}-{$lineNo}: {$msg}");
                    $results[] = [
                        'bom_parent' => $bomParent,
                        'line_no'    => $lineNo,
                        'new_item'   => $newMat['item_no'],
                        'status'     => 'error',
                        'message'    => "PATCH assemblyBom failed: {$msg}",
                    ];
                }

                $results[] = [
                    'bom_parent' => $bomParent,
                    'line_no'    => $lineNo,
                    'old_item'   => $oldNo,
                    'new_item'   => $newMat['item_no'],
                    'status'     => 'success',
                    'message'    => "Successfully updated line {$lineNo} ({$oldNo} → {$newMat['item_no']})",
                ];
            }

            \Log::info("✅ Finished updating all materials for {$bomParent}");
        }

        return [
            'status'  => 'success',
            'message' => 'All BOM updates processed successfully.',
            'results' => $results,
        ];

    } catch (\Throwable $e) {
        \Log::error('❌ updateBillOfMaterialItems FAILED', ['error' => $e->getMessage()]);
        return [
            'status'  => 'error',
            'message' => $e->getMessage(),
            'results' => $results,
        ];
    }
}


public function assemblyLineCustom()
{
    // ✅ Versi ringan dari getAllTransferLines(), tanpa response()->json() dan overhead HTTP
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/assemblyLineCustom?\$filter = locCode eq 'CI.1011'";
    // $base = "https://api.businesscentral.dynamics.com/v2.0/"
    //     . env('AZURE_TENANT_ID') . "/"
    //     . env('BC_ENVIRONMENT')
    //     . "/api/citbi/sku/v1.0/companies({$this->companyId})/assemblyLineCustom?\$filter = locCode eq 'CI.1012'";
//    $base = "https://api.businesscentral.dynamics.com/v2.0/"
//         . env('AZURE_TENANT_ID') . "/"
//         . env('BC_ENVIRONMENT')
//         . "/api/citbi/sku/v1.0/companies({$this->companyId})/assemblyLineCustom?\$filter = locCode eq 'RBC.2040'";
    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->get($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
        ]);

        $result = json_decode((string) $response->getBody(), true);
        return $result['value'] ?? [];

    } catch (\Throwable $e) {
        Log::error('BC getAllTransferLinesData error: '.$e->getMessage());
        return [];
    }
}

   public function getItemLot()
{
    try {
        set_time_limit(300);

        // =========================
        // BASE URL
        // =========================
        $baseOdata = "https://api.businesscentral.dynamics.com/v2.0/"
            . env('AZURE_TENANT_ID') . "/"
            . env('BC_ENVIRONMENT')
            . "/ODataV4/Company('" . $this->companyId . "')";

        $filter = urlencode("LocationCode eq 'CI.1011'");
        $endpoint = "$baseOdata/ItemLot?\$filter={$filter}";

        $headers = [
            'Authorization' => "Bearer {$this->token}",
            'Accept'        => 'application/json',
        ];

        $response = $this->client->get($endpoint, [
            'headers'         => $headers,
            'timeout'         => 300,
            'connect_timeout' => 60
        ]);

        $body = json_decode($response->getBody()->getContents(), true);
        $data = $body['value'] ?? [];


        // ============================================================
        // HELPER untuk pastikan Qty valid decimal (anti-error bcadd)
        // ============================================================
        $cleanDecimal = function ($value) {
            if ($value === null) return "0";

            $v = trim((string)$value);

            if ($v === "" || $v === "-" || strtolower($v) === "nan") {
                return "0";
            }

            // hilangkan koma ribuan
            $v = str_replace(',', '', $v);

            // hanya izinkan angka decimal
            if (!preg_match('/^-?\d+(\.\d+)?$/', $v)) {
                return "0";
            }

            return $v;
        };


        // ============================================================
        // GROUPING — semua row masuk, lot kosong maupun tidak
        // ============================================================
        $grouped = [];

        foreach ($data as $row) {

            $itemNo = $row['ItemNo'];

            $qtyRaw = $cleanDecimal($row['Qty'] ?? 0);

            if (!isset($grouped[$itemNo])) {
                $grouped[$itemNo] = [
                    'ItemNo'               => $row['ItemNo'],
                    'ItemDescription'      => $row['ItemDescription'] ?? '',
                    'LocationCode'         => $row['LocationCode'] ?? '',
                    'BaseUoM'              => $row['BaseUoM'] ?? '',
                    'Replenishment_System' => $row['Replenishment_System'] ?? '',
                    'UoMCode'              => $row['UoMCode'] ?? '',
                    'QtyPerUoM'            => $row['QtyPerUoM'] ?? 1,

                    // simpan sebagai string utk akurasi bcadd
                    'TotalQty'             => "0",

                    // simpan semua row lots, kosong atau tidak
                    'lots'                 => []
                ];
            }

            // SUM QTY (presisi)
            $grouped[$itemNo]['TotalQty'] = bcadd(
                $grouped[$itemNo]['TotalQty'],
                $qtyRaw,
                10
            );

            // catat data lots mentah
         if ((float)$qtyRaw > 0) {
            $grouped[$itemNo]['lots'][] = [
                'Lot_No'          => array_key_exists('Lot_No', $row) ? $row['Lot_No'] : null,
                'Expiration_Date' => $row['Expiration_Date'] ?? null,
                'Qty'             => (float)$qtyRaw
            ];
        }
        }


        // ============================================================
        // KONVERSI TotalQty ke float (aman, tanpa rounding biner)
        // ============================================================
        foreach ($grouped as &$g) {
            $g['TotalQty'] = (float)$g['TotalQty'];
        }

        return [
            'status' => 'success',
            'count'  => count($grouped),
            'items'  => array_values($grouped)
        ];

    } catch (\Exception $e) {
        return [
            'status'  => 'error',
            'message' => $e->getMessage()
        ];
    }
}
public function ValueEntries()
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/ODataV4/Company('" . $this->companyId . "')/BudgetIn";

    $client = new \GuzzleHttp\Client([
        'timeout'         => 180,
        'connect_timeout' => 20,
    ]);

    try {

        $response = $client->get($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
            ],
        ]);

        $result = json_decode((string) $response->getBody(), true);
        $rows   = $result['value'] ?? [];

        // ============================
        // 1) Filter CI.1011
        // ============================
        $filtered = array_filter($rows, function ($row) {
            return ($row['LocationCode'] ?? '') === 'CI.1011';
        });

        // ============================
        // 2) Sum CostAmountActual
        // ============================
        $totalCost = array_reduce($filtered, function ($carry, $row) {
            return $carry + (float) ($row['CostAmountActual'] ?? 0);
        }, 0);

        // ============================
        // 3) Return final single object
        // ============================
        return [
            'LocationCode' => 'CI.1011',
            'TotalCost'    => $totalCost,
        ];

    } catch (\Throwable $e) {
        Log::error('BC ValueEntries error: '.$e->getMessage());
        return [
            'LocationCode' => 'CI.1011',
            'TotalCost'    => 0
        ];
    }
}

public function getpaymenyrequest()
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/5f138de3-8774-4114-8fc4-1a93dfc31a3a/Sandbox28112025/api/citbi/sku/v1.0/companies(68899f26-1900-ee11-8f70-000d3ac804e2)/paymentjournalCustom";
       
    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->get($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
        ]);

        $result = json_decode((string) $response->getBody(), true);
        return $result['value'] ?? [];

    } catch (\Throwable $e) {
        Log::error('BC error: '.$e->getMessage());
        return [];
    }
}


public function getStockUnitCost()
{
    // =========================================
    // ODATA FILTER — RBC LOCATIONS
    // =========================================
    $filter = urlencode(
        "(" .
            "Location_Code eq 'RBC.2100' or " .
            "Location_Code eq 'RBC.2110' or " .
            "Location_Code eq 'RBC.2120' or " .
            "Location_Code eq 'RBC.2130' or " .
            "Location_Code eq 'RBC.2140'" .
        ")"
    );

    $url =
        "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/ODataV4/Company('" . $this->companyId . "')"
        . "/HotelStockUnitCost?\$filter={$filter}";

    $headers = [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept'        => 'application/json',
            'Prefer'        => 'odata.maxpagesize=50000'
        ]
    ];

    $allData = [];
    $grouped = [];

    do {
        $response = $this->client->get($url, $headers);
        $decoded = json_decode((string) $response->getBody(), true);

        if (!empty($decoded['value'])) {
            foreach ($decoded['value'] as $row) {
                $location = $row['Location_Code'] ?? 'UNKNOWN';

                // simpan flat
                $allData[] = $row;

                // group per location
                if (!isset($grouped[$location])) {
                    $grouped[$location] = [];
                }
                $grouped[$location][] = $row;
            }
        }

        $url = $decoded['@odata.nextLink'] ?? null;

    } while ($url);

    return [
        'status'           => 'success',
        'total'            => count($allData),
        'locations'        => array_keys($grouped),
        'items_by_location'=> $grouped,
    ];
}
   public function ItemTracking()
    {
        // $filter = urlencode("(Location_Code eq 'CI.1012')");
        $filter = urlencode("(Location_Code eq 'CI.1011')");
        // $filter = urlencode("(Location_Code eq 'RBC.2040')");
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/ItemTrackingLine?";
        
        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer' => 'odata.maxpagesize=50000'
            ]
        ];
        $allData = [];
        do {
            $response = $this->client->get($url, $headers);
            $decoded = json_decode((string)$response->getBody(), true);

            if (isset($decoded['value'])) {
                $allData = array_merge($allData, $decoded['value']);
            }

            $url = $decoded['@odata.nextLink'] ?? null;
        } while ($url);
        return ['value' => $allData];
    }

    

public function getBudgetLost()
{
    $filter = urlencode("(LocationCode eq 'CI.1011')");

    $url = "https://api.businesscentral.dynamics.com/v2.0/" 
        . env('AZURE_TENANT_ID') . "/" 
        . env('BC_ENVIRONMENT')
        . "/ODataV4/Company('" . $this->companyId . "')/BudgetLost?\$filter={$filter}";

    $headers = [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept'        => 'application/json',
            'Prefer'        => 'odata.maxpagesize=50000'
        ]
    ];

    $allData = [];
    do {
        $response = $this->client->get($url, $headers);
        $decoded  = json_decode((string)$response->getBody(), true);

        if (isset($decoded['value'])) {
            $allData = array_merge($allData, $decoded['value']);
        }

        $url = $decoded['@odata.nextLink'] ?? null;

    } while ($url);


    // ================================
    // SUM PER LOCATION
    // ================================
    $summary = [];

    foreach ($allData as $row) {
        $loc  = $row['LocationCode'] ?? null;
        $cost = $row['CostAmountActual'] ?? 0;

        if (!$loc) continue;

        if (!isset($summary[$loc])) {
            $summary[$loc] = 0;
        }

        $summary[$loc] += $cost;
    }


    // ================================
    // ROUND KE 100 JUTA + POSITIF
    // ================================
    $result = [];
    $roundBase = 100_000_000;

    foreach ($summary as $loc => $total) {

        // rounding toward zero
        if ($total >= 0) {
            $rounded = floor($total / $roundBase) * $roundBase;
        } else {
            $rounded = ceil($total / $roundBase) * $roundBase;
        }

        // ubah ke nilai positif
        $roundedPositive = abs($rounded);

        $result[] = [
            'LocationCode'     => $loc,
            'TotalAmount'      => $total,
            'Rounded'          => $rounded,
            'RoundedPositive'  => $roundedPositive
        ];
    }

    return [
        'summary' => $result,
        'raw'     => $allData
    ];
}



public function Postedassemble()
    {
        // $filter = urlencode("(Location_Code eq 'CI.1012')");
        $filter = urlencode("(Location_Code eq 'CI.1011')");
        // $filter = urlencode("(Location_Code eq 'RBC.2040')");
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/PostAssembly?";
        
        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer' => 'odata.maxpagesize=1000'
            ]
        ];
        $allData = [];
        do {
            $response = $this->client->get($url, $headers);
            $decoded = json_decode((string)$response->getBody(), true);

            if (isset($decoded['value'])) {
                $allData = array_merge($allData, $decoded['value']);
            }

            $url = $decoded['@odata.nextLink'] ?? null;
        } while ($url);
        return $allData;
    }


    public function updateLineDimensionsStatic()
{
    // ======================================================
    // STATIC TEST DATA (SESUAI DATA YANG KAMU KIRIM)
    // ======================================================
    $journalTemplate = 'PAYMENTS';
    $batchName       = 'PCI2306002';
    $lineNo          = 30000;

    $accountno    = 'VFB0017';
    $shortcutDim2    = '';

    // ======================================================
    // BASE URL — LANGSUNG, JELAS, TIDAK DIPOTONG
    // ======================================================
    $baseUrl =
        "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0"
        . "/companies({$this->companyId})"
        . "/paymentjournalCustom";

    // ======================================================
    // GUZZLE CLIENT (SAMA POLA BOM)
    // ======================================================
    $client = new \GuzzleHttp\Client([
        'timeout' => 60,
        'connect_timeout' => 15,
    ]);

    try {
        // ======================================================
        // STEP 1: GET (PASTIKAN ENTITY ADA + AMBIL ETAG)
        // ======================================================
        $getUrl = $baseUrl
            . "?\$filter="
            . "JournalTemplateName eq '{$journalTemplate}'"
            . " and BatchName eq '{$batchName}'"
            . " and LineNo eq {$lineNo}";

        \Log::info('🔍 GET payment journal line', ['url' => $getUrl]);

        $getRes = $client->get($getUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
            ],
        ]);

        $decoded = json_decode((string)$getRes->getBody(), true);
        $rows    = $decoded['value'] ?? [];

        if (empty($rows)) {
            throw new \Exception('Payment journal line NOT FOUND');
        }

        $etag = $rows[0]['@odata.etag'] ?? null;

        if (!$etag) {
            throw new \Exception('ETag NOT FOUND');
        }

        // ======================================================
        // STEP 2: PATCH (KEY-BASED, BUKAN FILTER)
        // ======================================================
        $patchUrl = $baseUrl
            . "(JournalTemplateName='{$journalTemplate}',"
            . "BatchName='{$batchName}',"
            . "LineNo={$lineNo})";

        $payload = [
            'AccountNo' => $accountno,
            
        ];

        \Log::info('🧩 PATCH payment journal line', [
            'url'     => $patchUrl,
            'payload' => $payload,
            'etag'    => $etag,
        ]);

        $patchRes = $client->patch($patchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Content-Type'  => 'application/json',
                'If-Match'      => $etag,
                'Prefer'        => 'return=representation',
            ],
            'json' => $payload,
        ]);

        $result = json_decode((string)$patchRes->getBody(), true);

        \Log::error('✅ PATCH SUCCESS', [
            'accountno' => $result['AccountNo'] ?? null,
            'ShortcutDim2' => $result['ShortcutDim2'] ?? null,
        ]);

        return $result;

    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $body = $e->hasResponse()
            ? (string)$e->getResponse()->getBody()
            : $e->getMessage();

        \Log::error('❌ BC PATCH PAYMENT JOURNAL FAILED', [
            'error' => $body,
        ]);

        throw new \Exception("BC ERROR: {$body}");}
}

public function updatePaymentRequestVendorStatic()
{
    // ======================================================
    // STATIC TEST DATA (SESUAI YANG KAMU KIRIM)
    // ======================================================
    $batchNo  = 'PCI2408397';
    $vendorNo = 'VMT0202';

    // ======================================================
    // BASE URL — PAYMENT REQUEST CUSTOM
    // ======================================================
    $baseUrl =
        "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0"
        . "/companies({$this->companyId})"
        . "/paymentRequestCustom";

    // ======================================================
    // GUZZLE CLIENT
    // ======================================================
    $client = new \GuzzleHttp\Client([
        'timeout'         => 60,
        'connect_timeout' => 15,
    ]);

    try {
        // ======================================================
        // STEP 1: GET — AMBIL DATA + ETAG
        // ======================================================
        $getUrl = $baseUrl
            . "?\$filter=BatchNo eq '{$batchNo}'";

        \Log::info('🔍 GET payment request', [
            'url' => $getUrl
        ]);

        $getRes = $client->get($getUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
            ],
        ]);

        $decoded = json_decode((string) $getRes->getBody(), true);
        $rows    = $decoded['value'] ?? [];

        if (empty($rows)) {
            throw new \Exception("Payment request NOT FOUND for BatchNo {$batchNo}");
        }

        $etag = $rows[0]['@odata.etag'] ?? null;

        if (!$etag) {
            throw new \Exception('ETag NOT FOUND');
        }

        // ======================================================
        // STEP 2: PATCH — BY KEY (BatchNo)
        // ======================================================
        $patchUrl = $baseUrl . "(BatchNo='{$batchNo}')";

        $payload = [
            'VendorNo' => $vendorNo,
        ];

        \Log::info('🧩 PATCH payment request', [
            'url'     => $patchUrl,
            'payload' => $payload,
            'etag'    => $etag,
        ]);

        $patchRes = $client->patch($patchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Content-Type'  => 'application/json',
                'If-Match'      => $etag,
                'Prefer'        => 'return=representation',
            ],
            'json' => $payload,
        ]);

        $result = json_decode((string) $patchRes->getBody(), true);

        \Log::info('✅ PATCH PAYMENT REQUEST SUCCESS', [
            'BatchNo'  => $result['BatchNo'] ?? null,
            'VendorNo'=> $result['VendorNo'] ?? null,
        ]);

        return $result;

    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $body = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : $e->getMessage();

        \Log::error('❌ BC PATCH PAYMENT REQUEST FAILED', [
            'error' => $body,
        ]);

        throw new \Exception("BC ERROR: {$body}");
    }
}

public function printAssemblyLabel(string $documentNo): array
    {
        $url =
            "https://api.businesscentral.dynamics.com/v2.0/"
            . env('AZURE_TENANT_ID') . "/"
            . env('BC_ENVIRONMENT')
            . "/ODataV4/PrintLabel_PrintAssemblyLabel"
            . "?Company=PT.CI%20LIVE";

        try {
            $body = new \stdClass();
            $body->documentNo = $documentNo;

            $response = Http::withHeaders([
                'Authorization' => 'Bearer ' . $this->token,
                'Accept'        => 'application/json',
                'Content-Type'  => 'application/json',
            ])->post($url, $body);

            if ($response->failed()) {
                Log::error('[BC] PrintAssemblyLabel failed', [
                    'status' => $response->status(),
                    'body'   => $response->body()
                ]);

                throw new \Exception(
                    'BC PrintAssemblyLabel failed'
                );
            }

            return $response->json();

        } catch (\Throwable $e) {
            Log::error('[BC] PrintAssemblyLabel exception', [
                'message' => $e->getMessage()
            ]);
            throw $e;
        }
    }

        public function createPaymentJournalStatic(): array
    {
        // ===============================
        // STATIC VALUES (SESUAI PERMINTAAN)
        // ===============================
        $journalTemplateName = 'PAYMENTS';
        $batchName           = 'PCI2512060';
        $lineNo              = 10000;

        $accountType         = 'Vendor';
        $accountNo           = 'VFB0152';

        $postingDate         = Carbon::today()->format('Y-m-d');

        $documentType        = 'Payment';
        $documentNo          = 'BK.CI.25120002';

        // ===============================
        // BC API URL
        // ===============================
        $url =
            "https://api.businesscentral.dynamics.com/v2.0/"
            . env('AZURE_TENANT_ID') . "/"
            . env('BC_ENVIRONMENT')
            . "/api/citbi/sku/v1.0"
            . "/companies({$this->companyId})"
            . "/paymentjournalCustom";

        // ===============================
        // REQUEST BODY
        // (PER FIELD, JELAS)
        // ===============================
        $body = [
            'JournalTemplateName' => $journalTemplateName,
            'BatchName'           => $batchName,
            'LineNo'              => $lineNo,

            'AccountType'         => $accountType,
            'AccountNo'           => $accountNo,

            'PostingDate'         => $postingDate,

            'DocumentType'        => $documentType,
            'DocumentNo'          => $documentNo,
        ];

        try {
            $response = Http::withHeaders([
                'Authorization' => 'Bearer ' . $this->token,
                'Accept'        => 'application/json',
                'Content-Type'  => 'application/json',
            ])->post($url, $body);

            if ($response->failed()) {
                Log::error('[BC] Create Payment Journal failed', [
                    'url'    => $url,
                    'status' => $response->status(),
                    'body'   => $response->body(),
                ]);

                throw new \Exception(
                    'Failed to create payment journal'
                );
            }

            return $response->json();

        } catch (\Throwable $e) {
            Log::error('[BC] Create Payment Journal exception', [
                'message' => $e->getMessage(),
            ]);

            throw $e;
        }
    }

}
