InvoiceStatus::class, 'issue_date' => 'date', 'due_date' => 'date', 'paid_date' => 'date', 'subtotal' => 'decimal:2', 'tax_rate' => 'decimal:2', 'tax_amount' => 'decimal:2', 'total' => 'decimal:2', ]; public function client(): BelongsTo { return $this->belongsTo(Client::class); } public function items(): HasMany { return $this->hasMany(InvoiceItem::class); } public function recalculate(): void { // Total of all items $this->subtotal = $this->items->sum('total'); $discountAmount = 0; if ($this->discount_rate > 0) { $discountableSubtotal = $this->items->where('is_discountable', true)->sum('total'); $discountAmount = round((float) $discountableSubtotal * ($this->discount_rate / 100), 2); } $this->subtotal = $this->subtotal - $discountAmount; // Tax is calculated only on taxable items $taxableSubtotal = 0; foreach ($this->items as $item) { if ($item->is_taxable) { if ($this->discount_rate > 0 && $item->is_discountable) { $taxableSubtotal += $item->total * (1 - $this->discount_rate / 100); } else { $taxableSubtotal += $item->total; } } } $this->tax_amount = round((float) $taxableSubtotal * ($this->tax_rate / 100), 2); // Grand total = subtotal (tax is borne by client separately) $this->total = $this->subtotal; $this->save(); } public function getFormattedTotalAttribute(): string { return 'Rp ' . number_format((float) $this->total, 0, ',', '.'); } public function getDiscountAmountAttribute(): float { if ($this->discount_rate <= 0) return 0.0; $discountableSubtotal = $this->items->where('is_discountable', true)->sum('total'); return round((float) $discountableSubtotal * ($this->discount_rate / 100), 2); } public static function generateNumber(): string { return InvoiceSettings::generateNumber(); } }