PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
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 / InvoiceBalance.php

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

197 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * What is still owed on an invoice.
4 *
5 * One place for the arithmetic the designs, the PDF, the payment panel and
6 * the gateways all need: total, less completed payments, less credit notes.
7 *
8 * @package EasyInvoice\Services
9 * @since 2.4.0
10 */
11
12 namespace EasyInvoice\Services;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 class InvoiceBalance {
19
20 /**
21 * Completed payments recorded against an invoice.
22 *
23 * @param int $invoice_id Invoice.
24 * @return float
25 */
26 public static function paid( int $invoice_id ): float {
27 if ( $invoice_id <= 0 ) {
28 return 0.0;
29 }
30 if ( isset( self::$paid_cache[ $invoice_id ] ) ) {
31 return (float) apply_filters( 'easy_invoice_pdf_amount_paid', self::$paid_cache[ $invoice_id ], $invoice_id );
32 }
33 return PdfRenderer::amountPaid( $invoice_id );
34 }
35
36 /** @var array<int,float> Completed payments per invoice, filled by prime(). */
37 private static $paid_cache = [];
38 /** @var array<int,float> Credit-note totals per invoice, filled by prime(). */
39 private static $credit_cache = [];
40
41 /**
42 * Load the paid and credited totals for many invoices in two queries.
43 *
44 * The dashboard and the reports call due() for every invoice on the
45 * site; without this each call ran its own payment and credit-note
46 * lookups (two to four queries per invoice, thousands on a busy site).
47 *
48 * @param int[] $invoice_ids
49 */
50 public static function prime( array $invoice_ids ): void {
51 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $in is a list of intval()'d ids.
52 global $wpdb;
53 $ids = array_values( array_unique( array_filter( array_map( 'intval', $invoice_ids ) ) ) );
54 if ( ! $ids ) {
55 return;
56 }
57 foreach ( array_chunk( $ids, 2000 ) as $chunk ) {
58 $in = implode( ',', $chunk );
59 // Completed payments grouped by invoice.
60 $rows = $wpdb->get_results(
61 "SELECT inv.meta_value AS invoice_id, SUM(CAST(amt.meta_value AS DECIMAL(18,4))) AS paid
62 FROM {$wpdb->posts} p
63 INNER JOIN {$wpdb->postmeta} inv ON inv.post_id = p.ID AND inv.meta_key = '_invoice_id'
64 INNER JOIN {$wpdb->postmeta} st ON st.post_id = p.ID AND st.meta_key = '_status'
65 LEFT JOIN {$wpdb->postmeta} amt ON amt.post_id = p.ID AND amt.meta_key = '_amount'
66 WHERE p.post_type = 'easy_invoice_payment' AND p.post_status = 'publish'
67 AND LOWER(st.meta_value) IN ('completed','complete','paid','success','succeeded')
68 AND inv.meta_value IN ($in)
69 GROUP BY inv.meta_value", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- integer ids.
70 ARRAY_A
71 );
72 foreach ( $chunk as $id ) {
73 self::$paid_cache[ $id ] = 0.0;
74 self::$credit_cache[ $id ] = 0.0;
75 }
76 foreach ( (array) $rows as $r ) {
77 self::$paid_cache[ (int) $r['invoice_id'] ] = round( (float) $r['paid'], 2 );
78 }
79 // Credit notes grouped by invoice.
80 $rows = $wpdb->get_results(
81 "SELECT inv.meta_value AS invoice_id, SUM(CAST(tot.meta_value AS DECIMAL(18,4))) AS credited
82 FROM {$wpdb->posts} p
83 INNER JOIN {$wpdb->postmeta} inv ON inv.post_id = p.ID AND inv.meta_key = '_easy_invoice_credited_invoice_id'
84 LEFT JOIN {$wpdb->postmeta} tot ON tot.post_id = p.ID AND tot.meta_key = '_easy_invoice_total'
85 WHERE p.post_type = 'easy_invoice_credit' AND p.post_status = 'publish'
86 AND inv.meta_value IN ($in)
87 GROUP BY inv.meta_value", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- integer ids.
88 ARRAY_A
89 );
90 foreach ( (array) $rows as $r ) {
91 self::$credit_cache[ (int) $r['invoice_id'] ] = round( (float) $r['credited'], 2 );
92 }
93 }
94 // phpcs:enable
95 }
96
97 /**
98 * Forget primed totals (after a payment or credit note is written).
99 *
100 * @param int|null $invoice_id One invoice, or everything when null.
101 */
102 public static function forget( ?int $invoice_id = null ): void {
103 if ( null === $invoice_id ) {
104 self::$paid_cache = self::$credit_cache = [];
105 return;
106 }
107 unset( self::$paid_cache[ $invoice_id ], self::$credit_cache[ $invoice_id ] );
108 }
109
110 /**
111 * Credit notes issued against an invoice.
112 *
113 * @param int $invoice_id Invoice.
114 * @return float
115 */
116 public static function credited( int $invoice_id ): float {
117 if ( $invoice_id <= 0 ) {
118 return 0.0;
119 }
120 if ( isset( self::$credit_cache[ $invoice_id ] ) ) {
121 return self::$credit_cache[ $invoice_id ];
122 }
123 return CreditNote::creditedTotal( $invoice_id );
124 }
125
126 /**
127 * Total less payments and credits, never below zero.
128 *
129 * @param mixed $invoice Invoice model.
130 * @return float
131 */
132 public static function due( $invoice ): float {
133 $total = is_callable( [ $invoice, 'getTotal' ] ) ? (float) $invoice->getTotal() : 0.0;
134 $id = is_callable( [ $invoice, 'getId' ] ) ? (int) $invoice->getId() : 0;
135 $due = max( 0.0, round( $total - self::paid( $id ) - self::credited( $id ), 2 ) );
136
137 /**
138 * Filter the amount still owed on an invoice — shown in the designs'
139 * header, the payment panel, and charged by the gateways.
140 *
141 * @param float $due Total less payments received and credit notes.
142 * @param mixed $invoice Invoice model.
143 */
144 return (float) apply_filters( 'easy_invoice_amount_due', $due, $invoice );
145 }
146
147 /**
148 * Open invoices still carrying a balance, with the amounts by currency
149 * and the subset past their due date.
150 *
151 * @param array $invoices Invoice models.
152 * @return array{count:int,overdue_count:int,amount:array,overdue_amount:array}
153 */
154 public static function outstanding( array $invoices ): array {
155 $out = [ 'count' => 0, 'overdue_count' => 0, 'amount' => [], 'overdue_amount' => [] ];
156 self::prime( array_map( function ( $invoice ) {
157 return is_callable( [ $invoice, 'getId' ] ) ? (int) $invoice->getId() : 0;
158 }, $invoices ) );
159 $today = gmdate( 'Y-m-d', current_time( 'timestamp' ) );
160 $site = strtoupper( (string) get_option( 'easy_invoice_currency_code', 'USD' ) );
161 foreach ( $invoices as $invoice ) {
162 $status = is_callable( [ $invoice, 'getStatus' ] ) ? strtolower( (string) $invoice->getStatus() ) : '';
163 if ( ! in_array( $status, [ 'available', 'unpaid', 'partial', 'overdue', 'sent', 'pending' ], true ) ) {
164 continue;
165 }
166 $due = self::due( $invoice );
167 if ( $due <= 0 ) {
168 continue;
169 }
170 $currency = is_callable( [ $invoice, 'getCurrencyCode' ] ) ? strtoupper( (string) $invoice->getCurrencyCode() ) : '';
171 if ( '' === $currency || 'GLOBAL' === $currency ) {
172 $currency = $site;
173 }
174 $out['count']++;
175 $out['amount'][ $currency ] = ( $out['amount'][ $currency ] ?? 0 ) + $due;
176 $due_date = is_callable( [ $invoice, 'getDueDate' ] ) ? (string) $invoice->getDueDate() : '';
177 if ( '' !== $due_date && strtotime( $due_date ) && gmdate( 'Y-m-d', strtotime( $due_date ) ) < $today ) {
178 $out['overdue_count']++;
179 $out['overdue_amount'][ $currency ] = ( $out['overdue_amount'][ $currency ] ?? 0 ) + $due;
180 }
181 }
182 return $out;
183 }
184
185 /**
186 * Whether payments and credits together cover the invoice.
187 *
188 * @param mixed $invoice Invoice model.
189 * @return bool
190 */
191 public static function isSettled( $invoice ): bool {
192 $total = is_callable( [ $invoice, 'getTotal' ] ) ? (float) $invoice->getTotal() : 0.0;
193 $id = is_callable( [ $invoice, 'getId' ] ) ? (int) $invoice->getId() : 0;
194 return self::paid( $id ) + self::credited( $id ) + 0.005 >= $total;
195 }
196 }
197