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

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

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.0/code/includes/Services/QuoteTotals.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.0/raw/includes/Services/QuoteTotals.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/QuoteTotals.php#L10-L20`.

```php
<?php
/**
 * Stored quote totals, so the quote list header can sum by currency in SQL
 * instead of loading every quote. Same shape as InvoiceTotalsCache: written on
 * save, backfilled in the background for quotes that pre-date it, dropped when
 * the total's inputs change.
 *
 * @package EasyInvoice
 * @since 2.4.0
 */

namespace EasyInvoice\Services;

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

class QuoteTotals {

	const META_TOTAL = '_easy_invoice_quote_cached_total';
	const OPTION_COMPLETE = 'easy_invoice_quote_totals_complete';
	const CRON_HOOK = 'easy_invoice_quote_totals_backfill';

	/** Meta keys whose change makes a stored total stale. */
	const DEPENDENT_META = [
		'_easy_invoice_quote_items', '_easy_invoice_quote_discount_type', '_easy_invoice_quote_discount_value',
		'_easy_invoice_quote_discount_calculation_method', '_easy_invoice_quote_tax_enabled', '_easy_invoice_quote_tax_rate',
		'_easy_invoice_quote_prices_include_tax',
	];

	/** Statuses whose value the header card reports (won or still in play). */
	const VALUE_STATUSES = [ 'accepted', 'sent', 'draft', 'available' ];

	/**
	 * @param \EasyInvoice\Models\Quote $quote Saved quote.
	 */
	public static function store( $quote ): void {
		if ( ! is_callable( [ $quote, 'getId' ] ) || ! is_callable( [ $quote, 'getTotal' ] ) ) {
			return;
		}
		$id = (int) $quote->getId();
		if ( $id <= 0 ) {
			return;
		}
		update_post_meta( $id, self::META_TOTAL, (string) round( (float) $quote->getTotal(), 2 ) );
		InvoiceTotalsCache::bumpVersion();
	}

	public static function invalidate( int $quote_id ): void {
		delete_post_meta( $quote_id, self::META_TOTAL );
		delete_option( self::OPTION_COMPLETE );
		InvoiceTotalsCache::bumpVersion();
	}

	public static function missingCount(): int {
		global $wpdb;
		return (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->posts} p
			 LEFT JOIN {$wpdb->postmeta} c ON c.post_id = p.ID AND c.meta_key = %s
			 WHERE p.post_type = 'easy_invoice_quote' AND p.post_status = 'publish' AND c.meta_id IS NULL",
			self::META_TOTAL
		) );
	}

	/** @return int Still missing afterwards. */
	public static function backfill( int $limit = 200 ): int {
		global $wpdb;
		$ids = array_map( 'intval', (array) $wpdb->get_col( $wpdb->prepare(
			"SELECT p.ID FROM {$wpdb->posts} p
			 LEFT JOIN {$wpdb->postmeta} c ON c.post_id = p.ID AND c.meta_key = %s
			 WHERE p.post_type = 'easy_invoice_quote' AND p.post_status = 'publish' AND c.meta_id IS NULL
			 ORDER BY p.ID DESC LIMIT %d",
			self::META_TOTAL,
			max( 1, $limit )
		) ) );
		if ( ! $ids ) {
			return 0;
		}
		update_meta_cache( 'post', $ids );
		$repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository();
		foreach ( $ids as $id ) {
			$quote = $repository->find( $id );
			if ( $quote ) {
				self::store( $quote );
			} else {
				update_post_meta( $id, self::META_TOTAL, '0' );
			}
			wp_cache_delete( $id, 'post_meta' );
		}
		return self::missingCount();
	}

	public static function scheduleBackfill(): void {
		if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
			wp_schedule_single_event( time() + 5, self::CRON_HOOK );
		}
	}

	public static function cronBackfill(): void {
		if ( self::backfill( 500 ) > 0 ) {
			wp_schedule_single_event( time() + 10, self::CRON_HOOK );
		} else {
			update_option( self::OPTION_COMPLETE, '1', false );
		}
	}

	private static function ensure(): void {
		if ( '1' === get_option( self::OPTION_COMPLETE, '' ) ) {
			return;
		}
		if ( self::missingCount() > 0 && self::backfill( 200 ) > 0 ) {
			self::scheduleBackfill();
			return;
		}
		update_option( self::OPTION_COMPLETE, '1', false );
	}

	/**
	 * Quote value by currency for VALUE_STATUSES. Memoised until the next document write.
	 *
	 * @return array<string,array{amount:float,count:int}>
	 */
	public static function valueByCurrency(): array {
		$version = (int) get_option( 'easy_invoice_stats_version', 1 );
		$hit     = get_transient( 'ei_stats_quote_value' );
		if ( is_array( $hit ) && isset( $hit['v'], $hit['d'] ) && (int) $hit['v'] === $version ) {
			return $hit['d'];
		}
		global $wpdb;
		self::ensure();
		$site = strtoupper( (string) get_option( 'easy_invoice_currency_code', 'USD' ) );
		$in   = "'" . implode( "','", array_map( 'esc_sql', self::VALUE_STATUSES ) ) . "'";
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- statuses constant, escaped.
		$rows = $wpdb->get_results( $wpdb->prepare(
			"SELECT UPPER(COALESCE(NULLIF(NULLIF(cur.meta_value, ''), 'global'), %s)) AS currency, COUNT(*) AS n, SUM(CAST(c.meta_value AS DECIMAL(18,4))) AS amount
			 FROM {$wpdb->posts} p
			 INNER JOIN {$wpdb->postmeta} s ON s.post_id = p.ID AND s.meta_key = '_easy_invoice_quote_status' AND s.meta_value IN ({$in})
			 INNER JOIN {$wpdb->postmeta} c ON c.post_id = p.ID AND c.meta_key = %s
			 LEFT  JOIN {$wpdb->postmeta} cur ON cur.post_id = p.ID AND cur.meta_key = '_easy_invoice_quote_currency_code'
			 WHERE p.post_type = 'easy_invoice_quote' AND p.post_status = 'publish'
			 GROUP BY currency",
			$site,
			self::META_TOTAL
		), ARRAY_A );
		// phpcs:enable
		$out = [];
		foreach ( (array) $rows as $r ) {
			$out[ (string) $r['currency'] ] = [ 'amount' => round( (float) $r['amount'], 2 ), 'count' => (int) $r['n'] ];
		}
		set_transient( 'ei_stats_quote_value', [ 'v' => $version, 'd' => $out ], 10 * MINUTE_IN_SECONDS );
		return $out;
	}
}

```
