# easy-invoice/2.4.0/includes/Services/InvoiceBalance.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.4.0. 197 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.0/code/includes/Services/InvoiceBalance.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.0/raw/includes/Services/InvoiceBalance.php
- Modified: 2026-09-15T12:31:20+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.4.0/code/includes/Services/InvoiceBalance.php#L10-L20`.

```php
<?php
/**
 * What is still owed on an invoice.
 *
 * One place for the arithmetic the designs, the PDF, the payment panel and
 * the gateways all need: total, less completed payments, less credit notes.
 *
 * @package EasyInvoice\Services
 * @since 2.4.0
 */

namespace EasyInvoice\Services;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class InvoiceBalance {

	/**
	 * Completed payments recorded against an invoice.
	 *
	 * @param int $invoice_id Invoice.
	 * @return float
	 */
	public static function paid( int $invoice_id ): float {
		if ( $invoice_id <= 0 ) {
			return 0.0;
		}
		if ( isset( self::$paid_cache[ $invoice_id ] ) ) {
			return (float) apply_filters( 'easy_invoice_pdf_amount_paid', self::$paid_cache[ $invoice_id ], $invoice_id );
		}
		return PdfRenderer::amountPaid( $invoice_id );
	}

	/** @var array<int,float> Completed payments per invoice, filled by prime(). */
	private static $paid_cache = [];
	/** @var array<int,float> Credit-note totals per invoice, filled by prime(). */
	private static $credit_cache = [];

	/**
	 * Load the paid and credited totals for many invoices in two queries.
	 *
	 * The dashboard and the reports call due() for every invoice on the
	 * site; without this each call ran its own payment and credit-note
	 * lookups (two to four queries per invoice, thousands on a busy site).
	 *
	 * @param int[] $invoice_ids
	 */
	public static function prime( array $invoice_ids ): void {
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $in is a list of intval()'d ids.
		global $wpdb;
		$ids = array_values( array_unique( array_filter( array_map( 'intval', $invoice_ids ) ) ) );
		if ( ! $ids ) {
			return;
		}
		foreach ( array_chunk( $ids, 2000 ) as $chunk ) {
			$in = implode( ',', $chunk );
			// Completed payments grouped by invoice.
			$rows = $wpdb->get_results(
				"SELECT inv.meta_value AS invoice_id, SUM(CAST(amt.meta_value AS DECIMAL(18,4))) AS paid
				 FROM {$wpdb->posts} p
				 INNER JOIN {$wpdb->postmeta} inv ON inv.post_id = p.ID AND inv.meta_key = '_invoice_id'
				 INNER JOIN {$wpdb->postmeta} st  ON st.post_id  = p.ID AND st.meta_key  = '_status'
				 LEFT  JOIN {$wpdb->postmeta} amt ON amt.post_id = p.ID AND amt.meta_key = '_amount'
				 WHERE p.post_type = 'easy_invoice_payment' AND p.post_status = 'publish'
				   AND LOWER(st.meta_value) IN ('completed','complete','paid','success','succeeded')
				   AND inv.meta_value IN ($in)
				 GROUP BY inv.meta_value", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- integer ids.
				ARRAY_A
			);
			foreach ( $chunk as $id ) {
				self::$paid_cache[ $id ]   = 0.0;
				self::$credit_cache[ $id ] = 0.0;
			}
			foreach ( (array) $rows as $r ) {
				self::$paid_cache[ (int) $r['invoice_id'] ] = round( (float) $r['paid'], 2 );
			}
			// Credit notes grouped by invoice.
			$rows = $wpdb->get_results(
				"SELECT inv.meta_value AS invoice_id, SUM(CAST(tot.meta_value AS DECIMAL(18,4))) AS credited
				 FROM {$wpdb->posts} p
				 INNER JOIN {$wpdb->postmeta} inv ON inv.post_id = p.ID AND inv.meta_key = '_easy_invoice_credited_invoice_id'
				 LEFT  JOIN {$wpdb->postmeta} tot ON tot.post_id = p.ID AND tot.meta_key = '_easy_invoice_total'
				 WHERE p.post_type = 'easy_invoice_credit' AND p.post_status = 'publish'
				   AND inv.meta_value IN ($in)
				 GROUP BY inv.meta_value", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- integer ids.
				ARRAY_A
			);
			foreach ( (array) $rows as $r ) {
				self::$credit_cache[ (int) $r['invoice_id'] ] = round( (float) $r['credited'], 2 );
			}
		}
		// phpcs:enable
	}

	/**
	 * Forget primed totals (after a payment or credit note is written).
	 *
	 * @param int|null $invoice_id One invoice, or everything when null.
	 */
	public static function forget( ?int $invoice_id = null ): void {
		if ( null === $invoice_id ) {
			self::$paid_cache = self::$credit_cache = [];
			return;
		}
		unset( self::$paid_cache[ $invoice_id ], self::$credit_cache[ $invoice_id ] );
	}

	/**
	 * Credit notes issued against an invoice.
	 *
	 * @param int $invoice_id Invoice.
	 * @return float
	 */
	public static function credited( int $invoice_id ): float {
		if ( $invoice_id <= 0 ) {
			return 0.0;
		}
		if ( isset( self::$credit_cache[ $invoice_id ] ) ) {
			return self::$credit_cache[ $invoice_id ];
		}
		return CreditNote::creditedTotal( $invoice_id );
	}

	/**
	 * Total less payments and credits, never below zero.
	 *
	 * @param mixed $invoice Invoice model.
	 * @return float
	 */
	public static function due( $invoice ): float {
		$total = is_callable( [ $invoice, 'getTotal' ] ) ? (float) $invoice->getTotal() : 0.0;
		$id    = is_callable( [ $invoice, 'getId' ] ) ? (int) $invoice->getId() : 0;
		$due   = max( 0.0, round( $total - self::paid( $id ) - self::credited( $id ), 2 ) );

		/**
		 * Filter the amount still owed on an invoice — shown in the designs'
		 * header, the payment panel, and charged by the gateways.
		 *
		 * @param float $due     Total less payments received and credit notes.
		 * @param mixed $invoice Invoice model.
		 */
		return (float) apply_filters( 'easy_invoice_amount_due', $due, $invoice );
	}

	/**
	 * Open invoices still carrying a balance, with the amounts by currency
	 * and the subset past their due date.
	 *
	 * @param array $invoices Invoice models.
	 * @return array{count:int,overdue_count:int,amount:array,overdue_amount:array}
	 */
	public static function outstanding( array $invoices ): array {
		$out   = [ 'count' => 0, 'overdue_count' => 0, 'amount' => [], 'overdue_amount' => [] ];
		self::prime( array_map( function ( $invoice ) {
			return is_callable( [ $invoice, 'getId' ] ) ? (int) $invoice->getId() : 0;
		}, $invoices ) );
		$today = gmdate( 'Y-m-d', current_time( 'timestamp' ) );
		$site  = strtoupper( (string) get_option( 'easy_invoice_currency_code', 'USD' ) );
		foreach ( $invoices as $invoice ) {
			$status = is_callable( [ $invoice, 'getStatus' ] ) ? strtolower( (string) $invoice->getStatus() ) : '';
			if ( ! in_array( $status, [ 'available', 'unpaid', 'partial', 'overdue', 'sent', 'pending' ], true ) ) {
				continue;
			}
			$due = self::due( $invoice );
			if ( $due <= 0 ) {
				continue;
			}
			$currency = is_callable( [ $invoice, 'getCurrencyCode' ] ) ? strtoupper( (string) $invoice->getCurrencyCode() ) : '';
			if ( '' === $currency || 'GLOBAL' === $currency ) {
				$currency = $site;
			}
			$out['count']++;
			$out['amount'][ $currency ] = ( $out['amount'][ $currency ] ?? 0 ) + $due;
			$due_date = is_callable( [ $invoice, 'getDueDate' ] ) ? (string) $invoice->getDueDate() : '';
			if ( '' !== $due_date && strtotime( $due_date ) && gmdate( 'Y-m-d', strtotime( $due_date ) ) < $today ) {
				$out['overdue_count']++;
				$out['overdue_amount'][ $currency ] = ( $out['overdue_amount'][ $currency ] ?? 0 ) + $due;
			}
		}
		return $out;
	}

	/**
	 * Whether payments and credits together cover the invoice.
	 *
	 * @param mixed $invoice Invoice model.
	 * @return bool
	 */
	public static function isSettled( $invoice ): bool {
		$total = is_callable( [ $invoice, 'getTotal' ] ) ? (float) $invoice->getTotal() : 0.0;
		$id    = is_callable( [ $invoice, 'getId' ] ) ? (int) $invoice->getId() : 0;
		return self::paid( $id ) + self::credited( $id ) + 0.005 >= $total;
	}
}

```
