PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.15
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.15
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / includes / Gateways / Cash.php

Cash.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.9.15, at includes/Gateways/Cash.php

382 lines 12.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Provides a Cash Payment Gateway.
4 *
5 * @author Paul Kilmurray <paul@kilbot.com>
6 *
7 * @see https://wcpos.com
8 *
9 * @extends WC_Payment_Gateway
10 * @package WCPOS\WooCommercePOS
11 */
12
13 namespace WCPOS\WooCommercePOS\Gateways;
14
15 use WC_Order;
16 use WC_Order_Item_Fee;
17 use WC_Payment_Gateway;
18
19 /**
20 * Cash class.
21 */
22 class Cash extends WC_Payment_Gateway {
23 /**
24 * Constructor for the gateway.
25 */
26 public function __construct() {
27 $this->id = 'pos_cash';
28 $this->title = /* translators: POS payment gateway label shown during checkout. */ __( 'Cash', 'woocommerce-pos' );
29 $this->description = '';
30 $this->icon = apply_filters( 'woocommerce_pos_cash_icon', '' );
31 $this->has_fields = true;
32 $this->enabled = 'no';
33 $this->supports = array( 'products', 'refunds' );
34
35 // Actions.
36 // @phpstan-ignore-next-line.
37 add_action(
38 'woocommerce_pos_update_options_payment_gateways_' . $this->id,
39 array(
40 $this,
41 'process_admin_options',
42 )
43 );
44 add_action( 'woocommerce_thankyou_pos_cash', array( $this, 'calculate_change' ) );
45 add_filter( 'wcpos_payment_gateway_provider', array( $this, 'wcpos_provider' ), 10, 3 );
46 add_filter( 'wcpos_payment_gateway_pos_type', array( $this, 'wcpos_pos_type' ), 10, 3 );
47 add_filter( 'wcpos_payment_gateway_provider_data', array( $this, 'wcpos_provider_data' ), 10, 3 );
48 add_filter( 'wcpos_payment_gateway_bootstrap', array( $this, 'wcpos_bootstrap' ), 10, 4 );
49 add_filter( 'wcpos_process_checkout_action_' . $this->id, array( $this, 'wcpos_process_checkout_action' ), 10, 5 );
50 }
51
52 /**
53 * Get payment details.
54 *
55 * @param WC_Order $order Order object.
56 *
57 * @return array
58 */
59 public static function payment_details( WC_Order $order ) {
60 return array(
61 'tendered' => get_post_meta( $order->get_id(), '_pos_cash_amount_tendered', true ),
62 'change' => get_post_meta( $order->get_id(), '_pos_cash_change', true ),
63 );
64 }
65
66 /**
67 * Display the payment fields on the checkout modal.
68 */
69 public function payment_fields(): void {
70 global $wp;
71
72 $order_id = isset( $wp->query_vars['order-pay'] ) ? $wp->query_vars['order-pay'] : null;
73 $order = $order_id ? wc_get_order( $order_id ) : null;
74
75 if ( $this->description ) {
76 echo '<p>' . wp_kses_post( $this->description ) . '</p>';
77 }
78
79 $currency_pos = get_option( 'woocommerce_currency_pos' );
80
81 if ( 'left' == $currency_pos || 'left_space' == $currency_pos ) {
82 $left_addon = '<span class="input-group-addon">' . get_woocommerce_currency_symbol( get_woocommerce_currency() ) . '</span>';
83 $right_addon = '';
84 } else {
85 $left_addon = '';
86 $right_addon = '<span class="input-group-addon">' . get_woocommerce_currency_symbol( get_woocommerce_currency() ) . '</span>';
87 }
88
89 echo '
90 <div class="form-row" id="pos-cash-tendered_field" style="display: flex; justify-content: space-between;">
91 <div style="flex: 1;">
92 <label for="pos-cash-tendered" style="padding-left:0">' . /* translators: Cash checkout field label for the amount of money received from the customer. */ esc_html__( 'Amount Tendered', 'woocommerce-pos' ) . '</label>
93 <div class="input-group">
94 ' . wp_kses_post( $left_addon ) . '
95 <input type="text" class="form-control" name="pos-cash-tendered" id="pos-cash-tendered" maxlength="20" data-numpad="cash" data-label="' . /* translators: Cash numpad label for the amount of money received from the customer. */ esc_attr__( 'Amount Tendered', 'woocommerce-pos' ) . '" data-placement="bottom" data-value="{{total}}">
96 ' . wp_kses_post( $right_addon ) . '
97 </div>
98 </div>
99 <div style="flex: 1;">
100 <label style="padding-left:0">' . /* translators: Cash checkout label for money returned to the customer when the received amount exceeds the total. */ esc_html__( 'Change', 'woocommerce-pos' ) . '</label>
101 <div id="pos-cash-change-display"></div>
102 </div>';
103 wp_nonce_field( 'pos_cash_payment_nonce', 'pos_cash_payment_nonce_field' );
104 echo '
105 </div>
106 ';
107
108 if ( $order && $order->get_total() > 0 ) {
109 echo '
110 <script>
111 document.addEventListener("DOMContentLoaded", function() {
112 var tenderedInput = document.getElementById("pos-cash-tendered");
113 var changeDisplay = document.getElementById("pos-cash-change-display");
114
115 tenderedInput.addEventListener("input", function() {
116 var tenderedAmount = parseFloat(tenderedInput.value);
117 var orderTotal = ' . json_encode( wc_format_decimal( $order->get_total() ) ) . '; // Get order total from PHP
118 var change = tenderedAmount - orderTotal;
119
120 if (change > 0) {
121 changeDisplay.innerHTML = change.toFixed(' . json_encode( wc_get_price_decimals() ) . ');
122 } else {
123 changeDisplay.innerHTML = "";
124 }
125 });
126 });
127 </script>
128 ';
129 }
130 }
131
132 /**
133 * Process the payment.
134 *
135 * @param int $order_id Order ID.
136 *
137 * @return string[]
138 */
139 public function process_payment( $order_id ): array {
140 // Check nonce.
141 if ( ! isset( $_POST['pos_cash_payment_nonce_field'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pos_cash_payment_nonce_field'] ) ), 'pos_cash_payment_nonce' ) ) {
142 wp_die( /* translators: Checkout error shown when cash nonce validation fails. */ esc_html__( 'Nonce verification failed', 'woocommerce-pos' ) );
143 }
144
145 // get order object.
146 $order = new WC_Order( $order_id );
147 $tendered = $order->get_total();
148
149 // get pos_cash data from $_POST.
150 if ( isset( $_POST['pos-cash-tendered'] ) && ! empty( $_POST['pos-cash-tendered'] ) ) {
151 $tendered = wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['pos-cash-tendered'] ) ) );
152 }
153 $result = $this->apply_tendered_payment( $order, $tendered, true );
154 if ( is_wp_error( $result ) ) {
155 wc_add_notice( $result->get_error_message(), 'error' );
156
157 return array(
158 'result' => 'failure',
159 'redirect' => '',
160 );
161 }
162
163 // Return thankyou redirect
164 // $redirect = add_query_arg(array(
165 // 'wcpos' => 1,
166 // ), get_home_url( null, '/wcpos-checkout/order-received/' . $order->get_id() ));.
167
168 return array(
169 'result' => 'success',
170 'redirect' => $this->get_return_url( $order ),
171 );
172 }
173
174 /**
175 * Process a refund.
176 *
177 * Cash refunds are handled manually, so just return true.
178 *
179 * @param int $order_id Order ID.
180 * @param float|null $amount Refund amount.
181 * @param string $reason Refund reason.
182 *
183 * @return bool
184 */
185 public function process_refund( $order_id, $amount = null, $reason = '' ) {
186 return true;
187 }
188
189 /**
190 * Calculate and display change.
191 *
192 * @param int $order_id Order ID.
193 */
194 public function calculate_change( $order_id ): void {
195 $message = '';
196 $tendered = get_post_meta( $order_id, '_pos_cash_amount_tendered', true );
197 $change = get_post_meta( $order_id, '_pos_cash_change', true );
198
199 // construct message.
200 if ( $tendered && $change ) {
201 $message = /* translators: Order note label for the cash amount received from the customer at checkout. */ __( 'Amount Tendered', 'woocommerce-pos' ) . ': ';
202 $message .= wc_price( $tendered ) . '<br>';
203 $message .= /* translators: Money returned to the customer after a cash payment. */ _x( 'Change', 'Money returned from cash sale', 'woocommerce-pos' ) . ': ';
204 $message .= wc_price( $change );
205 }
206
207 echo wp_kses_post( $message );
208 }
209
210 /**
211 * POS provider family.
212 *
213 * @param string $provider Provider value.
214 * @param mixed $gateway Gateway object.
215 * @param mixed $request REST request.
216 */
217 public function wcpos_provider( $provider, $gateway, $request = null ) {
218 if ( $gateway instanceof WC_Payment_Gateway && $this->id === $gateway->id ) {
219 return 'wcpos';
220 }
221
222 return $provider;
223 }
224
225 /**
226 * POS type.
227 *
228 * @param string $pos_type POS type.
229 * @param mixed $gateway Gateway object.
230 * @param mixed $request REST request.
231 */
232 public function wcpos_pos_type( $pos_type, $gateway, $request = null ) {
233 if ( $gateway instanceof WC_Payment_Gateway && $this->id === $gateway->id ) {
234 return 'manual';
235 }
236
237 return $pos_type;
238 }
239
240 /**
241 * Non-secret provider data.
242 *
243 * @param array $provider_data Provider data.
244 * @param mixed $gateway Gateway object.
245 * @param mixed $request REST request.
246 */
247 public function wcpos_provider_data( $provider_data, $gateway, $request = null ) {
248 if ( $gateway instanceof WC_Payment_Gateway && $this->id === $gateway->id ) {
249 return array();
250 }
251
252 return $provider_data;
253 }
254
255 /**
256 * Bootstrap response for POS cash.
257 *
258 * @param array $response Bootstrap response.
259 * @param string $gateway_id Gateway ID.
260 * @param array $context Bootstrap context.
261 * @param mixed $request REST request.
262 */
263 public function wcpos_bootstrap( $response, $gateway_id, $context = array(), $request = null ) {
264 if ( $this->id === $gateway_id ) {
265 return array(
266 'gateway_id' => $gateway_id,
267 'status' => 'ready',
268 'expires_at' => null,
269 'provider_data' => array(),
270 );
271 }
272
273 return $response;
274 }
275
276 /**
277 * Handle POS checkout contract.
278 *
279 * @param array|\WP_Error $state Checkout state.
280 * @param string $action Checkout action.
281 * @param array $payment_data Payment data.
282 * @param WC_Order $order Order object.
283 * @param mixed $request REST request.
284 */
285 public function wcpos_process_checkout_action( $state, $action, $payment_data, $order, $request = null ) {
286 if ( is_wp_error( $state ) ) {
287 return $state;
288 }
289
290 if ( ( $state['gateway_id'] ?? '' ) !== $this->id ) {
291 return $state;
292 }
293
294 if ( 'cancel' === $action ) {
295 $state['status'] = 'cancelled';
296 return $state;
297 }
298
299 if ( 'start' !== $action ) {
300 return $state;
301 }
302
303 $tendered = isset( $payment_data['amount_tendered'] ) && '' !== $payment_data['amount_tendered']
304 ? wc_format_decimal( $payment_data['amount_tendered'] )
305 : wc_format_decimal( $order->get_total() );
306
307 $result = $this->apply_tendered_payment( $order, $tendered, false );
308 if ( is_wp_error( $result ) ) {
309 return $result;
310 }
311
312 $state['status'] = 'completed';
313 $state['provider_data'] = array(
314 'amount_tendered' => $order->get_meta( '_pos_cash_amount_tendered' ),
315 'change' => $order->get_meta( '_pos_cash_change' ),
316 );
317
318 return $state;
319 }
320
321 /**
322 * Apply a tendered cash amount to an order.
323 *
324 * @param WC_Order $order Order object.
325 * @param string $tendered Tendered amount.
326 * @param bool $allow_partial Whether partial cash is allowed.
327 *
328 * @return true|\WP_Error
329 */
330 private function apply_tendered_payment( WC_Order $order, string $tendered, bool $allow_partial ) {
331 $tendered = wc_format_decimal( $tendered );
332
333 if ( (float) $tendered < 0 ) {
334 return new \WP_Error(
335 'wcpos_invalid_tendered_amount',
336 /* translators: Checkout validation error for the cash amount received from the customer. */
337 __( 'Tendered amount must be zero or greater.', 'woocommerce-pos' ),
338 array( 'status' => 400 )
339 );
340 }
341
342 $change = $tendered > $order->get_total() ? wc_format_decimal( floatval( $tendered ) - floatval( $order->get_total() ) ) : '0';
343 $order->set_payment_method( $this->id );
344 $order->set_payment_method_title( $this->title );
345 $order->update_meta_data( '_pos_cash_amount_tendered', $tendered );
346 $order->update_meta_data( '_pos_cash_change', $change );
347
348 if ( $tendered >= $order->get_total() ) {
349 $order->payment_complete();
350 return true;
351 }
352
353 if ( ! $allow_partial ) {
354 return new \WP_Error(
355 'wcpos_partial_cash_not_supported',
356 __( 'Partial cash payments are not supported by the POS API checkout flow.', 'woocommerce-pos' ),
357 array( 'status' => 400 )
358 );
359 }
360
361 $fee = new WC_Order_Item_Fee();
362 $fee->set_props(
363 array(
364 'name' => /* translators: Fee line-item name added when a cash payment is partial. */ __( 'Partial Payment', 'woocommerce-pos' ),
365 'tax_class' => 0,
366 'amount' => '-' . $tendered,
367 'total' => '-' . $tendered,
368 'total_tax' => 0,
369 )
370 );
371 $fee->add_meta_data( 'date_paid_gmt', gmdate( 'Y-m-d\TH:i:s' ), true );
372 $fee->set_order_id( $order->get_id() );
373 $fee->save();
374
375 $order->add_item( $fee );
376 $order->set_total( wc_format_decimal( floatval( $order->get_total() ) - floatval( $tendered ) ) );
377 $order->update_status( 'wc-pos-partial' );
378
379 return true;
380 }
381 }
382