PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.4
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.4
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Modules / Payments / Classes / PaymentAction.php

PaymentAction.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.4, at app/Modules/Payments/Classes/PaymentAction.php

910 lines 33.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Modules\Payments\Classes;
4
5 if (!defined('ABSPATH')) {
6 exit; // Exit if accessed directly.
7 }
8
9 use FluentForm\App\Helpers\Helper;
10 use FluentForm\App\Models\OrderItem;
11 use FluentForm\App\Models\Submission;
12 use FluentForm\App\Models\Subscription;
13 use FluentForm\App\Models\SubmissionMeta;
14 use FluentForm\App\Models\Transaction;
15 use FluentForm\App\Modules\Form\FormFieldsParser;
16 use FluentForm\App\Services\ConditionAssesor;
17 use FluentForm\App\Services\Form\SubmissionHandlerService;
18 use FluentForm\Framework\Helpers\ArrayHelper;
19 use FluentForm\App\Modules\Payments\PaymentHelper;
20
21 class PaymentAction
22 {
23 private $form;
24
25 private $data;
26
27 private $submissionData;
28
29 private $submissionId = null;
30
31 private $orderItems = [];
32
33 private $hookedOrderItems = [];
34
35 private $subscriptionItems = [];
36
37 private $quantityItems = [];
38
39 public $selectedPaymentMethod = '';
40
41 public $methodSettings = [];
42
43 protected $paymentInputs = null;
44
45 protected $subscriptionInputs = null;
46
47 protected $currency = null;
48
49 protected $methodField = null;
50
51 protected $discountCodes = [];
52
53 protected $couponField = [];
54
55 public function __construct($form, $insertData, $data)
56 {
57 $this->form = $form;
58 $this->data = $data;
59 $this->setSubmissionData($insertData);
60 $this->setupData();
61 }
62
63 private function setSubmissionData($insertData)
64 {
65 $insertData = (array)$insertData;
66 $insertData['response'] = json_decode($insertData['response'], true);
67 $this->submissionData = $insertData;
68 }
69
70 private function setupData()
71 {
72 $formFields = FormFieldsParser::getPaymentFields($this->form, ['admin_label', 'attributes', 'settings']);
73
74 $paymentInputElements = ['custom_payment_component', 'multi_payment_component'];
75 $quantityItems = [];
76 $paymentInputs = [];
77 $subscriptionInputs = [];
78 $paymentMethod = false;
79 $couponField = false;
80 foreach ($formFields as $fieldKey => $field) {
81 $element = ArrayHelper::get($field, 'element');
82 if (in_array($element, $paymentInputElements)) {
83 $paymentInputs[$fieldKey] = $field;
84 } else if ($element == 'item_quantity_component' || $element == 'rangeslider') {
85 if ('rangeslider' == $element && 'yes' != ArrayHelper::get($field, 'settings.enable_target_product')) {
86 continue;
87 }
88 if ($targetProductName = ArrayHelper::get($field, 'settings.target_product')) {
89 $quantityItems[$targetProductName] = ArrayHelper::get($field, 'attributes.name');
90 }
91 } else if ($element == 'payment_method') {
92 $paymentMethod = $field;
93 } else if ($element == 'payment_coupon' && Helper::hasPro()) {
94 $couponField = $field;
95 } else if ($element === 'subscription_payment_component') {
96 $subscriptionInputs[$fieldKey] = $field;
97 }
98 }
99
100 $this->paymentInputs = $paymentInputs;
101 $this->quantityItems = $quantityItems;
102 $this->subscriptionInputs = $subscriptionInputs;
103
104 if ($paymentMethod) {
105 $this->methodField = $paymentMethod;
106 if ($this->isConditionPass()) {
107 $methodName = ArrayHelper::get($paymentMethod, 'attributes.name');
108 $this->selectedPaymentMethod = ArrayHelper::get($this->data, $methodName);
109 $this->methodSettings = ArrayHelper::get($paymentMethod, 'settings.payment_methods.' . $this->selectedPaymentMethod);
110 }
111 }
112
113 if ($couponField) {
114 $couponCodes = ArrayHelper::get($this->data, '__ff_all_applied_coupons', '');
115 if ($couponCodes) {
116 $couponCodes = \json_decode($couponCodes, true);
117 if ($couponCodes && class_exists('FluentFormPro\Payments\Classes\CouponModel')) {
118 $couponCodes = array_unique($couponCodes);
119 $this->discountCodes = (new \FluentFormPro\Payments\Classes\CouponModel())->getCouponsByCodes($couponCodes);
120 $this->couponField = $couponField;
121 }
122 }
123 }
124
125 if ($this->subscriptionInputs) {
126 // Maybe we have subscription items with bill times = 1
127 // Or if we have discount codes then we have to apply the discount codes
128 $this->validateSubscriptionInputs();
129 }
130
131 $this->applyDiscountCodes();
132 }
133
134 public function isConditionPass()
135 {
136 $conditionSettings = ArrayHelper::get($this->methodField, 'settings.conditional_logics', []);
137 if (
138 !$conditionSettings ||
139 !ArrayHelper::isTrue($conditionSettings, 'status')
140 ) {
141 return true;
142 }
143
144 $conditionFeed = ['conditionals' => $conditionSettings];
145 return ConditionAssesor::evaluate($conditionFeed, $this->data);
146 }
147
148 public function draftFormEntry()
149 {
150 // Record Payment Items
151 $subscriptionItems = $this->getSubscriptionItems();
152
153 if (count($subscriptionItems) >= 2) {
154 // We are not supporting multiple subscription items at this moment
155 wp_send_json_error([
156 'message' => __('Sorry, multiple subscription item is not supported', 'fluentform')
157 ]);
158 }
159
160 $items = $this->getOrderItems();
161
162 $existingSubmission = $this->checkForExistingSubmission();
163
164 $formSettings = PaymentHelper::getFormSettings($this->form->id, 'public');
165 $submission = $this->submissionData;
166 $submission['payment_status'] = 'pending';
167 $submission['payment_method'] = $this->selectedPaymentMethod;
168 $submission['payment_type'] = $this->getPaymentType();
169 $submission['currency'] = $formSettings['currency'];
170 $submission['response'] = json_encode($submission['response']);
171 $submission['payment_total'] = $this->getCalculatedAmount();
172 $submission = apply_filters_deprecated(
173 'fluentform_with_payment_submission_data',
174 [
175 $submission,
176 $this->form
177 ],
178 FLUENTFORM_FRAMEWORK_UPGRADE,
179 'fluentform/payment_submission_data',
180 'Use fluentform/payment_submission_data instead of fluentform_with_payment_submission_data.'
181 );
182 $submission = apply_filters('fluentform/payment_submission_data', $submission, $this->form);
183
184 if ($existingSubmission) {
185 $insertId = $existingSubmission->id;
186 Submission::where('id', $insertId)->update($submission);
187
188 // delete the existing transactions here if any
189 Transaction::where('submission_id', $insertId)->delete();
190 } else {
191 $insertId = Submission::create($submission)->id;
192 $uidHash = md5(wp_generate_uuid4() . $insertId);
193 Helper::setSubmissionMeta($insertId, '_entry_uid_hash', $uidHash, $this->form->id);
194 $intermediatePaymentHash = md5('payment_' . wp_generate_uuid4() . '_' . $insertId . '_' . $this->form->id);
195 Helper::setSubmissionMeta($insertId, '__entry_intermediate_hash', $intermediatePaymentHash, $this->form->id);
196 }
197
198 $submission['id'] = $insertId;
199 $this->setSubmissionData($submission);
200 $this->submissionId = $insertId;
201
202
203 $paymentTotal = 0;
204 if ($items) {
205 foreach ($items as $index => $item) {
206 if ($item['type'] == 'discount') {
207 $paymentTotal -= $item['line_total'];
208 } else {
209 $paymentTotal += $item['line_total'];
210 }
211 $items[$index]['submission_id'] = $insertId;
212 $items[$index]['form_id'] = $submission['form_id'];
213 }
214 }
215
216 $this->insertOrderItems($items, $existingSubmission);
217
218 $subsTotal = 0;
219 if ($subscriptionItems && $existingSubmission) {
220 Subscription::where('submission_id', $existingSubmission->id)->delete();
221 }
222
223 foreach ($subscriptionItems as $subscriptionItem) {
224 $quantity = isset($subscriptionItem['quantity']) ? $subscriptionItem['quantity'] : 1;
225 $linePrice = $subscriptionItem['recurring_amount'] * $quantity;
226 $subsTotal += intval($linePrice);
227 $subscriptionItem['submission_id'] = $insertId;
228 Subscription::create($subscriptionItem);
229 }
230
231 do_action('fluentform/notify_on_form_submit', $this->submissionId, $this->submissionData['response'], $this->form);
232
233 $totalPayable = $paymentTotal + $subsTotal;
234
235 // We should make a transaction for subscription
236
237 if ($this->selectedPaymentMethod) {
238 Helper::setSubmissionMeta($insertId, '_selected_payment_method', $this->selectedPaymentMethod);
239 do_action_deprecated(
240 'fluentform_process_payment',
241 [
242 $this->submissionId,
243 $this->submissionData,
244 $this->form,
245 $this->methodSettings,
246 !!$subscriptionItems,
247 $totalPayable
248 ],
249 FLUENTFORM_FRAMEWORK_UPGRADE,
250 'fluentform/process_payment',
251 'Use fluentform/process_payment instead of fluentform_process_payment.'
252 );
253 do_action('fluentform/process_payment', $this->submissionId, $this->submissionData, $this->form, $this->methodSettings, !!$subscriptionItems, $totalPayable);
254
255 do_action_deprecated(
256 'fluentform_process_payment_' . $this->selectedPaymentMethod,
257 [
258 $this->submissionId,
259 $this->submissionData,
260 $this->form,
261 $this->methodSettings,
262 !!$subscriptionItems,
263 $totalPayable
264 ],
265 FLUENTFORM_FRAMEWORK_UPGRADE,
266 'fluentform/process_payment_' . $this->selectedPaymentMethod,
267 'Use fluentform/process_payment_' . $this->selectedPaymentMethod . ' instead of fluentform_process_payment_' . $this->selectedPaymentMethod
268 );
269 do_action('fluentform/process_payment_' . $this->selectedPaymentMethod, $this->submissionId, $this->submissionData, $this->form, $this->methodSettings, !!$subscriptionItems, $totalPayable);
270 }
271
272 /*
273 * The following code will run only if no payment method catch and process the payment
274 * In the payment method, ideally they will send the response. But if no payment method exist then
275 * we will handle here
276 */
277 $submission = Submission::find($insertId);
278
279 $returnData = (new SubmissionHandlerService())->processSubmissionData(
280 $submission->id, $this->submissionData['response'], $this->form
281 );
282
283 wp_send_json_success($returnData, 200);
284 }
285
286 public function getOrderItems($forced = false)
287 {
288 if ($forced) {
289 $this->orderItems = [];
290 }
291
292 if ($this->orderItems) {
293 return $this->orderItems;
294 }
295
296 $paymentInputs = $this->paymentInputs;
297
298 if (!$paymentInputs && !$this->hookedOrderItems) {
299 return [];
300 }
301
302 $data = $this->submissionData['response'];
303
304 foreach ($paymentInputs as $paymentInput) {
305 $name = ArrayHelper::get($paymentInput, 'attributes.name');
306 if (!$name || !isset($data[$name])) {
307 continue;
308 }
309 $price = 0;
310 $inputType = ArrayHelper::get($paymentInput, 'attributes.type');
311
312 if (!$data[$name]) {
313 continue;
314 }
315
316 if ($inputType == 'number') {
317 $price = $data[$name];
318 } else if ($inputType == 'single') {
319 $price = ArrayHelper::get($paymentInput, 'attributes.value');
320 if (ArrayHelper::get($paymentInput, 'settings.dynamic_default_value')) {
321 $price = $data[$name];
322 }
323 } else if ($inputType == 'radio' || $inputType == 'select') {
324 $item = $this->getItemFromVariables($paymentInput, $data[$name]);
325 if ($item) {
326 $quantity = $this->getQuantity($item['parent_holder']);
327 if (!$quantity) {
328 continue;
329 }
330 $item['quantity'] = $quantity;
331 $this->pushItem($item);
332 }
333 continue;
334 } else if (ArrayHelper::get($paymentInput, 'attributes.type') == 'checkbox') {
335 $selectedItems = $data[$name];
336 foreach ($selectedItems as $selectedItem) {
337 $item = $this->getItemFromVariables($paymentInput, $selectedItem);
338 if ($item) {
339 $quantity = $this->getQuantity($item['parent_holder']);
340 if (!$quantity) {
341 continue;
342 }
343 $item['quantity'] = $quantity;
344 $this->pushItem($item);
345 }
346 }
347 continue;
348 }
349
350 if (!is_numeric($price) || !$price) {
351 continue;
352 }
353
354 $productName = ArrayHelper::get($paymentInput, 'attributes.name');
355 $quantity = $this->getQuantity($productName);
356 if (!$quantity) {
357 continue;
358 }
359
360 $this->pushItem([
361 'parent_holder' => $productName,
362 'item_name' => ArrayHelper::get($paymentInput, 'admin_label'),
363 'item_price' => $price,
364 'quantity' => $quantity
365 ]);
366 }
367
368 // We may have initial amount from the subscription
369 if ($this->hookedOrderItems) {
370 $this->orderItems = array_merge($this->orderItems, $this->hookedOrderItems);
371 }
372
373 $this->orderItems = apply_filters_deprecated(
374 'fluentform_submission_order_items',
375 [
376 $this->orderItems,
377 $this->submissionData,
378 $this->form
379 ],
380 FLUENTFORM_FRAMEWORK_UPGRADE,
381 'fluentform/submission_order_items',
382 'Use fluentform/submission_order_items instead of fluentform_submission_order_items.'
383 );
384
385 $this->orderItems = apply_filters('fluentform/submission_order_items', $this->orderItems, $this->submissionData, $this->form, $this->selectedPaymentMethod);
386
387 return $this->orderItems;
388 }
389
390 private function getQuantity($productName)
391 {
392 $quantity = 1;
393 if (!$this->quantityItems) {
394 return $quantity;
395 }
396 if (!isset($this->quantityItems[$productName])) {
397 return $quantity;
398 }
399 $inputName = $this->quantityItems[$productName];
400 $quantity = ArrayHelper::get($this->submissionData['response'], $inputName);
401 if (!$quantity) {
402 return 0;
403 }
404 return intval($quantity);
405 }
406
407 private function pushItem($data)
408 {
409 if (!$data['item_price']) {
410 return;
411 }
412 $data['item_price'] = floatval($data['item_price'] * 100);
413
414 $defaults = [
415 'type' => 'single',
416 'form_id' => $this->form->id,
417 'quantity' => !empty($data['quantity']) ? $data['quantity'] : 1,
418 'created_at' => current_time('mysql'),
419 'updated_at' => current_time('mysql')
420 ];
421
422 $item = wp_parse_args($data, $defaults);
423
424 $item['line_total'] = $item['item_price'] * $item['quantity'];
425
426 if (!$this->orderItems) {
427 $this->orderItems = [];
428 }
429
430 $this->orderItems[] = $item;
431 }
432
433 private function getItemFromVariables($item, $key)
434 {
435 $elementName = $item['element'];
436 $pricingOptions = ArrayHelper::get($item, 'settings.pricing_options');
437 $pricingOptions = apply_filters_deprecated(
438 'fluentform_payment_field_' . $elementName . '_pricing_options',
439 [
440 $pricingOptions,
441 $item,
442 $this->form
443 ],
444 FLUENTFORM_FRAMEWORK_UPGRADE,
445 'fluentform/payment_field_' . $elementName . '_pricing_options',
446 'Use fluentform/payment_field_' . $elementName . '_pricing_options instead of fluentform_payment_field_' . $elementName . '_pricing_options.'
447 );
448 $pricingOptions = apply_filters('fluentform/payment_field_' . $elementName . '_pricing_options', $pricingOptions, $item, $this->form);
449
450 $selectedOption = [];
451 foreach ($pricingOptions as $priceOption) {
452 $label = sanitize_text_field($priceOption['label']);
453 $value = sanitize_text_field($priceOption['value']);
454 if ($label == $key || $value == $key) {
455 $selectedOption = $priceOption;
456 }
457 }
458
459 if (!$selectedOption || empty($selectedOption['value']) || !is_numeric($selectedOption['value'])) {
460 return false;
461 }
462
463 return [
464 'parent_holder' => ArrayHelper::get($item, 'attributes.name'),
465 'item_name' => $selectedOption['label'],
466 'item_price' => $selectedOption['value']
467 ];
468 }
469
470 public function getCalculatedAmount()
471 {
472 $items = $this->getOrderItems();
473
474 $total = 0;
475 foreach ($items as $item) {
476 if ($item['type'] == 'discount') {
477 $total -= $item['line_total'];
478 } else {
479 $total += $item['line_total'];
480 }
481 }
482 return $total;
483 }
484
485 public function getPaymentType()
486 {
487 return count($this->getSubscriptionItems()) ? 'subscription' : 'product'; // return value product|subscription|donation
488 }
489
490 private function getCurrency()
491 {
492 if ($this->currency !== null) {
493 return $this->currency;
494 }
495 $this->currency = 'usd';
496
497 return $this->currency;
498 }
499
500 public function getSubscriptionItems()
501 {
502 if ($this->subscriptionItems) {
503 return $this->subscriptionItems;
504 }
505
506 $data = $this->submissionData['response'];
507 $subscriptionInputs = $this->subscriptionInputs;
508
509 if (!$subscriptionInputs) {
510 return [];
511 }
512
513 foreach ($subscriptionInputs as $subscriptionInput) {
514 $name = ArrayHelper::get($subscriptionInput, 'attributes.name');
515 $quantity = $this->getQuantity($name);
516
517 if (!$name || !isset($data[$name]) || $quantity === 0) {
518 continue;
519 }
520
521 $label = ArrayHelper::get($subscriptionInput, 'settings.label', $name);
522
523 $subscriptionOptions = ArrayHelper::get($subscriptionInput, 'settings.subscription_options');
524
525 $plan = $subscriptionOptions[$data[$name]];
526
527 if (!$plan) {
528 continue;
529 }
530
531 if (ArrayHelper::get($plan, 'user_input') === 'yes') {
532 $plan['subscription_amount'] = ArrayHelper::get($data, $name . '_custom_' . $data[$name]);
533 $plan['subscription_amount'] = $plan['subscription_amount'] ?: 0;
534 }
535
536 $noTrial = ArrayHelper::get($plan, 'has_trial_days') === 'no' ||
537 !ArrayHelper::get($plan, 'trial_days');
538
539 if (!$plan['subscription_amount'] && $noTrial) {
540 continue;
541 }
542
543 if (ArrayHelper::get($plan, 'bill_times') == 1 && ArrayHelper::get($plan, 'has_trial_days') != 'yes') {
544 // Since the billing times is 1 and no trial days,
545 // the subscription acts like as an one time payment.
546 // We'll convert this as a payment item.
547 $signupFee = 0;
548
549 if ($plan['has_signup_fee'] === 'yes') {
550 $signupFee = PaymentHelper::convertToCents($plan['signup_fee']);
551 }
552
553 $onetimeTotal = $signupFee + PaymentHelper::convertToCents($plan['subscription_amount']);
554
555 $this->pushItem([
556 'parent_holder' => $name,
557 'item_name' => $label,
558 'quantity' => $quantity,
559 'item_price' => $onetimeTotal,
560 'line_total' => $quantity * $onetimeTotal,
561 'created_at' => current_time('mysql'),
562 'updated_at' => current_time('mysql')
563 ]);
564 } else {
565 $billTimes = (isset($plan['bill_times'])) ? $plan['bill_times'] : 0;
566
567 // If end date is set, dynamically calculate bill_times from today
568 if (
569 ArrayHelper::get($plan, 'has_end_date') === 'yes'
570 && ($endDateStr = ArrayHelper::get($plan, 'subscription_end_date'))
571 ) {
572 $endDate = strtotime($endDateStr . ' +1 day');
573 $now = current_time('timestamp');
574 if (!$endDate || $endDate <= $now) {
575 if (ArrayHelper::get($plan, 'expire_behavior') === 'hide') {
576 continue;
577 }
578 wp_send_json([
579 'errors' => [__('This subscription plan was expired', 'fluentform')]
580 ], 423);
581 }
582 $diffDays = max(1, ceil(($endDate - $now) / 86400));
583 $intervalMap = ['day' => 1, 'week' => 7, 'month' => 30, 'year' => 365];
584 $interval = isset($intervalMap[$plan['billing_interval']]) ? $intervalMap[$plan['billing_interval']] : 30;
585 $billTimes = max(1, ceil($diffDays / $interval));
586 }
587
588 $subscription = array(
589 'element_id' => $name,
590 'item_name' => $label,
591 'form_id' => $this->form->id,
592 'plan_name' => $plan['name'],
593 'billing_interval' => $plan['billing_interval'],
594 'trial_days' => 0,
595 'recurring_amount' => PaymentHelper::convertToCents($plan['subscription_amount']),
596 'bill_times' => $billTimes,
597 'initial_amount' => 0,
598 'status' => 'pending',
599 'original_plan' => maybe_serialize($plan),
600 'created_at' => current_time('mysql'),
601 'updated_at' => current_time('mysql'),
602 );
603
604 if (ArrayHelper::get($plan, 'has_signup_fee') === 'yes' && ArrayHelper::get($plan, 'signup_fee')) {
605 $subscription['initial_amount'] = PaymentHelper::convertToCents($plan['signup_fee']);
606 }
607
608 if (ArrayHelper::get($plan, 'has_trial_days') === 'yes' && ArrayHelper::get($plan, 'trial_days')) {
609 $subscription['trial_days'] = $plan['trial_days'];
610 $dateTime = current_datetime();
611 $localtime = $dateTime->getTimestamp() + $dateTime->getOffset();
612 $expirationDate = date('Y-m-d H:i:s', $localtime + absint($plan['trial_days']) * 86400);
613 $subscription['expiration_at'] = $expirationDate;
614 }
615
616 if ($quantity > 1) {
617 $subscription['quantity'] = $quantity;
618 }
619
620 $this->subscriptionItems[] = $subscription;
621 }
622 }
623 $this->subscriptionItems = apply_filters_deprecated(
624 'fluentform_submission_subscription_items',
625 [
626 $this->subscriptionItems,
627 $this->submissionData,
628 $this->form
629 ],
630 FLUENTFORM_FRAMEWORK_UPGRADE,
631 'fluentform/submission_subscription_items',
632 'Use fluentform/submission_subscription_items instead of fluentform_submission_subscription_items.'
633 );
634 $this->subscriptionItems = apply_filters('fluentform/submission_subscription_items', $this->subscriptionItems, $this->submissionData, $this->form);
635
636 return $this->subscriptionItems;
637 }
638
639 private function checkForExistingSubmission()
640 {
641 $entryUid = ArrayHelper::get($this->submissionData, 'response.__entry_intermediate_hash');
642
643 if (!$entryUid) {
644 return false;
645 }
646
647 $meta = SubmissionMeta::where('meta_key', '__entry_intermediate_hash')
648 ->where('value', $entryUid)
649 ->where('form_id', $this->form->id)
650 ->first();
651
652 if (!$meta) {
653 return false;
654 }
655
656 $submission = Submission::find($meta->response_id);
657
658 if ($submission && ($submission->payment_status == 'failed' || $submission->payment_status == 'pending' || $submission->payment_status == 'draft')) {
659 return $submission;
660 }
661
662 return false;
663 }
664
665 private function insertOrderItems($items, $existing = false)
666 {
667 if (!$existing) {
668 foreach ($items as $item) {
669 OrderItem::create($item);
670 }
671 return true;
672 }
673
674 if (!$items && $existing) {
675 OrderItem::where('submission_id', $existing->id)->delete();
676 return true;
677 }
678
679 $exitingItems = OrderItem::where('submission_id', $existing->id)->get();
680
681 if (!$exitingItems || count($exitingItems) === 0) {
682 foreach ($items as $item) {
683 OrderItem::create($item);
684 }
685 return true;
686 }
687
688 $existingHashes = [];
689 foreach ($exitingItems as $exitingItem) {
690 $hash = md5($exitingItem->type . ':' . $exitingItem->parent_holder . ':' . $exitingItem->item_name . ':' . $exitingItem->quantity . ':' . $exitingItem->item_price);
691 $existingHashes[$exitingItem->id] = $hash;
692 }
693
694 $verifiedIds = [];
695 $newIds = [];
696 foreach ($items as $item) {
697 $hash = md5($item['type'] . ':' . $item['parent_holder'] . ':' . $item['item_name'] . ':' . $item['quantity'] . ':' . $item['item_price']);
698 if (in_array($hash, $existingHashes)) {
699 // already exist no need to add
700 $verifiedIds[] = array_search($hash, $existingHashes);
701 } else {
702 $newId = OrderItem::create($item)->id;
703 $verifiedIds[] = $newId;
704 $newIds[] = $newId;
705 }
706 }
707
708 if ($verifiedIds) {
709 OrderItem::whereNotIn('id', $verifiedIds)->delete();
710 }
711
712 return true;
713 }
714
715 private function validateSubscriptionInputs()
716 {
717 $subscriptionInputs = $this->subscriptionInputs;
718 if (!$subscriptionInputs) {
719 return;
720 }
721 $data = $this->submissionData['response'];
722
723 $discountCodes = $this->discountCodes;
724
725 foreach ($subscriptionInputs as $inputIndex => $subscriptionInput) {
726 $name = ArrayHelper::get($subscriptionInput, 'attributes.name');
727 $quantity = $this->getQuantity($name);
728
729 if (!$quantity) {
730 continue;
731 }
732
733 if (!$name || !isset($data[$name])) {
734 continue;
735 }
736
737 $subscriptionOptions = ArrayHelper::get($subscriptionInput, 'settings.subscription_options');
738
739 $plan = $subscriptionOptions[$data[$name]];
740
741 if (!$plan) {
742 continue;
743 }
744
745 if ($discountCodes) {
746
747 }
748
749 if (ArrayHelper::get($plan, 'has_trial_days') == 'yes' && ArrayHelper::get($plan, 'trial_days')) {
750 continue; // this is a valid subscription
751 }
752
753 if (ArrayHelper::get($plan, 'bill_times') != 1) {
754 continue;
755 }
756
757 // We have bill times 1 so we have to remove this and push to hooked inputs and later merged to payment inputs
758
759 if (ArrayHelper::get($plan, 'user_input') === 'yes') {
760 $plan['subscription_amount'] = ArrayHelper::get($data, $name . '_custom_' . $data[$name]);
761 $plan['subscription_amount'] = $plan['subscription_amount'] ?: 0;
762 }
763
764 $amount = PaymentHelper::convertToCents($plan['subscription_amount']);
765
766 if (ArrayHelper::get($plan, 'has_signup_fee') === 'yes' && ArrayHelper::get($plan, 'signup_fee')) {
767 $amount += PaymentHelper::convertToCents($plan['signup_fee']);
768 }
769
770 $this->hookedOrderItems[] = [
771 'type' => 'single',
772 'form_id' => $this->form->id,
773 'parent_holder' => $name,
774 'item_name' => ArrayHelper::get($subscriptionInput, 'admin_label') . ' (' . $plan['name'] . ')',
775 'item_price' => $amount,
776 'quantity' => $quantity,
777 'line_total' => $quantity * $amount,
778 'created_at' => current_time('mysql'),
779 'updated_at' => current_time('mysql'),
780 ];
781
782 unset($this->subscriptionInputs[$inputIndex]);
783 }
784
785 }
786
787 protected function applyDiscountCodes()
788 {
789 if (!$this->discountCodes) {
790 return false;
791 }
792
793 $orderItems = $this->getOrderItems(true);
794
795
796 $subTotal = array_sum(array_column($orderItems, 'line_total')) / 100;
797
798 $subscriptionItems = $this->getSubscriptionItems();
799
800 $subInitialTotal = 0;
801 foreach ($subscriptionItems as $subscriptionItem) {
802 if ($subscriptionItem['trial_days']) {
803 continue; // it's a trial
804 }
805 $subInitialTotal += $subscriptionItem['recurring_amount'] + $subscriptionItem['initial_amount'];
806 }
807
808 $grandTotal = $subTotal;
809 if ($subInitialTotal) {
810 $grandTotal += ($subInitialTotal / 100);
811 }
812
813 $fixedAmountApplied = 0; // in cents
814
815 if (Helper::hasPro() && class_exists('FluentFormPro\Payments\Classes\CouponModel')) {
816 $couponModel = new \FluentFormPro\Payments\Classes\CouponModel();
817 $this->discountCodes = $couponModel->getValidCoupons($this->discountCodes, $this->form->id, $grandTotal);
818 } else {
819 $this->discountCodes = [];
820 }
821
822 foreach ($this->discountCodes as $coupon) {
823 $discountAmount = $coupon->amount;
824 if ($coupon->coupon_type == 'percent') {
825 $discountAmount = (floatval($coupon->amount) / 100) * $subTotal;
826 } else {
827 if ($subTotal >= $discountAmount) {
828 $fixedAmountApplied += $discountAmount;
829 } else {
830 $discountAmount = $subTotal;
831 $fixedAmountApplied += $subTotal;
832 }
833 }
834
835 $this->pushItem([
836 'parent_holder' => ArrayHelper::get($this->couponField, 'attributes.name'),
837 'item_name' => $coupon->title,
838 'item_price' => $discountAmount, // this is not cent. We convert to cent at pushItem method
839 'quantity' => 1,
840 'type' => 'discount'
841 ]);
842
843 $subTotal = $subTotal - $discountAmount;
844 }
845
846
847 // let's convert to cents now as all subscriptions calculations are on cents
848 $fixedAmountApplied = intval($fixedAmountApplied * 100);
849
850 if (!$subscriptionItems) {
851 return true;
852 }
853
854 $fixedMaxTotal = 0;
855 $hasFixedDiscounts = false;
856 foreach ($this->discountCodes as $discountCode) {
857 if($discountCode->coupon_type == 'fixed') {
858 $fixedMaxTotal += intval($coupon->amount * 100);
859 $hasFixedDiscounts = true;
860 }
861 }
862
863 $fixedMaxTotal = $fixedMaxTotal - $fixedAmountApplied;
864
865 foreach ($subscriptionItems as $subIndex => $subscriptionItem) {
866 $recurringAmount = $subscriptionItem['recurring_amount'];
867 $signupFee = 0;
868 if ($subscriptionItem['initial_amount']) {
869 $signupFee = $subscriptionItem['initial_amount'];
870 }
871 // Let's process the percentile discounts first
872 foreach ($this->discountCodes as $coupon) {
873 $discountAmount = $coupon->amount;
874 if ($coupon->coupon_type == 'percent') {
875 $discountRecurringAmount = floatval((floatval($discountAmount) / 100) * $recurringAmount);
876 $recurringAmount -= $discountRecurringAmount;
877 if ($signupFee) {
878 $discountSignupDiscountAmount = floatval((floatval($discountAmount) / 100) * $signupFee);
879 $signupFee -= $discountSignupDiscountAmount;
880 }
881 }
882 }
883
884 if($hasFixedDiscounts && $fixedMaxTotal > 0) {
885 if($fixedMaxTotal >= $subInitialTotal) {
886 $recurringAmount = 0;
887 $signupFee = 0;
888 } else {
889 $recurringAmount = $recurringAmount - ($fixedMaxTotal / $subInitialTotal) * $recurringAmount;
890 if($signupFee > 0) {
891 $signupFee = $signupFee - ($fixedMaxTotal / $subInitialTotal) * $signupFee;
892 }
893 }
894 }
895
896 $subscriptionItems[$subIndex]['recurring_amount'] = intval($recurringAmount);
897 $subscriptionItems[$subIndex]['initial_amount'] = intval($signupFee);
898
899 $originalPlan = Helper::safeUnserialize($subscriptionItem['original_plan']);
900
901 $originalPlan['subscription_amount'] = round($recurringAmount / 100, 2);
902 $originalPlan['signup_fee'] = round($recurringAmount / 100, 2);
903 $subscriptionItems[$subIndex]['original_plan'] = maybe_serialize($originalPlan);
904 }
905
906 $this->subscriptionItems = $subscriptionItems;
907 return true;
908 }
909 }
910