Files
2026-09-11 18:06:17 +07:00

61 lines
22 KiB
PHP

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
require_once APPPATH.'exceptions/BusinessException.php';
class ReceivableService
{
private $CI;
public function __construct(){
$this->CI=&get_instance();$this->CI->load->library(array('TransactionService','ApprovalService','AuditService','PostingService','NumberingService','AccountMappingService','FiscalPeriodService','CompanyContext'));$this->CI->load->model('InvoiceModel','receivableInvoiceModel');$this->CI->load->model('InventoryModel','receivableInventoryModel');
}
public function submitInvoice($id,$userId){return $this->CI->transactionservice->run(function()use($id,$userId){$i=$this->invoiceLock($id,'draft');$this->validateInvoice($i);$this->assertCreditLimit($i);$amount=$this->invoiceTotal($id);$r=$this->CI->approvalservice->create('invoice','invoice',$id,$amount,$userId,array('invoice'=>$i,'lines'=>$this->lines($id)));$this->CI->db->where('id',$id)->update('invoices',array('workflow_status'=>'submitted','total'=>$amount,'sisa_piutang'=>$amount));$after=$this->CI->receivableInvoiceModel->find($id);$this->CI->auditservice->record('invoice','invoice',$id,'submit',$i,$after,$userId);return $r;});}
public function approveInvoice($id,$userId,$roleId){return $this->CI->transactionservice->run(function()use($id,$userId,$roleId){$i=$this->invoiceLock($id,'submitted');$r=$this->CI->approvalservice->pendingForEntity('invoice',$id);if(!$r)throw new BusinessException('Approval invoice tidak ditemukan.');$a=$this->CI->approvalservice->act($r->id,'approved',$userId,$roleId);if($a->status==='approved')$this->CI->db->where('id',$id)->update('invoices',array('workflow_status'=>'approved'));$after=$this->CI->receivableInvoiceModel->find($id);$this->CI->auditservice->record('invoice','invoice',$id,'approve',$i,$after,$userId);return $after;});}
public function postInvoice($id,$userId){return $this->CI->transactionservice->run(function()use($id,$userId){$i=$this->invoiceLock($id,'approved');$this->CI->fiscalperiodservice->assertOpen($i->tanggal);if($i->journal_id)throw new BusinessException('Invoice sudah mempunyai jurnal.');$lines=$this->lines($id);$ar=$this->CI->accountmappingservice->get('accounts_receivable');$taxAccount=$this->CI->accountmappingservice->get('sales_tax_payable');$entries=array(array('account_id'=>$ar,'debit'=>$i->total,'kredit'=>0));$credits=array();$hpp=0;foreach($lines as $l){$net=(float)$l->subtotal-(float)$l->tax_amount;$credits[$l->account_id]=($credits[$l->account_id]??0)+$net;if((float)$l->tax_amount>0)$credits[$taxAccount]=($credits[$taxAccount]??0)+(float)$l->tax_amount;$hpp+=(float)$l->total_hpp_barang;}foreach($credits as $account=>$amount)$entries[]=array('account_id'=>$account,'debit'=>0,'kredit'=>$amount);if($hpp>0){$entries[]=array('account_id'=>$this->CI->accountmappingservice->get('cost_of_goods_sold'),'debit'=>$hpp,'kredit'=>0);$entries[]=array('account_id'=>$this->CI->accountmappingservice->get('inventory'),'debit'=>0,'kredit'=>$hpp);}$customer=$this->CI->db->select('nama')->get_where('customers',array('id'=>$i->customer_id))->row();$description='Pencatatan Invoice '.$i->no_invoice.' kepada '.($customer?$customer->nama:'Customer #'.$i->customer_id).' sebesar Rp '.number_format($i->total,2,',','.');$journal=$this->CI->postingservice->post(array('tanggal'=>$i->tanggal,'no_ref'=>$this->CI->numberingservice->next('invoice_journal',$i->tanggal),'keterangan'=>$description,'natural_description'=>true,'ref_type'=>'invoice','ref_id'=>$id,'created_by'=>$userId),$entries,false,true);
foreach($lines as $line)$this->postStock($i,$line);
$this->CI->db->where('id',$id)->update('invoices',array('workflow_status'=>'posted','posted'=>1,'posted_at'=>date('Y-m-d H:i:s'),'posted_by'=>$userId,'journal_id'=>$journal,'status'=>'unpaid','sisa_piutang'=>$i->total));$this->CI->load->library('TaxService');$this->CI->taxservice->invoice($id,$journal);$after=$this->CI->receivableInvoiceModel->find($id);$this->CI->auditservice->record('invoice','invoice',$id,'post',$i,$after,$userId);return $after;});}
public function recordPayment($customerId,$date,$cashAccountId,$amount,array $allocations,$allowAdvance,$userId,$reference='',$notes=''){return $this->CI->transactionservice->run(function()use($customerId,$date,$cashAccountId,$amount,$allocations,$allowAdvance,$userId,$reference,$notes){$amount=round((float)$amount,2);if($amount<=0)throw new BusinessException('Nilai pembayaran harus lebih dari nol.');$this->CI->fiscalperiodservice->assertOpen($date);$allocated=0;$locked=array();foreach($allocations as $invoiceId=>$value){$value=round((float)$value,2);if($value<=0)continue;$i=$this->CI->db->query('SELECT * FROM invoices WHERE id=? FOR UPDATE',array((int)$invoiceId))->row();if(!$i||$i->workflow_status!=='posted'||(int)$i->customer_id!==(int)$customerId)throw new BusinessException('Invoice alokasi tidak valid.');if($value>(float)$i->sisa_piutang+0.001)throw new BusinessException('Alokasi melebihi saldo invoice '.$i->no_invoice);$allocated+=$value;$locked[]=array($i,$value);}
if($allocated>$amount+0.001)throw new BusinessException('Total alokasi melebihi pembayaran.');$advance=round($amount-$allocated,2);if($advance>0&&!$allowAdvance)throw new BusinessException('Pembayaran melebihi alokasi. Aktifkan mekanisme uang muka pelanggan.');$paymentNo=$this->CI->numberingservice->next('payment',$date);$entries=array(array('account_id'=>(int)$cashAccountId,'debit'=>$amount,'kredit'=>0));if($allocated>0)$entries[]=array('account_id'=>$this->CI->accountmappingservice->get('accounts_receivable'),'debit'=>0,'kredit'=>$allocated);if($advance>0)$entries[]=array('account_id'=>$this->CI->accountmappingservice->get('customer_advances'),'debit'=>0,'kredit'=>$advance);$customer=$this->CI->db->select('nama')->get_where('customers',array('id'=>$customerId))->row();$invoiceNumbers=array_map(function($row){return$row[0]->no_invoice;},$locked);$description='Penerimaan pembayaran '.$paymentNo.' dari '.($customer?$customer->nama:'Customer #'.$customerId).($invoiceNumbers?' untuk Invoice '.implode(', ',$invoiceNumbers):' sebagai uang muka pelanggan').' sebesar Rp '.number_format($amount,2,',','.');if(trim((string)$reference)!=='')$description.=' - Referensi: '.trim((string)$reference);if(trim((string)$notes)!=='')$description.=' - '.trim((string)$notes);$journal=$this->CI->postingservice->post(array('tanggal'=>$date,'no_ref'=>$paymentNo,'keterangan'=>$description,'natural_description'=>true,'ref_type'=>'customer_payment','created_by'=>$userId),$entries,false,true);$this->CI->db->insert('payments',array('invoice_id'=>count($locked)===1?$locked[0][0]->id:null,'tanggal'=>$date,'jumlah'=>$amount,'journal_id'=>$journal,'metode'=>'manual','payment_no'=>$paymentNo,'customer_id'=>$customerId,'status'=>'posted','reference_no'=>$reference,'notes'=>$notes,'created_by'=>$userId));$pid=$this->CI->db->insert_id();foreach($locked as $row){list($i,$value)=$row;$this->CI->db->insert('payment_allocations',array('payment_id'=>$pid,'invoice_id'=>$i->id,'amount'=>$value,'created_at'=>date('Y-m-d H:i:s')));$paid=(float)$i->total_bayar+$value;$balance=max(0,(float)$i->total-$paid);$this->CI->db->where('id',$i->id)->update('invoices',array('total_bayar'=>$paid,'sisa_piutang'=>$balance,'status'=>$balance<=0?'paid':'partial'));}if($advance>0)$this->CI->db->insert('customer_advances',array('advance_no'=>'ADV-'.$paymentNo,'customer_id'=>$customerId,'payment_id'=>$pid,'original_amount'=>$advance,'available_amount'=>$advance,'status'=>'available','journal_id'=>$journal,'created_at'=>date('Y-m-d H:i:s')));$this->CI->auditservice->record('receivable','payment',$pid,'post',null,array('payment_no'=>$paymentNo,'amount'=>$amount,'allocated'=>$allocated,'advance'=>$advance),$userId);return array('payment_id'=>$pid,'payment_no'=>$paymentNo,'advance'=>$advance);});}
public function reverseInvoice($id,$date,$reason,$userId){return $this->CI->transactionservice->run(function()use($id,$date,$reason,$userId){$i=$this->invoiceLock($id,'posted');if(trim($reason)==='')throw new BusinessException('Alasan reversal wajib diisi.');if((float)$i->total_bayar>0)throw new BusinessException('Invoice yang telah dibayar harus diselesaikan dengan credit note/refund.');$this->CI->fiscalperiodservice->assertOpen($date);$entries=array();foreach($this->CI->db->where('journal_id',$i->journal_id)->get('journal_details')->result() as $l)$entries[]=array('account_id'=>$l->account_id,'debit'=>$l->kredit,'kredit'=>$l->debit);if(!$entries)throw new BusinessException('Jurnal invoice tidak ditemukan.');$description='Pembatalan Invoice '.$i->no_invoice.' karena '.trim($reason);$rid=$this->CI->postingservice->post(array('tanggal'=>$date,'no_ref'=>$this->CI->numberingservice->next('reversal',$date),'keterangan'=>$description,'natural_description'=>true,'ref_type'=>'invoice_reversal','ref_id'=>$id,'reversal_of'=>$i->journal_id,'reversal_reason'=>$reason,'created_by'=>$userId),$entries,false,true);foreach($this->lines($id)as$line)$this->reverseStock($i,$line,$date,$reason);$this->CI->db->where('id',$id)->update('invoices',array('workflow_status'=>'reversed','status'=>'cancelled','reversal_journal_id'=>$rid,'sisa_piutang'=>0));$this->CI->auditservice->record('invoice','invoice',$id,'reverse',$i,$this->CI->receivableInvoiceModel->find($id),$userId);return $rid;});}
public function postWriteoff($invoiceId,$date,$amount,$reason,$userId){return $this->CI->transactionservice->run(function()use($invoiceId,$date,$amount,$reason,$userId){$i=$this->CI->db->query('SELECT * FROM invoices WHERE id=? FOR UPDATE',array((int)$invoiceId))->row();$amount=round((float)$amount,2);if(!$i||$i->workflow_status!=='posted'||$amount<=0||$amount>(float)$i->sisa_piutang)throw new BusinessException('Nilai write-off tidak valid.');if(trim($reason)==='')throw new BusinessException('Alasan write-off wajib diisi.');$this->CI->fiscalperiodservice->assertOpen($date);$no=$this->CI->numberingservice->next('writeoff',$date);$description='Penghapusan piutang Invoice '.$i->no_invoice.' sebesar Rp '.number_format($amount,2,',','.').' karena '.trim($reason);$journal=$this->CI->postingservice->post(array('tanggal'=>$date,'no_ref'=>$no,'keterangan'=>$description,'natural_description'=>true,'ref_type'=>'receivable_writeoff','ref_id'=>$invoiceId,'created_by'=>$userId),array(array('account_id'=>$this->CI->accountmappingservice->get('bad_debt_expense'),'debit'=>$amount,'kredit'=>0),array('account_id'=>$this->CI->accountmappingservice->get('accounts_receivable'),'debit'=>0,'kredit'=>$amount)),false,true);$this->CI->db->insert('receivable_writeoffs',array('writeoff_no'=>$no,'invoice_id'=>$invoiceId,'writeoff_date'=>$date,'amount'=>$amount,'reason'=>$reason,'status'=>'posted','journal_id'=>$journal,'created_by'=>$userId,'created_at'=>date('Y-m-d H:i:s')));$wid=$this->CI->db->insert_id();$balance=max(0,(float)$i->sisa_piutang-$amount);$this->CI->db->where('id',$i->id)->update('invoices',array('sisa_piutang'=>$balance,'status'=>$balance<=0?'paid':'partial'));$this->CI->auditservice->record('receivable','writeoff',$wid,'post',null,array('invoice_id'=>$invoiceId,'amount'=>$amount),$userId);return $wid;});}
public function aging($asOf=null){$asOf=$asOf?:date('Y-m-d');$company=$this->CI->companycontext->id();return $this->CI->db->query("SELECT i.customer_id,c.nama,SUM(CASE WHEN i.jatuh_tempo>=? THEN i.sisa_piutang ELSE 0 END) current_amount,SUM(CASE WHEN DATEDIFF(?,i.jatuh_tempo) BETWEEN 1 AND 30 THEN i.sisa_piutang ELSE 0 END) days_1_30,SUM(CASE WHEN DATEDIFF(?,i.jatuh_tempo) BETWEEN 31 AND 60 THEN i.sisa_piutang ELSE 0 END) days_31_60,SUM(CASE WHEN DATEDIFF(?,i.jatuh_tempo) BETWEEN 61 AND 90 THEN i.sisa_piutang ELSE 0 END) days_61_90,SUM(CASE WHEN DATEDIFF(?,i.jatuh_tempo)>90 THEN i.sisa_piutang ELSE 0 END) days_over_90,SUM(i.sisa_piutang) total FROM invoices i JOIN customers c ON c.id=i.customer_id WHERE i.company_id=? AND i.workflow_status='posted' AND i.sisa_piutang>0 AND i.tanggal<=? GROUP BY i.customer_id,c.nama ORDER BY c.nama",array($asOf,$asOf,$asOf,$asOf,$asOf,$company,$asOf))->result();}
public function statement($customerId,$from,$to){$company=$this->CI->companycontext->id();$customer=$this->CI->db->get_where('customers',array('id'=>$customerId,'company_id'=>$company))->row();if(!$customer)throw new BusinessException('Customer tidak ditemukan pada perusahaan aktif.');$invoices=$this->CI->db->where(array('company_id'=>$company,'customer_id'=>$customerId))->where('tanggal >=',$from)->where('tanggal <=',$to)->where('workflow_status','posted')->order_by('tanggal')->get('invoices')->result();$payments=$this->CI->db->where(array('company_id'=>$company,'customer_id'=>$customerId))->where('tanggal >=',$from)->where('tanggal <=',$to)->where('status','posted')->order_by('tanggal')->get('payments')->result();return array('customer'=>$customer,'invoices'=>$invoices,'payments'=>$payments);}
private function postStock($invoice,$line)
{
if(!$line->items_id||$line->stock_posted_at)return;
$item=$this->CI->db->select('i.company_id,k.tracking_type')->from('items i')->join('kode_barang k','k.id=i.kode_id','left')->where('i.id',(int)$line->items_id)->get()->row();if(!$item)throw new BusinessException('Master barang invoice tidak ditemukan.');if(isset($invoice->company_id)&&(int)$invoice->company_id>0&&(int)$item->company_id!==(int)$invoice->company_id)throw new BusinessException('Barang invoice berasal dari perusahaan lain.');
$key='SALE-INVOICE-LINE-'.(int)$line->id;
$ledger=$this->CI->db->get_where('inventory_ledger',array('idempotency_key'=>$key))->row();
if($ledger){$this->CI->receivableInventoryModel->synchronizeStock($line->items_id);$this->CI->db->where('id',$line->id)->update('invoice_details',array('stock_posted_at'=>$ledger->created_at));return;}
$available=(float)$this->CI->db->query("SELECT COALESCE(SUM(IF(direction='in',qty,-qty)),0) qty FROM inventory_ledger WHERE item_id=? AND warehouse_id=? FOR UPDATE",array((int)$line->items_id,(int)$line->warehouse_id))->row()->qty;$reserved=(float)$this->CI->db->select('COALESCE(SUM(qty-released_qty),0) qty',false)->where(array('item_id'=>(int)$line->items_id,'warehouse_id'=>(int)$line->warehouse_id))->where_in('status',array('active','partially_released'))->get('stock_reservations')->row()->qty;$available-=$reserved;
if($available<(float)$line->qty-.0001)throw new BusinessException('Stok gudang tidak cukup untuk '.$line->nama_item.'. Tersedia '.number_format($available,0,',','.').'.');
$hasBarcode=(int)$this->CI->db->where(array('item_id'=>(int)$line->items_id,'warehouse_id'=>(int)$line->warehouse_id))->where('qty_sisa >',0)->count_all_results('item_barcodes');if($hasBarcode&&!$line->barcode_id)throw new BusinessException('Barcode/SN wajib dipilih untuk '.$line->nama_item.' agar stok fisik dapat ditelusuri.');
if($line->barcode_id){$barcode=$this->CI->db->query('SELECT * FROM item_barcodes WHERE id=? AND item_id=? AND warehouse_id=? FOR UPDATE',array((int)$line->barcode_id,(int)$line->items_id,(int)$line->warehouse_id))->row();$barcodeAvailable=$barcode?(float)$barcode->qty_sisa-(float)$barcode->reserved_qty-(float)($barcode->return_reserved_qty??0):0;if(!$barcode||$barcode->status!=='available'||$barcodeAvailable<(float)$line->qty-.0001)throw new BusinessException('Barcode '.$line->nama_item.' tidak tersedia, berada di lokasi lain, atau sedang direservasi.');$remaining=(float)$barcode->qty_sisa-(float)$line->qty;$this->CI->db->where(array('id'=>$barcode->id,'version'=>$barcode->version))->update('item_barcodes',array('qty_sisa'=>$remaining,'status'=>$remaining<=.0001?'sold_out':'available','version'=>(int)$barcode->version+1));if($this->CI->db->affected_rows()!==1)throw new BusinessException('Barcode sedang diproses transaksi lain. Silakan ulangi.');}
$cost=(float)$line->qty>0?(float)$line->total_hpp_barang/(float)$line->qty:0;$description='[AUTO] Penjualan '.$invoice->no_invoice.' | '.$line->nama_item.' | Qty '.number_format((float)$line->qty,0,',','.');
$this->CI->db->insert('inventory_ledger',array('item_id'=>$line->items_id,'warehouse_id'=>$line->warehouse_id,'barcode_id'=>$line->barcode_id,'movement_date'=>$invoice->tanggal,'direction'=>'out','qty'=>$line->qty,'unit_cost'=>$cost,'value'=>$cost*(float)$line->qty,'document_type'=>'invoice_details','document_id'=>$invoice->id,'document_line_id'=>$line->id,'idempotency_key'=>$key,'created_at'=>date('Y-m-d H:i:s')));
$this->consumeCostLayers($line->items_id,$line->warehouse_id,$line->qty);
$this->CI->db->insert('stock_logs',array('item_id'=>$line->items_id,'barcode_id'=>$line->barcode_id,'qty'=>$line->qty,'tipe'=>'keluar','unit_cost'=>$cost,'keterangan'=>$description,'warehouse_id'=>$line->warehouse_id,'ref_type'=>'invoice_details','ref_id'=>$line->id,'idempotency_key'=>$key));$stockLog=$this->CI->db->insert_id();
$this->CI->db->insert('item_movements',array('item_id'=>$line->items_id,'barcode_id'=>$line->barcode_id,'qty'=>$line->qty,'from_type'=>'warehouse','from_id'=>$line->warehouse_id,'to_type'=>'invoice','to_id'=>$invoice->id,'movement_type'=>'sold_out','reference_type'=>'invoice_details','reference_id'=>$line->id,'notes'=>$description,'created_at'=>date('Y-m-d H:i:s')));
$this->CI->receivableInventoryModel->synchronizeStock($line->items_id);
$this->CI->db->where('id',$line->id)->update('invoice_details',array('stock_posted_at'=>date('Y-m-d H:i:s'),'stock_log_id'=>$stockLog));
}
private function consumeCostLayers($itemId,$warehouseId,$qty)
{
$remaining=(float)$qty;$layers=$this->CI->db->query("SELECT * FROM inventory_cost_layers WHERE item_id=? AND warehouse_id=? AND status='open' AND remaining_qty>0 ORDER BY received_date,id FOR UPDATE",array((int)$itemId,(int)$warehouseId))->result();
foreach($layers as$layer){if($remaining<=.0001)break;$used=min($remaining,(float)$layer->remaining_qty);$left=(float)$layer->remaining_qty-$used;$this->CI->db->where('id',$layer->id)->update('inventory_cost_layers',array('remaining_qty'=>$left,'status'=>$left<=.0001?'closed':'open'));$remaining-=$used;}
}
private function reverseStock($invoice,$line,$date,$reason)
{
if(!$line->items_id||!$line->stock_posted_at)return;$key='SALE-INVOICE-REVERSAL-'.(int)$line->id;if($this->CI->db->get_where('inventory_ledger',array('idempotency_key'=>$key))->row())return;
$original=$this->CI->db->get_where('inventory_ledger',array('idempotency_key'=>'SALE-INVOICE-LINE-'.(int)$line->id))->row();if(!$original)throw new BusinessException('Ledger stok penjualan untuk '.$line->nama_item.' tidak ditemukan; reversal dihentikan agar histori tidak rusak.');
$description='[AUTO] Reversal stok invoice '.$invoice->no_invoice.' | '.$line->nama_item.' | Alasan '.$reason;$this->CI->db->insert('inventory_ledger',array('item_id'=>$line->items_id,'warehouse_id'=>$line->warehouse_id,'barcode_id'=>$line->barcode_id,'movement_date'=>$date,'direction'=>'in','qty'=>$line->qty,'unit_cost'=>$original->unit_cost,'value'=>$original->value,'document_type'=>'invoice_reversal','document_id'=>$invoice->id,'document_line_id'=>$line->id,'idempotency_key'=>$key,'created_at'=>date('Y-m-d H:i:s')));$ledgerId=$this->CI->db->insert_id();
$this->CI->db->insert('inventory_cost_layers',array('item_id'=>$line->items_id,'warehouse_id'=>$line->warehouse_id,'source_ledger_id'=>$ledgerId,'received_date'=>$date,'original_qty'=>$line->qty,'remaining_qty'=>$line->qty,'unit_cost'=>$original->unit_cost,'status'=>'open'));
$this->CI->db->insert('stock_logs',array('item_id'=>$line->items_id,'barcode_id'=>$line->barcode_id,'qty'=>$line->qty,'tipe'=>'masuk','unit_cost'=>$original->unit_cost,'keterangan'=>$description,'warehouse_id'=>$line->warehouse_id,'ref_type'=>'invoice_reversal','ref_id'=>$invoice->id,'idempotency_key'=>$key));
$this->CI->db->insert('item_movements',array('item_id'=>$line->items_id,'barcode_id'=>$line->barcode_id,'qty'=>$line->qty,'from_type'=>'invoice','from_id'=>$invoice->id,'to_type'=>'warehouse','to_id'=>$line->warehouse_id,'movement_type'=>'customer_return','reference_type'=>'invoice_reversal','reference_id'=>$invoice->id,'notes'=>$description,'created_at'=>date('Y-m-d H:i:s')));
if($line->barcode_id){$barcode=$this->CI->db->query('SELECT * FROM item_barcodes WHERE id=? FOR UPDATE',array($line->barcode_id))->row();if($barcode)$this->CI->db->where('id',$barcode->id)->update('item_barcodes',array('qty_sisa'=>(float)$barcode->qty_sisa+(float)$line->qty,'status'=>'available'));}$this->CI->receivableInventoryModel->synchronizeStock($line->items_id);
}
private function invoiceLock($id,$status){$i=$this->CI->db->query('SELECT * FROM invoices WHERE id=? AND company_id=? FOR UPDATE',array((int)$id,$this->CI->companycontext->id()))->row();if(!$i)throw new BusinessException('Invoice tidak ditemukan.',array(),404);if($i->workflow_status!==$status)throw new BusinessException('Invoice harus berstatus '.$status.'.');return $i;}
private function lines($id){return $this->CI->db->where('invoice_id',$id)->order_by('id')->get('invoice_details')->result();}
private function invoiceTotal($id){$r=$this->CI->db->select('COALESCE(SUM(subtotal),0) total',false)->get_where('invoice_details',array('invoice_id'=>$id))->row();return round((float)$r->total,2);}
private function validateInvoice($i){if($i->jatuh_tempo<$i->tanggal)throw new BusinessException('Jatuh tempo tidak boleh sebelum tanggal invoice.');if(!$this->lines($i->id))throw new BusinessException('Invoice belum memiliki item.');}
private function assertCreditLimit($i){$c=$this->CI->db->get_where('customers',array('id'=>$i->customer_id))->row();if(!$c)throw new BusinessException('Customer tidak ditemukan.');if($c->is_credit_blocked)throw new BusinessException('Kredit customer sedang diblokir.');if((float)$c->credit_limit>0){$out=(float)$this->CI->db->select('COALESCE(SUM(sisa_piutang),0) total',false)->where('customer_id',$c->id)->where('workflow_status','posted')->get('invoices')->row()->total;if($out+$this->invoiceTotal($i->id)>(float)$c->credit_limit)throw new BusinessException('Credit limit customer terlampaui.');}}
}