PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.0
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.0
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Services / QuoteTotals.php

QuoteTotals.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.0, at includes/Services/QuoteTotals.php

154 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Stored quote totals, so the quote list header can sum by currency in SQL
4 * instead of loading every quote. Same shape as InvoiceTotalsCache: written on
5 * save, backfilled in the background for quotes that pre-date it, dropped when
6 * the total's inputs change.
7 *
8 * @package EasyInvoice
9 * @since 2.4.0
10 */
11
12 namespace EasyInvoice\Services;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 class QuoteTotals {
19
20 const META_TOTAL = '_easy_invoice_quote_cached_total';
21 const OPTION_COMPLETE = 'easy_invoice_quote_totals_complete';
22 const CRON_HOOK = 'easy_invoice_quote_totals_backfill';
23
24 /** Meta keys whose change makes a stored total stale. */
25 const DEPENDENT_META = [
26 '_easy_invoice_quote_items', '_easy_invoice_quote_discount_type', '_easy_invoice_quote_discount_value',
27 '_easy_invoice_quote_discount_calculation_method', '_easy_invoice_quote_tax_enabled', '_easy_invoice_quote_tax_rate',
28 '_easy_invoice_quote_prices_include_tax',
29 ];
30
31 /** Statuses whose value the header card reports (won or still in play). */
32 const VALUE_STATUSES = [ 'accepted', 'sent', 'draft', 'available' ];
33
34 /**
35 * @param \EasyInvoice\Models\Quote $quote Saved quote.
36 */
37 public static function store( $quote ): void {
38 if ( ! is_callable( [ $quote, 'getId' ] ) || ! is_callable( [ $quote, 'getTotal' ] ) ) {
39 return;
40 }
41 $id = (int) $quote->getId();
42 if ( $id <= 0 ) {
43 return;
44 }
45 update_post_meta( $id, self::META_TOTAL, (string) round( (float) $quote->getTotal(), 2 ) );
46 InvoiceTotalsCache::bumpVersion();
47 }
48
49 public static function invalidate( int $quote_id ): void {
50 delete_post_meta( $quote_id, self::META_TOTAL );
51 delete_option( self::OPTION_COMPLETE );
52 InvoiceTotalsCache::bumpVersion();
53 }
54
55 public static function missingCount(): int {
56 global $wpdb;
57 return (int) $wpdb->get_var( $wpdb->prepare(
58 "SELECT COUNT(*) FROM {$wpdb->posts} p
59 LEFT JOIN {$wpdb->postmeta} c ON c.post_id = p.ID AND c.meta_key = %s
60 WHERE p.post_type = 'easy_invoice_quote' AND p.post_status = 'publish' AND c.meta_id IS NULL",
61 self::META_TOTAL
62 ) );
63 }
64
65 /** @return int Still missing afterwards. */
66 public static function backfill( int $limit = 200 ): int {
67 global $wpdb;
68 $ids = array_map( 'intval', (array) $wpdb->get_col( $wpdb->prepare(
69 "SELECT p.ID FROM {$wpdb->posts} p
70 LEFT JOIN {$wpdb->postmeta} c ON c.post_id = p.ID AND c.meta_key = %s
71 WHERE p.post_type = 'easy_invoice_quote' AND p.post_status = 'publish' AND c.meta_id IS NULL
72 ORDER BY p.ID DESC LIMIT %d",
73 self::META_TOTAL,
74 max( 1, $limit )
75 ) ) );
76 if ( ! $ids ) {
77 return 0;
78 }
79 update_meta_cache( 'post', $ids );
80 $repository = \EasyInvoice\Providers\QuoteServiceProvider::getQuoteRepository();
81 foreach ( $ids as $id ) {
82 $quote = $repository->find( $id );
83 if ( $quote ) {
84 self::store( $quote );
85 } else {
86 update_post_meta( $id, self::META_TOTAL, '0' );
87 }
88 wp_cache_delete( $id, 'post_meta' );
89 }
90 return self::missingCount();
91 }
92
93 public static function scheduleBackfill(): void {
94 if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
95 wp_schedule_single_event( time() + 5, self::CRON_HOOK );
96 }
97 }
98
99 public static function cronBackfill(): void {
100 if ( self::backfill( 500 ) > 0 ) {
101 wp_schedule_single_event( time() + 10, self::CRON_HOOK );
102 } else {
103 update_option( self::OPTION_COMPLETE, '1', false );
104 }
105 }
106
107 private static function ensure(): void {
108 if ( '1' === get_option( self::OPTION_COMPLETE, '' ) ) {
109 return;
110 }
111 if ( self::missingCount() > 0 && self::backfill( 200 ) > 0 ) {
112 self::scheduleBackfill();
113 return;
114 }
115 update_option( self::OPTION_COMPLETE, '1', false );
116 }
117
118 /**
119 * Quote value by currency for VALUE_STATUSES. Memoised until the next document write.
120 *
121 * @return array<string,array{amount:float,count:int}>
122 */
123 public static function valueByCurrency(): array {
124 $version = (int) get_option( 'easy_invoice_stats_version', 1 );
125 $hit = get_transient( 'ei_stats_quote_value' );
126 if ( is_array( $hit ) && isset( $hit['v'], $hit['d'] ) && (int) $hit['v'] === $version ) {
127 return $hit['d'];
128 }
129 global $wpdb;
130 self::ensure();
131 $site = strtoupper( (string) get_option( 'easy_invoice_currency_code', 'USD' ) );
132 $in = "'" . implode( "','", array_map( 'esc_sql', self::VALUE_STATUSES ) ) . "'";
133 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- statuses constant, escaped.
134 $rows = $wpdb->get_results( $wpdb->prepare(
135 "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
136 FROM {$wpdb->posts} p
137 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})
138 INNER JOIN {$wpdb->postmeta} c ON c.post_id = p.ID AND c.meta_key = %s
139 LEFT JOIN {$wpdb->postmeta} cur ON cur.post_id = p.ID AND cur.meta_key = '_easy_invoice_quote_currency_code'
140 WHERE p.post_type = 'easy_invoice_quote' AND p.post_status = 'publish'
141 GROUP BY currency",
142 $site,
143 self::META_TOTAL
144 ), ARRAY_A );
145 // phpcs:enable
146 $out = [];
147 foreach ( (array) $rows as $r ) {
148 $out[ (string) $r['currency'] ] = [ 'amount' => round( (float) $r['amount'], 2 ), 'count' => (int) $r['n'] ];
149 }
150 set_transient( 'ei_stats_quote_value', [ 'v' => $version, 'd' => $out ], 10 * MINUTE_IN_SECONDS );
151 return $out;
152 }
153 }
154