PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.3
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.3
2.4.3 2.4.4 2.4.2 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 All 60 releases
easy-invoice / includes / Gateways / OfflineGateway.php

OfflineGateway.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.3, at includes/Gateways/OfflineGateway.php

369 lines 15.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * One offline payment method — bank transfer, cheque, cash, or a method the
4 * merchant added — as a gateway the payment panel can offer.
5 *
6 * The client reads the instructions, optionally types the reference their
7 * bank gave them and attaches a receipt, and submits. A pending payment
8 * record is written against the invoice; staff confirm or reject it on the
9 * Payments screen and only a confirmed payment counts toward the balance.
10 *
11 * @package EasyInvoice
12 */
13
14 namespace EasyInvoice\Gateways;
15
16 use EasyInvoice\Abstracts\AbstractPaymentGateway;
17 use EasyInvoice\Interfaces\PaymentGatewayInterface;
18 use EasyInvoice\Services\InvoiceBalance;
19 use EasyInvoice\Services\OfflinePayments;
20
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit;
23 }
24
25 class OfflineGateway extends AbstractPaymentGateway implements PaymentGatewayInterface {
26
27 /** @var array{id:string,title:string,description:string,icon:string} */
28 private $method;
29
30 /**
31 * @param array $method One entry of OfflinePayments::methods().
32 */
33 public function __construct( array $method ) {
34 parent::__construct();
35 $this->method = $method;
36 add_action( 'easy_invoice_payment_gateways_after', [ $this, 'maybeRenderInstructions' ], 10, 2 );
37 }
38
39 public function init(): void {
40 $this->settings = $this->getSettings();
41 }
42
43 public function getName(): string {
44 return (string) $this->method['id'];
45 }
46
47 public function getTitle(): string {
48 return (string) $this->method['title'];
49 }
50
51 public function getDescription(): string {
52 return (string) ( $this->method['description'] ?? '' );
53 }
54
55 public function getIcon(): string {
56 return (string) ( $this->method['icon'] ?? 'bank' );
57 }
58
59 /** Built-in or merchant-added? The settings screen lets the latter be removed. */
60 public function isCustom(): bool {
61 return ! isset( OfflinePayments::builtin()[ $this->getName() ] );
62 }
63
64 public function isAvailable(): bool {
65 return $this->isEnabled();
66 }
67
68 public function getSettings(): array {
69 return OfflinePayments::settings( $this->getName() );
70 }
71
72 public function handleCallback( array $data ): array {
73 return [
74 'success' => true,
75 'message' => __( 'Offline payments have no callback.', 'easy-invoice' ),
76 ];
77 }
78
79 /**
80 * Fields shown under the method on Settings → Payment Methods.
81 */
82 public function getSettingsConfig(): array {
83 $id = $this->getName();
84 $fields = [];
85
86 if ( 'bank_transfer' === $id ) {
87 $fields += [
88 'easy_invoice_manual_bank_name' => [ 'label' => __( 'Bank name', 'easy-invoice' ), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-6 md:col-span-3' ],
89 'easy_invoice_manual_account_name' => [ 'label' => __( 'Account name', 'easy-invoice' ), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-6 md:col-span-3' ],
90 'easy_invoice_manual_account_number' => [ 'label' => __( 'Account number', 'easy-invoice' ), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-6 md:col-span-3' ],
91 'easy_invoice_manual_routing_number' => [ 'label' => __( 'Routing number / sort code', 'easy-invoice' ), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-6 md:col-span-3' ],
92 'easy_invoice_manual_iban' => [ 'label' => __( 'IBAN', 'easy-invoice' ), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-6 md:col-span-3' ],
93 'easy_invoice_manual_swift_code' => [ 'label' => __( 'SWIFT / BIC', 'easy-invoice' ), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-6 md:col-span-3' ],
94 'easy_invoice_manual_bank_address' => [ 'label' => __( 'Bank address', 'easy-invoice' ), 'type' => 'textarea', 'default' => '', 'col_span' => 'sm:col-span-6' ],
95 ];
96 } elseif ( 'cheque' === $id ) {
97 $fields += [
98 'easy_invoice_cheque_payable_to' => [ 'label' => __( 'Make cheques payable to', 'easy-invoice' ), 'type' => 'text', 'default' => '', 'col_span' => 'sm:col-span-6 md:col-span-3' ],
99 'easy_invoice_cheque_mailing_address' => [ 'label' => __( 'Mailing address', 'easy-invoice' ), 'type' => 'textarea', 'default' => '', 'col_span' => 'sm:col-span-6 md:col-span-3' ],
100 ];
101 }
102
103 $fields[ OfflinePayments::optionKey( $id, 'instructions' ) ] = [
104 'label' => __( 'Instructions shown to the client', 'easy-invoice' ),
105 'type' => 'textarea',
106 'default' => $this->settings['instructions'] ?? '',
107 'col_span' => 'sm:col-span-6',
108 'description' => __( 'Shown when the client picks this method. Plain text or simple HTML; the invoice number is always shown as the reference to quote.', 'easy-invoice' ),
109 ];
110 $fields[ OfflinePayments::optionKey( $id, 'reference' ) ] = [
111 'label' => __( 'Ask for a transaction reference', 'easy-invoice' ),
112 'type' => 'checkbox',
113 'default' => 'cash' === $id ? 'no' : 'yes',
114 'col_span' => 'sm:col-span-6 md:col-span-3',
115 'description' => __( 'The reference or confirmation number the client\'s bank gave them.', 'easy-invoice' ),
116 ];
117 $fields[ OfflinePayments::optionKey( $id, 'proof' ) ] = [
118 'label' => __( 'Receipt upload', 'easy-invoice' ),
119 'type' => 'select',
120 'default' => 'cash' === $id ? 'off' : 'optional',
121 'col_span' => 'sm:col-span-6 md:col-span-3',
122 'options' => [
123 'off' => __( 'Not offered', 'easy-invoice' ),
124 'optional' => __( 'Optional', 'easy-invoice' ),
125 'required' => __( 'Required', 'easy-invoice' ),
126 ],
127 ];
128
129 return [ 'fields' => $fields ];
130 }
131
132 /* ------------------------------------------------------------------ */
133 /* Client side */
134 /* ------------------------------------------------------------------ */
135
136 /**
137 * What the panel shows once this method is picked: details, instructions
138 * and the fields the client fills in. Printed on
139 * easy_invoice_payment_gateways_after for this gateway only.
140 *
141 * @param object $invoice Invoice model.
142 * @param string $selected_gateway Gateway the client chose.
143 */
144 public function maybeRenderInstructions( $invoice, $selected_gateway = '' ): void {
145 if ( $selected_gateway !== $this->getName() ) {
146 return;
147 }
148 $settings = OfflinePayments::settings( $this->getName() );
149 $details = OfflinePayments::details( $this->getName() );
150 $number = is_object( $invoice ) && method_exists( $invoice, 'getNumber' ) ? (string) $invoice->getNumber() : '';
151 $due = is_object( $invoice ) ? InvoiceBalance::due( $invoice ) : 0.0;
152 $formatter = class_exists( '\EasyInvoice\Helpers\InvoiceFormatter' ) && is_object( $invoice ) ? new \EasyInvoice\Helpers\InvoiceFormatter( $invoice ) : null;
153 $label = OfflinePayments::label( $this->getName() );
154 ?>
155 <div class="ei-offline" data-ei-offline="1" data-ei-submit-label="<?php echo esc_attr( __( 'Submit payment details', 'easy-invoice' ) ); ?>">
156 <h4><?php echo esc_html( $label ); ?></h4>
157 <?php if ( $details ) : ?>
158 <table class="bank-details-table ei-offline__details">
159 <tbody>
160 <?php foreach ( $details as $row_label => $value ) : ?>
161 <tr><th scope="row"><?php echo esc_html( $row_label ); ?></th><td><?php echo nl2br( esc_html( $value ) ); ?></td></tr>
162 <?php endforeach; ?>
163 </tbody>
164 </table>
165 <?php endif; ?>
166 <?php if ( '' !== trim( $settings['instructions'] ) ) : ?>
167 <div class="ei-offline__instructions"><?php echo wp_kses_post( wpautop( $settings['instructions'] ) ); ?></div>
168 <?php endif; ?>
169 <?php if ( '' !== $number ) : ?>
170 <p class="payment-reference"><strong><?php esc_html_e( 'Reference to quote:', 'easy-invoice' ); ?></strong> <?php echo esc_html( $number ); ?>
171 <?php if ( $formatter && $due > 0 ) : ?>
172 · <strong><?php esc_html_e( 'Amount due:', 'easy-invoice' ); ?></strong> <?php echo esc_html( $formatter->format( $due ) ); ?>
173 <?php endif; ?>
174 </p>
175 <?php endif; ?>
176
177 <div class="ei-offline__form">
178 <p class="ei-offline__lead"><?php esc_html_e( 'Once you have paid, tell us here so we can match it to your invoice.', 'easy-invoice' ); ?></p>
179 <?php if ( $settings['reference'] ) : ?>
180 <div class="form-group">
181 <label for="ei-payment-reference"><?php esc_html_e( 'Transaction reference', 'easy-invoice' ); ?> <span class="ei-offline__optional"><?php esc_html_e( '(optional)', 'easy-invoice' ); ?></span></label>
182 <input type="text" id="ei-payment-reference" name="payment_reference" maxlength="120" autocomplete="off" placeholder="<?php esc_attr_e( 'e.g. the confirmation number from your bank', 'easy-invoice' ); ?>">
183 </div>
184 <?php endif; ?>
185 <?php if ( 'off' !== $settings['proof'] ) : ?>
186 <div class="form-group">
187 <label for="ei-payment-proof"><?php esc_html_e( 'Receipt or proof of payment', 'easy-invoice' ); ?>
188 <?php if ( 'required' === $settings['proof'] ) : ?><span class="ei-offline__required">*</span><?php else : ?><span class="ei-offline__optional"><?php esc_html_e( '(optional)', 'easy-invoice' ); ?></span><?php endif; ?>
189 </label>
190 <input type="file" id="ei-payment-proof" name="payment_proof" accept="image/jpeg,image/png,image/gif,image/webp,application/pdf" <?php echo 'required' === $settings['proof'] ? 'data-ei-required="1"' : ''; ?>>
191 <p class="reference-note"><?php esc_html_e( 'JPG, PNG, GIF, WebP or PDF, up to 5 MB. Only our staff can open it.', 'easy-invoice' ); ?></p>
192 </div>
193 <?php endif; ?>
194 <div class="form-group">
195 <label for="ei-payment-notes"><?php esc_html_e( 'Note to us', 'easy-invoice' ); ?> <span class="ei-offline__optional"><?php esc_html_e( '(optional)', 'easy-invoice' ); ?></span></label>
196 <textarea id="ei-payment-notes" name="payment_notes" rows="2" maxlength="1000"></textarea>
197 </div>
198 </div>
199 </div>
200 <?php
201 }
202
203 /**
204 * Cut a string to a number of characters (mbstring when available).
205 */
206 private static function clip( string $value, int $max ): string {
207 return function_exists( 'mb_substr' ) ? mb_substr( $value, 0, $max ) : substr( $value, 0, $max );
208 }
209
210 /**
211 * The client pressed "Submit payment details": record what they told us
212 * as a pending payment and let staff know.
213 *
214 * @param float $amount Amount the panel is submitting (the balance, or an instalment when allowed).
215 * @param array $data Request fields.
216 */
217 public function processPayment( float $amount, array $data = [] ): array {
218 $invoice_id = isset( $data['invoice_id'] ) ? absint( $data['invoice_id'] ) : 0;
219 $post = $invoice_id ? get_post( $invoice_id ) : null;
220 if ( ! $post || \EasyInvoice\Constants\PostTypes::EASY_INVOICE_POST_TYPE !== $post->post_type ) {
221 return [
222 'success' => false,
223 'message' => __( 'Invalid invoice.', 'easy-invoice' ),
224 ];
225 }
226 $invoice = new \EasyInvoice\Models\Invoice( $post );
227 $settings = OfflinePayments::settings( $this->getName() );
228
229 // A client can tell us about a payment more than once (a second
230 // instalment, a corrected reference) but not flood the invoice.
231 $open = OfflinePayments::pendingForInvoice( $invoice_id );
232 /**
233 * Filter how many offline submissions may await confirmation on one invoice.
234 *
235 * @param int $max Default 3.
236 */
237 if ( count( $open ) >= (int) apply_filters( 'easy_invoice_offline_pending_limit', 3 ) ) {
238 return [
239 'success' => false,
240 'message' => __( 'You have already told us about a payment on this invoice and we are checking it. Please contact us if something has changed.', 'easy-invoice' ),
241 ];
242 }
243
244 $reference = isset( $data['payment_reference'] ) ? sanitize_text_field( wp_unslash( (string) $data['payment_reference'] ) ) : '';
245 $notes = isset( $data['payment_notes'] ) ? sanitize_textarea_field( wp_unslash( (string) $data['payment_notes'] ) ) : '';
246 $reference = self::clip( $reference, 120 );
247 $notes = self::clip( $notes, 1000 );
248
249 $proof = null;
250 // The upload is validated in storeProof() (is_uploaded_file, size, real
251 // file type) — nothing from $_FILES is used before that.
252 // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput
253 $upload = isset( $_FILES['payment_proof'] ) && is_array( $_FILES['payment_proof'] ) ? $_FILES['payment_proof'] : [];
254 // phpcs:enable
255 if ( 'off' !== $settings['proof'] && ! empty( $upload['name'] ) ) {
256 $proof = OfflinePayments::storeProof( $upload );
257 if ( is_wp_error( $proof ) ) {
258 return [
259 'success' => false,
260 'message' => $proof->get_error_message(),
261 ];
262 }
263 }
264 if ( 'required' === $settings['proof'] && null === $proof ) {
265 return [
266 'success' => false,
267 'message' => __( 'Please attach your receipt or proof of payment.', 'easy-invoice' ),
268 ];
269 }
270
271 $due = InvoiceBalance::due( $invoice );
272 if ( $amount <= 0 || $amount > $due + 0.005 ) {
273 $amount = $due;
274 }
275 if ( $amount <= 0 ) {
276 return [
277 'success' => false,
278 'message' => __( 'Nothing is owed on this invoice.', 'easy-invoice' ),
279 ];
280 }
281
282 $currency_code = $invoice->getCurrencyCode();
283 if ( '' === (string) $currency_code || 'global' === $currency_code ) {
284 $currency_code = get_option( 'easy_invoice_currency_code', 'USD' );
285 }
286 $currency_symbol = \EasyInvoice\Helpers\CurrencyHelper::getCurrencySymbol( $currency_code );
287 $method = $this->getName();
288 $transaction_id = strtoupper( $method ) . '-' . $invoice_id . '-' . time();
289
290 $meta = [
291 '_invoice_id' => $invoice_id,
292 '_amount' => round( $amount, 2 ),
293 '_payment_method' => $method,
294 '_status' => 'pending',
295 '_transaction_id' => $transaction_id,
296 '_payment_date' => current_time( 'mysql' ),
297 '_notes' => $notes,
298 '_payment_type' => 'offline',
299 '_currency' => $currency_code,
300 '_currency_symbol' => $currency_symbol,
301 '_submitted_by' => get_current_user_id(),
302 '_gateway_response' => wp_json_encode(
303 [
304 'method' => $method,
305 'submitted' => current_time( 'mysql' ),
306 'reference' => $reference,
307 'ip' => isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '',
308 ]
309 ),
310 ];
311 if ( '' !== $reference ) {
312 $meta[ OfflinePayments::META_REFERENCE ] = $reference;
313 }
314 if ( $proof ) {
315 $meta[ OfflinePayments::META_PROOF_FILE ] = $proof['file'];
316 $meta[ OfflinePayments::META_PROOF_NAME ] = $proof['name'];
317 }
318
319 $payment_id = wp_insert_post(
320 [
321 /* translators: 1: payment method, 2: invoice number. */
322 'post_title' => sprintf( __( '%1$s for invoice %2$s', 'easy-invoice' ), OfflinePayments::label( $method ), $invoice->getNumber() ),
323 'post_type' => 'easy_invoice_payment',
324 'post_status' => 'publish',
325 'post_author' => get_current_user_id() ?: (int) $post->post_author,
326 'meta_input' => $meta,
327 ],
328 true
329 );
330 if ( is_wp_error( $payment_id ) || ! $payment_id ) {
331 if ( $proof ) {
332 $abs = trailingslashit( wp_upload_dir()['basedir'] ) . $proof['file'];
333 if ( file_exists( $abs ) ) {
334 wp_delete_file( $abs );
335 }
336 }
337 return [
338 'success' => false,
339 'message' => __( 'Your payment details could not be saved. Please try again or contact us.', 'easy-invoice' ),
340 ];
341 }
342
343 // The invoice keeps its status (a pending payment is not money yet);
344 // the panel and the page read this to say a submission is waiting.
345 update_post_meta( $invoice_id, '_payment_status', 'pending' );
346 update_post_meta( $invoice_id, '_easy_invoice_payment_method', $method );
347
348 /**
349 * Fires when a client says they have paid (or will pay) by an
350 * offline method. The payment record is pending until staff confirm it.
351 *
352 * @param int $invoice_id Invoice.
353 * @param string $method Method id (bank_transfer, cheque, cash, or a custom id).
354 * @param int $payment_id Pending payment record.
355 */
356 do_action( 'easy_invoice_manual_payment_submitted', $invoice_id, $method, (int) $payment_id );
357
358 $formatter = new \EasyInvoice\Helpers\InvoiceFormatter( $invoice );
359 return [
360 'success' => true,
361 'offline' => true,
362 'payment_id' => (int) $payment_id,
363 'payment_status' => 'pending',
364 /* translators: 1: amount, 2: payment method. */
365 'message' => sprintf( __( 'Thank you — we have noted your %1$s payment by %2$s. It will show as paid once we have confirmed it.', 'easy-invoice' ), $formatter->format( $amount ), OfflinePayments::label( $method ) ),
366 ];
367 }
368 }
369