| @@ -49,8 +49,30 @@ | ||
| 49 | 49 | return $total_payments; |
| 50 | 50 | } |
| 51 | 51 | |
| 52 | 52 | /** |
| 53 | + * Amount of the most recently completed payment on an invoice. | |
| 54 | + * | |
| 55 | + * @param int $invoice_id | |
| 56 | + * @return float Zero when there is none. | |
| 57 | + */ | |
| 58 | + private function latestCompletedPaymentAmount($invoice_id): float | |
| 59 | + { | |
| 60 | + $payments = get_posts([ | |
| 61 | + 'post_type' => 'easy_invoice_payment', | |
| 62 | + 'post_status' => 'publish', | |
| 63 | + 'numberposts' => 1, | |
| 64 | + 'orderby' => 'date', | |
| 65 | + 'order' => 'DESC', | |
| 66 | + 'meta_query' => [ | |
| 67 | + ['key' => '_invoice_id', 'value' => $invoice_id, 'compare' => '='], | |
| 68 | + ['key' => '_status', 'value' => 'completed', 'compare' => '='], | |
| 69 | + ], | |
| 70 | + ]); | |
| 71 | + return $payments ? (float) get_post_meta($payments[0]->ID, '_amount', true) : 0.0; | |
| 72 | + } | |
| 73 | + | |
| 74 | + /** | |
| 53 | 75 | * Check if invoice should be marked as paid based on total payments |
| 54 | 76 | * |
| 55 | 77 | * @param int $invoice_id |
| 56 | 78 | * @param \EasyInvoice\Models\Invoice $invoice |
| @@ -58,11 +80,13 @@ | ||
| 58 | 80 | */ |
| 59 | 81 | private function shouldMarkInvoiceAsPaid($invoice_id, $invoice): bool |
| 60 | 82 | { |
| 61 | 83 | $total_payments = $this->calculateTotalPaymentsForInvoice($invoice_id); |
| 62 | - $invoice_total = $invoice->getTotal(); | |
| 63 | - | |
| 64 | - return $total_payments >= $invoice_total; | |
| 84 | + $invoice_total = (float) $invoice->getTotal(); | |
| 85 | + // Credit notes reduce what is owed, so they count towards settlement. | |
| 86 | + $credited = class_exists('\\EasyInvoice\\Services\\InvoiceBalance') ? \EasyInvoice\Services\InvoiceBalance::credited((int) $invoice_id) : 0.0; | |
| 87 | + | |
| 88 | + return $total_payments + $credited + 0.005 >= $invoice_total; | |
| 65 | 89 | } |
| 66 | 90 | |
| 67 | 91 | /** |
| 68 | 92 | * Update invoice status to paid only if total payments are sufficient |
| @@ -84,17 +108,44 @@ | ||
| 84 | 108 | } else { |
| 85 | 109 | error_log("Easy Invoice: Invoice #$invoice_id status updated to 'paid' - payments complete"); |
| 86 | 110 | } |
| 87 | 111 | |
| 112 | + // Trigger hook when invoice is marked as paid (for email notifications, etc.) | |
| 113 | + // Get payment method and transaction ID from the most recent payment | |
| 114 | + $payment_method = get_post_meta($invoice_id, '_payment_method', true) ?: $gateway_name ?: 'online'; | |
| 115 | + $transaction_id = get_post_meta($invoice_id, '_transaction_id', true) ?: ''; | |
| 116 | + // The receipt names the payment that settled the invoice, which | |
| 117 | + // is the whole total only when it was paid in one go. | |
| 118 | + $payment_amount = $this->latestCompletedPaymentAmount($invoice_id) ?: $invoice->getTotal(); | |
| 119 | + | |
| 120 | + do_action('easy_invoice_payment_completed', $invoice_id, $invoice, [ | |
| 121 | + 'payment_method' => $payment_method, | |
| 122 | + 'gateway_name' => $gateway_name, | |
| 123 | + 'transaction_id' => $transaction_id, | |
| 124 | + 'amount' => $payment_amount | |
| 125 | + ]); | |
| 126 | + | |
| 88 | 127 | return true; |
| 89 | 128 | } else { |
| 90 | - // Log that status remains unchanged | |
| 91 | - if (method_exists($this, 'log')) { | |
| 92 | - $this->log("Invoice #$invoice_id status remains unchanged - partial payment received", 'info'); | |
| 93 | - } else { | |
| 94 | - error_log("Easy Invoice: Invoice #$invoice_id status remains unchanged - partial payment received"); | |
| 129 | + // Something has been received but not everything: say so, unless | |
| 130 | + // the invoice is in a state that should not change (draft, | |
| 131 | + // cancelled) or already reads as part paid. | |
| 132 | + $current = strtolower((string) $invoice->getStatus()); | |
| 133 | + $received = $this->calculateTotalPaymentsForInvoice($invoice_id); | |
| 134 | + if (in_array($current, ['available', 'unpaid', 'overdue'], true) && $received > 0) { | |
| 135 | + $invoice->setStatus('partial'); | |
| 136 | + $invoice->save(); | |
| 95 | 137 | } |
| 96 | - | |
| 138 | + if ($received > 0) { | |
| 139 | + /** This action is documented in includes/Controllers/PaymentController.php */ | |
| 140 | + do_action('easy_invoice_payment_received', $invoice_id, $invoice, [ | |
| 141 | + 'payment_method' => get_post_meta($invoice_id, '_payment_method', true) ?: $gateway_name ?: 'online', | |
| 142 | + 'gateway_name' => $gateway_name, | |
| 143 | + 'transaction_id' => get_post_meta($invoice_id, '_transaction_id', true) ?: '', | |
| 144 | + 'amount' => $this->latestCompletedPaymentAmount($invoice_id), | |
| 145 | + ]); | |
| 146 | + } | |
| 147 | + | |
| 97 | 148 | return false; |
| 98 | 149 | } |
| 99 | 150 | } |
| 100 | 151 | } |