# BUY_PRICE (Harga Beli / Cost Tracking) - Source of Truth

**Last Updated:** 2026-06-08  
**Model Class:** `common\models\BuyPrice`  
**Database Table:** `buy_price`

---

## 1. OVERVIEW & ARSITEKTUR

### 1.1 Tujuan dan Peran Global

Model `BuyPrice` adalah **historical cost tracker** yang menyimpan riwayat harga beli per item per cabang. Sistem ini dirancang untuk:

- **Track Harga Beli Historical** dari berbagai sumber transaksi (pembelian, penerimaan, WIP, dll)
- **Support Multiple Costing Methods** seperti FIFO, Weighted Average, dan Latest Purchase
- **Link dengan Stok Records** untuk COGS calculation dan inventory valuation
- **Enable Accurate Financial Reporting** dengan harga yang relevan per periode/tanggal
- **Support Unit Conversion** untuk handling berbagai satuan pengukuran

### 1.2 Relasi dengan Stok Model

```
Stok Record
    ├─ qty: Quantity delta
    ├─ latest_qty: Running balance
    ├─ buy_price_id: FOREIGN KEY → BuyPrice.id
    └─ type: Transaction type (gr, purchase, wip, sales, etc)

BuyPrice Record (linked via buy_price_id)
    ├─ harga_beli: Unit cost at time of transaction
    ├─ conversion: Unit conversion factor
    ├─ type: Source of price (SPP, PTY, DR, PENERIMAAN, WIP, SALES, GR)
    ├─ applies_date: When this price becomes applicable
    └─ no_ref: Reference to source document (PO, WIP ID, etc)
```

**Critical Design Decision:**
- Harga beli adalah **snapshot per transaction** (tied to Stok record)
- Bukan master data yang berubah (tidak seperti master price list)
- Memungkinkan COGS calculation yang akurat even jika harga berubah

### 1.3 Struktur Database Schema

| Column | Type | Role | Notes |
|--------|------|------|-------|
| `id` | INT PRIMARY KEY | Unique identifier | Auto-increment |
| `barang_id` | INT FOREIGN KEY | Item reference | Links to `barang` table |
| `cabang_id` | INT FOREIGN KEY | Branch/warehouse | Links to `cabang` table |
| `harga_beli` | DOUBLE | Unit cost | Price per unit (dalam currency default: IDR) |
| `conversion` | DOUBLE | Conversion factor | To handle unit conversion (e.g., kg → gram) |
| `no_ref` | INT | Reference ID | Links to source document (PO ID, WIP ID, etc) |
| `type` | VARCHAR | Source type | SPP, PTY, DR, PENERIMAAN, WIP, SALES, GR |
| `applies_date` | DATETIME | Effective date | When this price is effective |
| `pembelian_id` | INT | (Deprecated/Legacy) | Old purchase order reference |
| `created_at` | DATETIME | Audit timestamp | When record created |
| `updated_at` | DATETIME | Audit timestamp | When record updated |
| `deleted_at` | DATETIME | Soft delete flag | For logical deletion |

### 1.4 Transaction Type Constants

```php
const TYPE_SPP = "SPP";              // Supplier Price Quote
const TYPE_PETTY_CASH = "PTY";       // Petty Cash Purchase
const TYPE_DIRECT_RECEIVE = "DR";    // Direct Receive (without PO)
const TYPE_PENERIMAAN = "PENERIMAAN"; // Formal Goods Receipt
const TYPE_WIP = "WIP";              // Work In Progress (manufactured)
const TYPE_SALES = "SALES";          // Sales-derived cost (for addons)
const TYPE_GR = "GR";                // Goods Receive (from PO)
```

**Type Patterns:**
- **Purchase-driven:** SPP, PTY, DR, PENERIMAAN, GR (external acquisition)
- **Manufacturing:** WIP (internal production cost)
- **Sales-driven:** SALES (for addon items in sales)

### 1.5 Usage Flow Diagram

```
Source Transaction
    ├─ Purchase Order (PO)
    │   └─ Create BuyPrice with TYPE_GR
    │       └─ Create Stok with buy_price_id
    │
    ├─ Direct Receive (DR)
    │   └─ Create BuyPrice with TYPE_DR
    │       └─ Create Stok with buy_price_id
    │
    ├─ WIP Manufacturing
    │   ├─ Calculate cost from recipe
    │   └─ Create BuyPrice with TYPE_WIP (valueWipPerUnit)
    │       └─ Create Stok with buy_price_id
    │
    └─ Sales Transaction
        ├─ Fetch latest BuyPrice for COGS
        └─ Use harga_beli * qty for cost calculation

Query for Cost:
    Stok.getCostByDate()
        → Join with BuyPrice
        → Return harga_beli per unit
        → Used in COGS calculation
```

---

## 2. CORE MECHANISMS & PATTERNS

### 2.1 Creating BuyPrice Records

BuyPrice records **harus dibuat sebelum atau bersamaan dengan Stok record** untuk tracking yang akurat.

#### Pattern A: Direct Purchase/Receipt

```php
// When receiving goods from supplier
$mBuy = new BuyPrice();
$mBuy->barang_id = $barangId;
$mBuy->cabang_id = $cabangId;
$mBuy->harga_beli = 50000;  // IDR per unit
$mBuy->conversion = 1;       // 1 unit = 1 kg (or adjust as needed)
$mBuy->no_ref = $poId;       // Reference to Purchase Order
$mBuy->applies_date = date("Y-m-d H:i:s");
$mBuy->type = BuyPrice::TYPE_GR;  // Goods Receipt
if ($mBuy->save(false)) {
    // Then create Stok record linking to this BuyPrice
    $stok = new Stok();
    $stok->barang_id = $barangId;
    $stok->subwarehouse_id = $subwarehouseId;
    $stok->qty = 100;
    $stok->type = Stok::TYPE_PURCHASE;
    $stok->buy_price_id = $mBuy->id;  // ← LINK
    $stok->finalSave();
}
```

**Important:** Create BuyPrice first, save to get ID, then reference it in Stok.

#### Pattern B: WIP Manufacturing

```php
// From MyStockManagement::saveWIP()
$mBuy = new BuyPrice();
$mBuy->barang_id = $wipBarangId;
$mBuy->cabang_id = $cabangId;
$mBuy->harga_beli = $valueWipPerUnit;  // Calculated cost
$mBuy->no_ref = $wipId;                // Reference to WIP record
$mBuy->applies_date = date("Y-m-d H:i:s");
$mBuy->type = BuyPrice::TYPE_WIP;
if ($mBuy->save()) {
    // Link to Stok for finished goods stock
    $ms = new Stok();
    $ms->barang_id = $wipBarangId;
    $ms->subwarehouse_id = $subwarehouseId;
    $ms->qty = $quantityProduced;
    $ms->type = Stok::TYPE_WIP;
    $ms->buy_price_id = $mBuy->id;  // ← LINK
    $ms->finalSave();
}
```

#### Pattern C: Sales Addon

```php
// From MyStockManagement::insertBuyPrice()
$mBuy = new BuyPrice();
$mBuy->barang_id = $addonBarangId;
$mBuy->cabang_id = $cabangId;
$mBuy->harga_beli = $valueWip;  // Cost of addon
$mBuy->no_ref = $salesOrderId;
$mBuy->applies_date = date("Y-m-d H:i:s");
$mBuy->type = BuyPrice::TYPE_SALES;
if ($mBuy->save(false)) {
    // Create consumption record for addon
    self::insertStock($addonBarangId, $cabangId, $parentName, $porsiPerItem, 
                     $salesOrderId, Stok::TYPE_SALES, $mBuy->id);
}
```

### 2.2 Retrieving Cost: `getHargaBeliByBarangId()` Method

Core method di `MyStockManagement` untuk fetch harga beli dengan **weighted average costing**.

#### Algorithm Overview

```php
public static function getHargaBeliByBarangId(
    $brgId,           // Barang ID
    $cbgId,           // Cabang ID (optional)
    $asOfDate = null  // As-of date for historical cost
)
```

#### Step-by-Step Logic

```
1. Normalize & Validate Date Input
   - Support format: Y-m-d, Y-m-d H:i:s, Y-m-d H:i:s.ffffff
   - Default to current timestamp if not provided

2. Get Current Stock Balance (latest_qty)
   - Query Stok where barang_id & <= asOfDate
   - Order by used_at DESC to get most recent
   - Extract latest_qty (running balance)

3. Query Stock Records (LEFT JOIN with BuyPrice)
   - Link Stok → BuyPrice via buy_price_id
   - Filter: barang_id, used_at <= asOfDate
   - Filter: type IN [TYPE_PETTY_CASH, TYPE_GOODS_RECEIVE] (cost records)
   - Exclude: deleted_at IS NOT NULL
   - Order: used_at DESC (newest first)
   - Limit: 5 records (most recent purchases)

4. Group by buy_price_id
   - Create dictionary of unique purchases
   - Store: harga_beli, conversion, applies_date
   - Calculate: sisa_stock (remaining quantity)

5. Calculate Average Cost (if positive stock)
   - IF current stock > 0:
     * Use weighted average method
     * Sum up (harga_beli * qty) for all active purchases
     * Divide by number of purchases
   - IF current stock <= 0:
     * Use latest purchase price
     * Return most recent harga_beli

6. Return Result Array
   - harga_beli: Average cost per unit
   - latest_qty: Current stock quantity
   - jumlah_pembelian_aktif: Number of purchases in average
   - details: Array of each purchase detail
   - is_average_cost: Boolean (true if multiple purchases)
```

#### Example Usage

```php
// Get current cost
$cost = MyStockManagement::getHargaBeliByBarangId($barangId, $cabangId);
if ($cost) {
    echo "Unit Cost: Rp " . number_format($cost['harga_beli'], 0);
    echo "Current Stock: " . $cost['latest_qty'];
    echo "Method: " . ($cost['is_average_cost'] ? "Weighted Average" : "Latest");
}

// Get cost as of specific date (for month-end valuation)
$costAsOf = MyStockManagement::getHargaBeliByBarangId(
    $barangId,
    $cabangId,
    '2024-06-30 23:59:59'
);

// Get cost without branch filter
$costAllBranches = MyStockManagement::getHargaBeliByBarangId(
    $barangId,
    null,  // No cabang filter
    '2024-06-30'
);
```

#### Return Value Structure

```php
[
    'barang_id' => 5,
    'harga_beli' => 50000,                    // Per unit cost
    'latest_qty' => 150,                      // Current balance
    'jumlah_pembelian_aktif' => 2,            // How many POs in average
    'as_of_date' => '2024-06-30 23:59:59',    // Date queried
    'is_average_cost' => true,                // Weighted average vs latest
    'details' => [                            // Per-purchase breakdown
        [
            'buy_price_id' => 42,
            'applies_date' => '2024-06-01 10:00:00',
            'harga_beli' => 48000,
            'conversion' => 1,
            'sisa_stock' => 100,
            'nilai_stock' => 4800000
        ],
        [
            'buy_price_id' => 43,
            'applies_date' => '2024-06-15 14:30:00',
            'harga_beli' => 52000,
            'conversion' => 1,
            'sisa_stock' => 50,
            'nilai_stock' => 2600000
        ]
    ]
]
```

---

## 3. BEST PRACTICES & BOILERPLATE

### 3.1 CORRECT Usage Patterns

#### ✅ Pattern 1: BuyPrice + Stok in Sequence

```php
// CORRECT: BuyPrice first, then Stok
$buyPrice = new BuyPrice();
$buyPrice->barang_id = $barangId;
$buyPrice->cabang_id = $cabangId;
$buyPrice->harga_beli = 45000;
$buyPrice->type = BuyPrice::TYPE_GR;
$buyPrice->no_ref = $poId;
$buyPrice->save(false);

// Now use the saved ID
$stok = new Stok();
$stok->barang_id = $barangId;
$stok->subwarehouse_id = $subwarehouseId;
$stok->qty = 100;
$stok->buy_price_id = $buyPrice->id;  // ← Reference saved ID
$stok->finalSave();
```

#### ✅ Pattern 2: Batch Cost Tracking

```php
public function processPurchaseOrder($poId)
{
    $poItems = PurchaseOrderItem::find()
        ->where(['po_id' => $poId])
        ->all();

    $buyPrices = [];  // Track for linking

    // Step 1: Create all BuyPrice records
    foreach ($poItems as $item) {
        $bp = new BuyPrice();
        $bp->barang_id = $item->barang_id;
        $bp->cabang_id = $item->cabang_id;
        $bp->harga_beli = $item->unit_price;
        $bp->conversion = $item->conversion ?? 1;
        $bp->type = BuyPrice::TYPE_GR;
        $bp->no_ref = $poId;
        $bp->applies_date = date("Y-m-d H:i:s");
        $bp->save(false);

        $buyPrices[$item->barang_id] = $bp->id;
    }

    // Step 2: Create Stok records linking to BuyPrice
    foreach ($poItems as $item) {
        $stok = new Stok();
        $stok->barang_id = $item->barang_id;
        $stok->subwarehouse_id = $item->subwarehouse_id;
        $stok->qty = $item->qty;
        $stok->type = Stok::TYPE_PURCHASE;
        $stok->buy_price_id = $buyPrices[$item->barang_id];
        $stok->id_ref = $poId;
        $stok->finalSave();
    }
}
```

#### ✅ Pattern 3: Cost Retrieval with Fallback

```php
public function getCostSafe($barangId, $cabangId, $asOfDate = null)
{
    // Try get cost from BuyPrice tracking
    $result = MyStockManagement::getHargaBeliByBarangId(
        $barangId,
        $cabangId,
        $asOfDate
    );

    if ($result && isset($result['harga_beli'])) {
        return $result['harga_beli'];
    }

    // Fallback: Get from BarangStok.last_harga_beli
    $barAngStok = BarangStok::find()
        ->where(['barang_id' => $barangId, 'cabang_id' => $cabangId])
        ->one();

    if ($barAngStok && $barAngStok->last_harga_beli) {
        return $barAngStok->last_harga_beli;
    }

    // Final fallback: Return 0
    return 0;
}
```

### 3.2 ❌ WRONG Patterns to Avoid

#### ❌ DON'T: Update harga_beli in existing records

```php
// WRONG: Modifying historical cost
$buyPrice = BuyPrice::findOne($id);
$buyPrice->harga_beli = 60000;  // ← NO! This corrupts history
$buyPrice->save();
```

**Why:** BuyPrice adalah snapshot. Jika harga berubah, buat record baru, jangan update yang ada.

#### ❌ DON'T: Create BuyPrice without Stok linkage

```php
// WRONG: Orphaned BuyPrice record
$bp = new BuyPrice();
$bp->barang_id = $barangId;
$bp->harga_beli = 50000;
$bp->save();
// No Stok record created → orphaned cost entry
```

**Why:** BuyPrice adalah transaction-level cost, harus linked ke Stok record.

#### ❌ DON'T: Bypass conversion factor

```php
// WRONG: Ignoring unit conversion
$buyPrice->harga_beli = 100;    // Price per 10kg?
$buyPrice->conversion = 1;      // Says per 1 unit, but actually 10kg
// → Downstream calculations will be incorrect
```

**Why:** Conversion factor critical untuk accurate cost calculation.

### 3.3 Validation Rules

```php
public function rules()
{
    return [
        // Required fields
        [['barang_id', 'cabang_id'], 'required'],
        
        // Data types
        [['barang_id', 'cabang_id', 'no_ref'], 'integer'],
        [['harga_beli', 'conversion'], 'number'],
        [['type'], 'string'],
        
        // Relationships
        [['barang_id'], 'exist', 'targetClass' => Barang::class],
        [['cabang_id'], 'exist', 'targetClass' => Cabang::class],
        
        // Safe (Yii will auto-populate)
        [['applies_date', 'created_at', 'updated_at', 'deleted_at'], 'safe'],
    ];
}
```

**Pre-validation Pattern:**
```php
$buyPrice = new BuyPrice(['scenario' => 'create']);
if (!$buyPrice->validate()) {
    throw new \Exception(json_encode($buyPrice->getErrors()));
}
$buyPrice->save(false);  // Skip validation again
```

### 3.4 Behavioral Hooks

```php
public function behaviors()
{
    return [
        MyBehavior::timestampBehavior('created_at', 'updated_at'),
        // No BlameableBehavior - cost tracking is system-level, not user action
    ];
}
```

**Auto-populated Fields:**
- `created_at`: Set on insert
- `updated_at`: Set on insert/update
- `deleted_at`: Manual (soft delete for audit trail)

### 3.5 Soft Delete for Audit Trail

```php
// Correct way: Soft delete (mark deleted_at)
$buyPrice = BuyPrice::findOne($id);
$buyPrice->deleted_at = date('Y-m-d H:i:s');
$buyPrice->save();

// Query still correctly filters deleted records
$activeCosts = BuyPrice::find()
    ->where(['barang_id' => $barangId])
    ->andWhere(['deleted_at' => null])
    ->all();
```

**Why soft delete?** Maintains audit trail while excluding from active queries.

---

## 4. INTEGRATION WITH COGS & FINANCIAL REPORTING

### 4.1 COGS Calculation Pattern

Cost of Goods Sold calculation relies heavily on BuyPrice.

```php
// From MyStockManagement::getCogs()
public static function getCogs($startSalesId, $endSalesId)
{
    $saleItems = PenjualanItem::find()
        ->innerJoin('penjualan', 'penjualan.id = penjualan_item.penjualan_id')
        ->select(['harga_beli', 'qty', 'j_addon'])
        ->where(['penjualan.delete' => 'N'])
        ->andWhere(['between', 'penjualan_id', $startSalesId, $endSalesId])
        ->asArray()
        ->all();

    $totalCogs = 0;
    foreach ($saleItems as $item) {
        // harga_beli in PenjualanItem was populated from BuyPrice
        $totalCogs += $item['harga_beli'] * $item['qty'];
        
        // Note: Addon costs already included in harga_beli,
        // don't double-count j_addon
    }
    return $totalCogs;
}
```

**Flow:**
```
PurchaseOrder (with harga beli)
    ↓
BuyPrice created with unit cost
    ↓
Stok record linked via buy_price_id
    ↓
Sales transaction uses Stok cost tracking
    ↓
PenjualanItem.harga_beli populated from BuyPrice
    ↓
getCogs() calculates total cost
    ↓
Financial report: Revenue - COGS = Gross Profit
```

### 4.2 Weighted Average Cost Example

```
Timeline:
  2024-06-01: Buy 100 units @ 50,000 → BuyPrice ID#1
  2024-06-10: Buy 50 units @ 52,000  → BuyPrice ID#2
  2024-06-20: Use 120 units

Stock Balance on 2024-06-20:
  - After PO1: 100 units
  - After PO2: 150 units
  - After usage: 30 units

Weighted Average Cost Calculation:
  - PO1 portion: 100 units @ 50,000 = 5,000,000
  - PO2 portion: 50 units @ 52,000 = 2,600,000
  - Total value: 7,600,000
  - Total units: 150
  - Average cost: 7,600,000 / 150 = 50,666.67 per unit

COGS for 120 units used:
  - 120 × 50,666.67 = 6,080,000

Remaining inventory (30 units):
  - 30 × 50,666.67 = 1,520,000
```

---

## 5. SPECIAL CASES & SCENARIOS

### 5.1 Unit Conversion (Conversion Factor)

BuyPrice supports unit conversion untuk handling berbagai satuan.

```php
// Example: Item dijual per kg, tapi dibeli per box (5 kg)
$buyPrice = new BuyPrice();
$buyPrice->barang_id = $itemId;
$buyPrice->harga_beli = 200000;   // Price per box
$buyPrice->conversion = 5;         // 1 unit (box) = 5 kg

// When querying:
$cost = MyStockManagement::getHargaBeliByBarangId($itemId, $cabangId);
// Result: harga_beli = 200000 / 5 = 40,000 per kg (internally adjusted)
```

**Usage Pattern:**
```php
// In cost calculation
$unitCost = $buyPrice['harga_beli'];
$conversionFactor = $buyPrice['details'][0]['conversion'];
$costPerBaseUnit = $unitCost / $conversionFactor;
```

### 5.2 WIP Cost Calculation

For manufactured items, BuyPrice stores calculated cost:

```php
// Step 1: Calculate recipe cost
$recipeCost = MyStockManagement::calcCostParentOrAddon(
    $finishedGood,
    $cabangId,
    $wipId,
    $asOfDate
);
// Returns: ['value' => 125000, 'value_response' => [...]]

// Step 2: Create BuyPrice with WIP cost
$buyPrice = new BuyPrice();
$buyPrice->barang_id = $finishedGoodId;
$buyPrice->harga_beli = $recipeCost['value'];  // Calculated cost
$buyPrice->type = BuyPrice::TYPE_WIP;
$buyPrice->no_ref = $wipId;
$buyPrice->save();

// Step 3: Link to Stok with produced quantity
$stok = new Stok();
$stok->buy_price_id = $buyPrice->id;
$stok->qty = $quantityProduced;
$stok->type = Stok::TYPE_WIP;
$stok->finalSave();
```

### 5.3 Negative Stock Scenario

When stock goes negative (overselling), use latest purchase price:

```php
// In getHargaBeliByBarangId()
if ($qtyLatestStock <= 0 && !empty($purchaseByBuyPriceId)) {
    // Stock is negative/zero
    // Use most recent purchase price
    $latestPurchase = reset($purchaseByBuyPriceId);  // First element (DESC ordered)
    $averageHargaBeli = round((float)$latestPurchase['harga_beli'], 2);
}
```

**Why:** Mencegah invalid cost calculation ketika stock oversold.

---

## 6. RELATIONSHIP WITH OTHER MODELS

### 6.1 Relations

```php
public function getBarang()      // BuyPrice → Barang (product)
public function getCabang()      // BuyPrice → Cabang (branch)
```

### 6.2 Referenced By

```
BuyPrice ← Stok.buy_price_id (FOREIGN KEY)
        ← PenjualanItem.harga_beli (stored value, historical)
        ← BarangStok.last_harga_beli (latest, denormalized)
        ← MyStockManagement (cost calculations)
```

### 6.3 Model Relationships Diagram

```
Barang (Product Master)
    ├─ Stok (Transactions)
    │   └─ buy_price_id → BuyPrice
    │
    ├─ BarangStok (Per-Branch Config)
    │   └─ last_harga_beli (denormalized from BuyPrice)
    │
    └─ BuyPrice (Cost History)
        └─ harga_beli (per transaction)

PenjualanItem (Sales Details)
    └─ harga_beli (snapshot from BuyPrice at sale time)

Cabang (Branch/Warehouse)
    └─ BuyPrice (per branch tracking)
```

---

## 7. QUERY PATTERNS & COMMON OPERATIONS

### 7.1 Get Latest Cost

```php
public static function getLatestCost($barangId, $cabangId)
{
    return BuyPrice::find()
        ->where(['barang_id' => $barangId, 'cabang_id' => $cabangId])
        ->andWhere(['deleted_at' => null])
        ->orderBy('applies_date desc')
        ->one();
}

// Usage
$latestCost = BuyPrice::getLatestCost($barangId, $cabangId);
if ($latestCost) {
    echo "Latest cost: " . $latestCost->harga_beli;
    echo "Applied date: " . $latestCost->applies_date;
}
```

### 7.2 Get Cost History (Audit Trail)

```php
public static function getCostHistory($barangId, $cabangId, $startDate, $endDate)
{
    return BuyPrice::find()
        ->where(['barang_id' => $barangId, 'cabang_id' => $cabangId])
        ->andWhere(['between', 'applies_date', $startDate, $endDate])
        ->orderBy('applies_date desc')
        ->all();
}

// Usage: Show cost variations over time
$history = BuyPrice::getCostHistory($barangId, $cabangId, '2024-01-01', '2024-12-31');
foreach ($history as $record) {
    echo "{$record->applies_date}: Rp {$record->harga_beli}\n";
}
```

### 7.3 Get Cost by Type (Filter by Source)

```php
public static function getCostByType($barangId, $cabangId, $type)
{
    return BuyPrice::find()
        ->where(['barang_id' => $barangId, 'cabang_id' => $cabangId, 'type' => $type])
        ->andWhere(['deleted_at' => null])
        ->orderBy('applies_date desc')
        ->one();
}

// Usage: Get only GR (Goods Receipt) costs, exclude others
$grCost = BuyPrice::getCostByType($barangId, $cabangId, BuyPrice::TYPE_GR);
```

### 7.4 Get Active Costs (for current valuation)

```php
public static function getActiveCosts($barangId)
{
    $stokItems = Stok::find()
        ->where(['barang_id' => $barangId])
        ->andWhere(['is not', 'buy_price_id', null])
        ->distinct('buy_price_id')
        ->column();

    return BuyPrice::find()
        ->where(['in', 'id', $stokItems])
        ->andWhere(['deleted_at' => null])
        ->orderBy('applies_date desc')
        ->all();
}

// Usage: Get all costs that have active stock
$activeCosts = BuyPrice::getActiveCosts($barangId);
```

---

## 8. PERFORMANCE CONSIDERATIONS

### 8.1 Indexing Strategy

**Recommended Indexes:**
```sql
-- Primary lookups
CREATE INDEX idx_buyprice_barang_cabang ON buy_price(barang_id, cabang_id, applies_date DESC);

-- Date range queries
CREATE INDEX idx_buyprice_applies_date ON buy_price(applies_date DESC);

-- Cost tracking
CREATE INDEX idx_buyprice_type ON buy_price(type, applies_date DESC);

-- Soft delete filtering
CREATE INDEX idx_buyprice_deleted ON buy_price(barang_id, deleted_at);
```

### 8.2 Query Optimization

**AVOID:**
```php
// ❌ N+1 Query: Buy Price fetched per Stok record
$stoks = Stok::find()->where(['barang_id' => $barangId])->all();
foreach ($stoks as $stok) {
    if ($stok->buy_price_id) {
        $cost = $stok->buyPrice->harga_beli;  // ← Extra query
    }
}

// ✅ GOOD: Eager load BuyPrice
$stoks = Stok::find()
    ->where(['barang_id' => $barangId])
    ->joinWith('buyPrice')
    ->all();
foreach ($stoks as $stok) {
    if ($stok->buyPrice) {
        $cost = $stok->buyPrice->harga_beli;  // ← No extra query
    }
}
```

### 8.3 Caching Strategy

```php
// Cache latest cost per item
public static function getCachedLatestCost($barangId, $cabangId)
{
    $cacheKey = "buyprice_latest_{$barangId}_{$cabangId}";
    
    $cost = Yii::$app->cache->get($cacheKey);
    if ($cost === false) {
        $cost = BuyPrice::find()
            ->where(['barang_id' => $barangId, 'cabang_id' => $cabangId])
            ->andWhere(['deleted_at' => null])
            ->orderBy('applies_date desc')
            ->one();
        
        Yii::$app->cache->set($cacheKey, $cost, 3600);  // 1 hour cache
    }
    
    return $cost;
}

// Invalidate cache when new price created
public function afterSave($insert, $changedAttributes)
{
    parent::afterSave($insert, $changedAttributes);
    
    if ($insert) {
        $cacheKey = "buyprice_latest_{$this->barang_id}_{$this->cabang_id}";
        Yii::$app->cache->delete($cacheKey);  // Invalidate
    }
}
```

---

## 9. COMMON PITFALLS & TROUBLESHOOTING

| Pitfall | Symptom | Solution |
|---------|---------|----------|
| BuyPrice created but no Stok link | Orphaned cost entry | Always create Stok immediately after BuyPrice |
| Updating harga_beli on existing record | Historical data corrupted | NEVER update. Create new record instead |
| Missing conversion factor | Wrong cost per unit | Always set conversion, default to 1 if not applicable |
| Not filtering deleted_at in queries | Old costs included in calculations | Use `andWhere(['deleted_at' => null])` |
| Multiple purchases same timestamp | Wrong average cost calculation | Use `applies_date` descending for deterministic ordering |
| No_ref not populated | Can't trace cost back to source | Always set no_ref to source document ID |
| Negative stock using wrong cost | COGS overstated/understated | getHargaBeliByBarangId() handles this via latest price fallback |

---

## 10. INTEGRATION CHECKLIST

When implementing cost tracking in new features:

- [ ] Create BuyPrice record with correct `type` constant
- [ ] Populate: `barang_id`, `cabang_id`, `harga_beli`, `conversion`
- [ ] Set: `no_ref` (source document ID), `applies_date` (current time)
- [ ] Save BuyPrice, get ID
- [ ] Create Stok record with `buy_price_id` link
- [ ] Handle soft delete: Always check `deleted_at IS NULL`
- [ ] Test: Verify cost appears in COGS calculations
- [ ] Monitor: Alert on cost outliers (sudden price jumps)
- [ ] Document: Add comments explaining cost basis for unusual prices

---

## 11. REFERENCES & RELATED COMPONENTS

| Related Component | Purpose | File |
|------------------|---------|------|
| `Stok` | Stock transaction tracking | `common/models/Stok.php` |
| `MyStockManagement` | Cost calculation & retrieval | `common/base/MyStockManagement.php` |
| `BarangStok` | Per-branch item config (last_harga_beli) | `common/models/BarangStok.php` |
| `Barang` | Product master | `common/models/Barang.php` |
| `BarangRecipe` | Recipe/BOM for WIP costing | `common/models/BarangRecipe.php` |
| `PenjualanItem` | Sales detail (harga_beli snapshot) | `common/models/PenjualanItem.php` |
| `Pembelian` | Purchase order header | `common/models/Pembelian.php` |

---

**Document Status:** PRODUCTION  
**Last Reviewed:** 2026-06-08  
**Maintainer:** Tech Lead - Financial & Inventory  
**See Also:** `docs/features/STOCK.md`, `docs/COST_MANAGEMENT_CONNECTED_ITEMS.md`
