1
|
<?php
|
2
|
|
3
|
/**
|
4
|
* @file
|
5
|
* Callback functions for the Payment module's checkout panes.
|
6
|
*/
|
7
|
|
8
|
// Constants that govern the behavior of the payment method checkout pane when
|
9
|
// no payment methods are enabled for an order.
|
10
|
define('COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY', 'empty');
|
11
|
define('COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY_EVENT', 'empty_event');
|
12
|
define('COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE', 'message');
|
13
|
define('COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE_EVENT', 'message_event');
|
14
|
|
15
|
|
16
|
/**
|
17
|
* Checkout pane callback: returns the payment pane's settings form.
|
18
|
*/
|
19
|
function commerce_payment_pane_settings_form($checkout_pane) {
|
20
|
$form = array();
|
21
|
|
22
|
$form['commerce_payment_pane_require_method'] = array(
|
23
|
'#type' => 'checkbox',
|
24
|
'#title' => t('Require a payment method at all times, preventing checkout if none is available.'),
|
25
|
'#default_value' => variable_get('commerce_payment_pane_require_method', FALSE),
|
26
|
);
|
27
|
$form['commerce_payment_pane_no_method_behavior'] = array(
|
28
|
'#type' => 'radios',
|
29
|
'#title' => t('Checkout pane behavior when no payment methods are enabled for an order'),
|
30
|
'#description' => t('Note: regardless of your selection, no payment transaction will be created for the order upon checkout completion as they represent actual financial transactions.'),
|
31
|
'#options' => array(
|
32
|
COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY => t('Leave the payment checkout pane empty.'),
|
33
|
COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY_EVENT => t('Leave the payment checkout pane empty and trigger <em>When an order is first paid in full</em> on submission of free orders.'),
|
34
|
COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE => t('Display a message in the pane indicating payment is not required for the order.'),
|
35
|
COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE_EVENT => t('Display a message in the pane indicating payment is not required and trigger <em>When an order is first paid in full</em> on submission of free orders.'),
|
36
|
),
|
37
|
'#default_value' => variable_get('commerce_payment_pane_no_method_behavior', COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE),
|
38
|
'#states' => array(
|
39
|
'visible' => array(
|
40
|
':input[name="commerce_payment_pane_require_method"]' => array('checked' => FALSE),
|
41
|
),
|
42
|
),
|
43
|
);
|
44
|
|
45
|
return $form;
|
46
|
}
|
47
|
|
48
|
/**
|
49
|
* Payment pane: form callback.
|
50
|
*/
|
51
|
function commerce_payment_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {
|
52
|
$pane_form = array();
|
53
|
|
54
|
// Invoke the payment methods event that will populate the order with
|
55
|
// an array of method IDs for available payment methods.
|
56
|
$order->payment_methods = array();
|
57
|
rules_invoke_all('commerce_payment_methods', $order);
|
58
|
|
59
|
// Sort the payment methods array by the enabling Rules' weight values.
|
60
|
uasort($order->payment_methods, 'drupal_sort_weight');
|
61
|
|
62
|
// Generate an array of payment method options for the checkout form.
|
63
|
$options = array();
|
64
|
|
65
|
foreach ($order->payment_methods as $instance_id => $method_info) {
|
66
|
// Ensure we've received a valid payment method that can be used on the
|
67
|
// checkout form.
|
68
|
if ($payment_method = commerce_payment_method_load($method_info['method_id'])) {
|
69
|
if (!empty($payment_method['checkout'])) {
|
70
|
$options[$instance_id] = $payment_method['display_title'];
|
71
|
}
|
72
|
}
|
73
|
}
|
74
|
|
75
|
// If no payment methods were found, return the empty form.
|
76
|
if (empty($options)) {
|
77
|
if (!variable_get('commerce_payment_pane_require_method', FALSE)) {
|
78
|
$behavior = variable_get('commerce_payment_pane_no_method_behavior', COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE);
|
79
|
|
80
|
switch ($behavior) {
|
81
|
case COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE:
|
82
|
case COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE_EVENT:
|
83
|
$pane_form['message'] = array(
|
84
|
'#markup' => '<div>' . t('Payment is not required to complete your order.') . '</div>',
|
85
|
);
|
86
|
break;
|
87
|
|
88
|
case COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY:
|
89
|
case COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY_EVENT:
|
90
|
default:
|
91
|
break;
|
92
|
}
|
93
|
|
94
|
return $pane_form;
|
95
|
}
|
96
|
else {
|
97
|
$pane_form['message'] = array(
|
98
|
'#markup' => '<div>' . t('Unfortunately we could not find any suitable payment methods, and we require a payment method to complete checkout.') . '<br /><strong>' . t('Please contact us to resolve any issues with your order.') . '</strong></div>',
|
99
|
);
|
100
|
}
|
101
|
}
|
102
|
|
103
|
// Store the payment methods in the form for validation purposes.
|
104
|
$pane_form['payment_methods'] = array(
|
105
|
'#type' => 'value',
|
106
|
'#value' => $order->payment_methods,
|
107
|
);
|
108
|
|
109
|
// If at least one payment option is available...
|
110
|
if (!empty($options)) {
|
111
|
// Add a radio select widget to specify the payment method.
|
112
|
$pane_form['payment_method'] = array(
|
113
|
'#type' => 'radios',
|
114
|
'#options' => $options,
|
115
|
'#ajax' => array(
|
116
|
'callback' => 'commerce_payment_pane_checkout_form_details_refresh',
|
117
|
'wrapper' => 'payment-details',
|
118
|
),
|
119
|
);
|
120
|
|
121
|
// Find the default payment method using either the preselected value stored
|
122
|
// in the order / checkout pane or the first available method.
|
123
|
$pane_values = !empty($form_state['values'][$checkout_pane['pane_id']]) ? $form_state['values'][$checkout_pane['pane_id']] : array();
|
124
|
|
125
|
if (isset($pane_values['payment_method']) && isset($options[$pane_values['payment_method']])) {
|
126
|
$default_value = $pane_values['payment_method'];
|
127
|
}
|
128
|
elseif (isset($form_state['input']['commerce_payment']['payment_method'])) {
|
129
|
$default_value = $form_state['complete form']['commerce_payment']['payment_method']['#default_value'];
|
130
|
}
|
131
|
elseif (isset($order->data['payment_method']) && isset($options[$order->data['payment_method']])) {
|
132
|
$default_value = $order->data['payment_method'];
|
133
|
}
|
134
|
else {
|
135
|
reset($options);
|
136
|
$default_value = key($options);
|
137
|
}
|
138
|
|
139
|
// Set the default value for the payment method radios.
|
140
|
$pane_form['payment_method']['#default_value'] = $default_value;
|
141
|
|
142
|
// Add the payment method specific form elements.
|
143
|
$method_info = $order->payment_methods[$pane_form['payment_method']['#default_value']];
|
144
|
$payment_method = commerce_payment_method_load($method_info['method_id']);
|
145
|
$payment_method['settings'] = $method_info['settings'];
|
146
|
|
147
|
if ($callback = commerce_payment_method_callback($payment_method, 'submit_form')) {
|
148
|
$pane_form['payment_details'] = $callback($payment_method, $pane_values, $checkout_pane, $order);
|
149
|
}
|
150
|
else {
|
151
|
$pane_form['payment_details'] = array();
|
152
|
}
|
153
|
|
154
|
$pane_form['payment_details']['#prefix'] = '<div id="payment-details">';
|
155
|
$pane_form['payment_details']['#suffix'] = '</div>';
|
156
|
}
|
157
|
|
158
|
return $pane_form;
|
159
|
}
|
160
|
|
161
|
/**
|
162
|
* Returns the payment details element for display via AJAX.
|
163
|
*/
|
164
|
function commerce_payment_pane_checkout_form_details_refresh($form, $form_state) {
|
165
|
return $form['commerce_payment']['payment_details'];
|
166
|
}
|
167
|
|
168
|
/**
|
169
|
* Payment pane: validation callback.
|
170
|
*/
|
171
|
function commerce_payment_pane_checkout_form_validate($form, &$form_state, $checkout_pane, $order) {
|
172
|
$pane_id = $checkout_pane['pane_id'];
|
173
|
|
174
|
// Only attempt validation if we actually had payment methods on the form.
|
175
|
if (!empty($form[$pane_id]) && !empty($form_state['values'][$pane_id])) {
|
176
|
$pane_form = $form[$pane_id];
|
177
|
$pane_values = $form_state['values'][$pane_id];
|
178
|
|
179
|
// Only attempt validation if there were payment methods available.
|
180
|
if (!empty($pane_values['payment_methods'])) {
|
181
|
// If the selected payment method was changed...
|
182
|
if ($pane_values['payment_method'] != $pane_form['payment_method']['#default_value']) {
|
183
|
// And the newly selected method has a valid form callback...
|
184
|
if ($payment_method = commerce_payment_method_instance_load($pane_values['payment_method'])) {
|
185
|
if (commerce_payment_method_callback($payment_method, 'submit_form')) {
|
186
|
// Fail validation so the form is rebuilt to include the payment method
|
187
|
// specific form elements.
|
188
|
return FALSE;
|
189
|
}
|
190
|
}
|
191
|
}
|
192
|
|
193
|
// Delegate validation to the payment method callback.
|
194
|
$payment_method = commerce_payment_method_instance_load($pane_values['payment_method']);
|
195
|
|
196
|
if ($callback = commerce_payment_method_callback($payment_method, 'submit_form_validate')) {
|
197
|
// Initialize the payment details array to accommodate payment methods
|
198
|
// that don't add any additional details to the checkout pane form.
|
199
|
if (!isset($pane_values['payment_details'])) {
|
200
|
$pane_values['payment_details'] = array();
|
201
|
}
|
202
|
|
203
|
$result = $callback($payment_method, $pane_form['payment_details'], $pane_values['payment_details'], $order, array($checkout_pane['pane_id'], 'payment_details'));
|
204
|
|
205
|
// To prevent payment method validation routines from having to return TRUE
|
206
|
// explicitly, only return FALSE if it was specifically returned. Otherwise
|
207
|
// default to TRUE.
|
208
|
return $result === FALSE ? FALSE : TRUE;
|
209
|
}
|
210
|
}
|
211
|
elseif (variable_get('commerce_payment_pane_require_method', FALSE)) {
|
212
|
drupal_set_message(t('You cannot complete checkout without submitting payment. Please contact us if an error continues to prevent you from seeing valid payment methods for your order.'), 'error');
|
213
|
return FALSE;
|
214
|
}
|
215
|
}
|
216
|
else {
|
217
|
// Otherwise ensure we don't have any leftover payment method data in the
|
218
|
// order's data array.
|
219
|
unset($order->data['payment_method'], $order->data['payment_redirect_key']);
|
220
|
}
|
221
|
|
222
|
// Nothing to validate.
|
223
|
return TRUE;
|
224
|
}
|
225
|
|
226
|
/**
|
227
|
* Payment pane: submit callback.
|
228
|
*/
|
229
|
function commerce_payment_pane_checkout_form_submit($form, &$form_state, $checkout_pane, $order) {
|
230
|
// Check to make sure there are no validation issues with other form elements
|
231
|
// before executing payment method callbacks.
|
232
|
if (form_get_errors()) {
|
233
|
drupal_set_message(t('Your payment will not be processed until all errors on the page have been addressed.'), 'warning');
|
234
|
return FALSE;
|
235
|
}
|
236
|
|
237
|
$pane_id = $checkout_pane['pane_id'];
|
238
|
|
239
|
// Only submit if we actually had payment methods on the form.
|
240
|
if (!empty($form[$pane_id]) && !empty($form_state['values'][$pane_id])) {
|
241
|
$pane_form = $form[$pane_id];
|
242
|
$pane_values = $form_state['values'][$pane_id];
|
243
|
|
244
|
// Only process if there were payment methods available.
|
245
|
if ($pane_values['payment_methods']) {
|
246
|
$order->data['payment_method'] = $pane_values['payment_method'];
|
247
|
|
248
|
// If we can calculate a single order total for the order...
|
249
|
if ($balance = commerce_payment_order_balance($order)) {
|
250
|
// Delegate submit to the payment method callback.
|
251
|
$payment_method = commerce_payment_method_instance_load($pane_values['payment_method']);
|
252
|
|
253
|
if ($callback = commerce_payment_method_callback($payment_method, 'submit_form_submit')) {
|
254
|
// Initialize the payment details array to accommodate payment methods
|
255
|
// that don't add any additional details to the checkout pane form.
|
256
|
if (empty($pane_values['payment_details'])) {
|
257
|
$pane_values['payment_details'] = array();
|
258
|
}
|
259
|
|
260
|
// If payment fails, rebuild the checkout form without progressing.
|
261
|
if ($callback($payment_method, $pane_form['payment_details'], $pane_values['payment_details'], $order, $balance) === FALSE) {
|
262
|
$form_state['rebuild'] = TRUE;
|
263
|
}
|
264
|
}
|
265
|
}
|
266
|
}
|
267
|
}
|
268
|
else {
|
269
|
// If there were no payment methods on the form, check to see if the pane is
|
270
|
// configured to trigger "When an order is first paid in full" on submission
|
271
|
// for free orders.
|
272
|
$behavior = variable_get('commerce_payment_pane_no_method_behavior', COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE);
|
273
|
|
274
|
if (in_array($behavior, array(COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY_EVENT, COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE_EVENT))) {
|
275
|
// Check the balance of the order.
|
276
|
$balance = commerce_payment_order_balance($order);
|
277
|
|
278
|
if (!empty($balance) && $balance['amount'] <= 0) {
|
279
|
// Trigger the event now for free orders, simulating payment being
|
280
|
// submitted on pane submission that brings the balance to 0. Use an
|
281
|
// empty transaction, as we wouldn't typically save a transaction where
|
282
|
// a financial transaction has not actually occurred.
|
283
|
rules_invoke_all('commerce_payment_order_paid_in_full', $order, commerce_payment_transaction_new('', $order->order_id));
|
284
|
|
285
|
// Update the order's data array to indicate this just happened.
|
286
|
$order->data['commerce_payment_order_paid_in_full_invoked'] = TRUE;
|
287
|
}
|
288
|
}
|
289
|
}
|
290
|
}
|
291
|
|
292
|
/**
|
293
|
* Payment redirect pane: form callback.
|
294
|
*/
|
295
|
function commerce_payment_redirect_pane_checkout_form(&$form, &$form_state, $checkout_pane, $order) {
|
296
|
// First load the order's specified payment method instance.
|
297
|
if (!empty($order->data['payment_method'])) {
|
298
|
$payment_method = commerce_payment_method_instance_load($order->data['payment_method']);
|
299
|
}
|
300
|
else {
|
301
|
$payment_method = FALSE;
|
302
|
}
|
303
|
|
304
|
// If the payment method doesn't exist or does not require a redirect...
|
305
|
if (!$payment_method || !$payment_method['offsite']) {
|
306
|
if (!$payment_method) {
|
307
|
$log = t('Customer skipped the Payment page because no payment was required.');
|
308
|
}
|
309
|
else {
|
310
|
$log = t('Customer skipped the Payment page because payment was already submitted.');
|
311
|
}
|
312
|
|
313
|
// Advance the customer to the next step of the checkout process.
|
314
|
commerce_payment_redirect_pane_next_page($order, $log);
|
315
|
drupal_goto(commerce_checkout_order_uri($order));
|
316
|
}
|
317
|
|
318
|
// If the user came to the cancel URL...
|
319
|
if (arg(3) == 'back' && arg(4) == $order->data['payment_redirect_key']) {
|
320
|
// Perform any payment cancellation functions if necessary.
|
321
|
if ($callback = commerce_payment_method_callback($payment_method, 'redirect_form_back')) {
|
322
|
$callback($order, $payment_method);
|
323
|
}
|
324
|
|
325
|
// Send the customer to the previous step of the checkout process.
|
326
|
commerce_payment_redirect_pane_previous_page($order, t('Customer canceled payment at the payment gateway.'));
|
327
|
drupal_goto(commerce_checkout_order_uri($order));
|
328
|
}
|
329
|
|
330
|
// If the user came to the return URL...
|
331
|
if (arg(3) == 'return' && arg(4) == $order->data['payment_redirect_key']) {
|
332
|
// Check for a validate handler on return.
|
333
|
$validate_callback = commerce_payment_method_callback($payment_method, 'redirect_form_validate');
|
334
|
|
335
|
// If there is no validate handler or if there is and it isn't FALSE...
|
336
|
if (!$validate_callback || $validate_callback($order, $payment_method) !== FALSE) {
|
337
|
// Perform any submit functions if necessary.
|
338
|
if ($callback = commerce_payment_method_callback($payment_method, 'redirect_form_submit')) {
|
339
|
$callback($order, $payment_method);
|
340
|
}
|
341
|
|
342
|
// Send the customer on to the next checkout page.
|
343
|
commerce_payment_redirect_pane_next_page($order, t('Customer successfully submitted payment at the payment gateway.'));
|
344
|
drupal_goto(commerce_checkout_order_uri($order));
|
345
|
}
|
346
|
else {
|
347
|
// Otherwise display the failure message and send the customer back.
|
348
|
drupal_set_message(t('Payment failed at the payment server. Please review your information and try again.'), 'error');
|
349
|
|
350
|
commerce_payment_redirect_pane_previous_page($order, t('Customer payment submission failed at the payment gateway.'));
|
351
|
drupal_goto(commerce_checkout_order_uri($order));
|
352
|
}
|
353
|
}
|
354
|
|
355
|
// If the function to build the redirect form exists...
|
356
|
if ($callback = commerce_payment_method_callback($payment_method, 'redirect_form')) {
|
357
|
// Generate a key to use in the return URL from the redirected service if it
|
358
|
// does not already exist.
|
359
|
if (empty($order->data['payment_redirect_key'])) {
|
360
|
$order->data['payment_redirect_key'] = drupal_hash_base64(time());
|
361
|
commerce_order_save($order);
|
362
|
}
|
363
|
|
364
|
// If the payment method has the 'offsite_autoredirect' option enabled, add
|
365
|
// the redirection behavior.
|
366
|
if (!empty($payment_method['offsite_autoredirect'])) {
|
367
|
$form['#attached']['js'][] = drupal_get_path('module', 'commerce_payment') . '/commerce_payment.js';
|
368
|
$form['help']['#markup'] = '<div class="checkout-help">' . t('Please wait while you are redirected to the payment server. If nothing happens within 10 seconds, please click on the button below.') . '</div>';
|
369
|
}
|
370
|
|
371
|
// Merge the new form into the current form array, preserving the help text
|
372
|
// if it exists. We also add a wrapper so the form can be easily submitted.
|
373
|
$form += drupal_get_form($callback, $order, $payment_method);
|
374
|
|
375
|
$form['#prefix'] = '<div class="payment-redirect-form">';
|
376
|
$form['#suffix'] = '</div>';
|
377
|
}
|
378
|
else {
|
379
|
// Alert the administrator that the module does not provide a required form.
|
380
|
drupal_set_message(t('The %title payment method indicates it is offsite but does not define the necessary form to process the redirect.', array('%title' => $payment_method['title'])), 'error');
|
381
|
}
|
382
|
}
|
383
|
|
384
|
/**
|
385
|
* Utility function: return a payment redirect page for POST.
|
386
|
*
|
387
|
* @param $action
|
388
|
* The destination URL the values should be posted to.
|
389
|
* @param $values
|
390
|
* An associative array of values that will be posted to the destination URL.
|
391
|
* @return
|
392
|
* A renderable array.
|
393
|
*/
|
394
|
function commerce_payment_post_redirect_form($action, array $values = array()) {
|
395
|
$form = array(
|
396
|
'#type' => 'form',
|
397
|
'#action' => $action,
|
398
|
'#method' => 'POST',
|
399
|
'#id' => '',
|
400
|
'#attributes' => array(),
|
401
|
);
|
402
|
foreach ($values as $key => $value) {
|
403
|
$form[$value] = array(
|
404
|
'#type' => 'hidden',
|
405
|
'#name' => $key,
|
406
|
'#value' => $value,
|
407
|
'#id' => '',
|
408
|
'#attributes' => array(),
|
409
|
);
|
410
|
}
|
411
|
$form['submit'] = array(
|
412
|
'#type' => 'submit',
|
413
|
'#id' => '',
|
414
|
'#value' => t('Proceed to payment'),
|
415
|
);
|
416
|
|
417
|
return array(
|
418
|
'form' => array(
|
419
|
'#type' => 'markup',
|
420
|
'#markup' => drupal_render($form),
|
421
|
),
|
422
|
);
|
423
|
}
|