PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.8
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.8
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.8, at app/Modules/Payments/Classes/PaymentAction.php

976 lines 35.9 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 && $this->isCouponFieldVisible($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 isFieldConditionPass($field)
149 {
150 $conditionSettings = ArrayHelper::get($field, 'settings.conditional_logics', []);
151 if (
152 !$conditionSettings ||
153 !ArrayHelper::isTrue($conditionSettings, 'status')
154 ) {
155 return true;
156 }
157
158 $conditionFeed = ['conditionals' => $conditionSettings];
159 return ConditionAssesor::evaluate($conditionFeed, $this->data);
160 }
161
162 /**
163 * Visible only when the coupon field's own conditions and every ancestor
164 * container's conditions pass — so a coupon inside a hidden container is
165 * not honored.
166 */
167 public function isCouponFieldVisible($couponField)
168 {
169 if (!$this->isFieldConditionPass($couponField)) {
170 return false;
171 }
172
173 $formFields = $this->form->form_fields;
174 if (is_string($formFields)) {
175 $formFields = json_decode($formFields, true);
176 }
177 $couponName = ArrayHelper::get($couponField, 'attributes.name');
178 $ancestors = $this->getFieldAncestorContainers(ArrayHelper::get($formFields, 'fields', []), $couponName);
179
180 foreach ((array) $ancestors as $container) {
181 if (!$this->isFieldConditionPass($container)) {
182 return false;
183 }
184 }
185
186 return true;
187 }
188
189 /**
190 * Return the ancestor container fields wrapping $targetName (containers nest
191 * children under columns[].fields[]), or null if not found in this branch.
192 */
193 public function getFieldAncestorContainers($fields, $targetName, $ancestors = [])
194 {
195 foreach ($fields as $field) {
196 if (ArrayHelper::get($field, 'attributes.name') === $targetName) {
197 return $ancestors;
198 }
199 foreach (ArrayHelper::get($field, 'columns', []) as $column) {
200 $found = $this->getFieldAncestorContainers(
201 ArrayHelper::get($column, 'fields', []),
202 $targetName,
203 array_merge($ancestors, [$field])
204 );
205 if (!is_null($found)) {
206 return $found;
207 }
208 }
209 }
210
211 return null;
212 }
213
214 public function draftFormEntry()
215 {
216 // Record Payment Items
217 $subscriptionItems = $this->getSubscriptionItems();
218
219 if (count($subscriptionItems) >= 2) {
220 // We are not supporting multiple subscription items at this moment
221 wp_send_json_error([
222 'message' => __('Sorry, multiple subscription item is not supported', 'fluentform')
223 ]);
224 }
225
226 $items = $this->getOrderItems();
227
228 $existingSubmission = $this->checkForExistingSubmission();
229
230 $formSettings = PaymentHelper::getFormSettings($this->form->id, 'public');
231 $submission = $this->submissionData;
232 $submission['payment_status'] = 'pending';
233 $submission['payment_method'] = $this->selectedPaymentMethod;
234 $submission['payment_type'] = $this->getPaymentType();
235 $submission['currency'] = $formSettings['currency'];
236 $submission['response'] = json_encode($submission['response']);
237 $submission['payment_total'] = $this->getCalculatedAmount();
238 $submission = apply_filters_deprecated(
239 'fluentform_with_payment_submission_data',
240 [
241 $submission,
242 $this->form
243 ],
244 FLUENTFORM_FRAMEWORK_UPGRADE,
245 'fluentform/payment_submission_data',
246 'Use fluentform/payment_submission_data instead of fluentform_with_payment_submission_data.'
247 );
248 $submission = apply_filters('fluentform/payment_submission_data', $submission, $this->form);
249
250 if ($existingSubmission) {
251 $insertId = $existingSubmission->id;
252 Submission::where('id', $insertId)->update($submission);
253
254 // delete the existing transactions here if any
255 Transaction::where('submission_id', $insertId)->delete();
256 } else {
257 $insertId = Submission::create($submission)->id;
258 $uidHash = md5(wp_generate_uuid4() . $insertId);
259 Helper::setSubmissionMeta($insertId, '_entry_uid_hash', $uidHash, $this->form->id);
260 $intermediatePaymentHash = md5('payment_' . wp_generate_uuid4() . '_' . $insertId . '_' . $this->form->id);
261 Helper::setSubmissionMeta($insertId, '__entry_intermediate_hash', $intermediatePaymentHash, $this->form->id);
262 }
263
264 $submission['id'] = $insertId;
265 $this->setSubmissionData($submission);
266 $this->submissionId = $insertId;
267
268
269 $paymentTotal = 0;
270 if ($items) {
271 foreach ($items as $index => $item) {
272 if ($item['type'] == 'discount') {
273 $paymentTotal -= $item['line_total'];
274 } else {
275 $paymentTotal += $item['line_total'];
276 }
277 $items[$index]['submission_id'] = $insertId;
278 $items[$index]['form_id'] = $submission['form_id'];
279 }
280 }
281
282 $this->insertOrderItems($items, $existingSubmission);
283
284 $subsTotal = 0;
285 if ($subscriptionItems && $existingSubmission) {
286 Subscription::where('submission_id', $existingSubmission->id)->delete();
287 }
288
289 foreach ($subscriptionItems as $subscriptionItem) {
290 $quantity = isset($subscriptionItem['quantity']) ? $subscriptionItem['quantity'] : 1;
291 $linePrice = $subscriptionItem['recurring_amount'] * $quantity;
292 $subsTotal += intval($linePrice);
293 $subscriptionItem['submission_id'] = $insertId;
294 Subscription::create($subscriptionItem);
295 }
296
297 do_action('fluentform/notify_on_form_submit', $this->submissionId, $this->submissionData['response'], $this->form);
298
299 $totalPayable = $paymentTotal + $subsTotal;
300
301 // We should make a transaction for subscription
302
303 if ($this->selectedPaymentMethod) {
304 Helper::setSubmissionMeta($insertId, '_selected_payment_method', $this->selectedPaymentMethod);
305 do_action_deprecated(
306 'fluentform_process_payment',
307 [
308 $this->submissionId,
309 $this->submissionData,
310 $this->form,
311 $this->methodSettings,
312 !!$subscriptionItems,
313 $totalPayable
314 ],
315 FLUENTFORM_FRAMEWORK_UPGRADE,
316 'fluentform/process_payment',
317 'Use fluentform/process_payment instead of fluentform_process_payment.'
318 );
319 do_action('fluentform/process_payment', $this->submissionId, $this->submissionData, $this->form, $this->methodSettings, !!$subscriptionItems, $totalPayable);
320
321 do_action_deprecated(
322 'fluentform_process_payment_' . $this->selectedPaymentMethod,
323 [
324 $this->submissionId,
325 $this->submissionData,
326 $this->form,
327 $this->methodSettings,
328 !!$subscriptionItems,
329 $totalPayable
330 ],
331 FLUENTFORM_FRAMEWORK_UPGRADE,
332 'fluentform/process_payment_' . $this->selectedPaymentMethod,
333 'Use fluentform/process_payment_' . $this->selectedPaymentMethod . ' instead of fluentform_process_payment_' . $this->selectedPaymentMethod
334 );
335 do_action('fluentform/process_payment_' . $this->selectedPaymentMethod, $this->submissionId, $this->submissionData, $this->form, $this->methodSettings, !!$subscriptionItems, $totalPayable);
336 }
337
338 /*
339 * The following code will run only if no payment method catch and process the payment
340 * In the payment method, ideally they will send the response. But if no payment method exist then
341 * we will handle here
342 */
343 $submission = Submission::find($insertId);
344
345 $returnData = (new SubmissionHandlerService())->processSubmissionData(
346 $submission->id, $this->submissionData['response'], $this->form
347 );
348
349 wp_send_json_success($returnData, 200);
350 }
351
352 public function getOrderItems($forced = false)
353 {
354 if ($forced) {
355 $this->orderItems = [];
356 }
357
358 if ($this->orderItems) {
359 return $this->orderItems;
360 }
361
362 $paymentInputs = $this->paymentInputs;
363
364 if (!$paymentInputs && !$this->hookedOrderItems) {
365 return [];
366 }
367
368 $data = $this->submissionData['response'];
369
370 foreach ($paymentInputs as $paymentInput) {
371 $name = ArrayHelper::get($paymentInput, 'attributes.name');
372 if (!$name || !isset($data[$name])) {
373 continue;
374 }
375 $price = 0;
376 $inputType = ArrayHelper::get($paymentInput, 'attributes.type');
377
378 if (!$data[$name]) {
379 continue;
380 }
381
382 if ($inputType == 'number') {
383 $price = $data[$name];
384 } else if ($inputType == 'single') {
385 $price = ArrayHelper::get($paymentInput, 'attributes.value');
386 if (ArrayHelper::get($paymentInput, 'settings.dynamic_default_value')) {
387 $price = $data[$name];
388 }
389 } else if ($inputType == 'radio' || $inputType == 'select') {
390 $item = $this->getItemFromVariables($paymentInput, $data[$name]);
391 if ($item) {
392 $quantity = $this->getQuantity($item['parent_holder']);
393 if (!$quantity) {
394 continue;
395 }
396 $item['quantity'] = $quantity;
397 $this->pushItem($item);
398 }
399 continue;
400 } else if (ArrayHelper::get($paymentInput, 'attributes.type') == 'checkbox') {
401 $selectedItems = $data[$name];
402 foreach ($selectedItems as $selectedItem) {
403 $item = $this->getItemFromVariables($paymentInput, $selectedItem);
404 if ($item) {
405 $quantity = $this->getQuantity($item['parent_holder']);
406 if (!$quantity) {
407 continue;
408 }
409 $item['quantity'] = $quantity;
410 $this->pushItem($item);
411 }
412 }
413 continue;
414 }
415
416 if (!is_numeric($price) || !$price) {
417 continue;
418 }
419
420 $productName = ArrayHelper::get($paymentInput, 'attributes.name');
421 $quantity = $this->getQuantity($productName);
422 if (!$quantity) {
423 continue;
424 }
425
426 $this->pushItem([
427 'parent_holder' => $productName,
428 'item_name' => ArrayHelper::get($paymentInput, 'admin_label'),
429 'item_price' => $price,
430 'quantity' => $quantity
431 ]);
432 }
433
434 // We may have initial amount from the subscription
435 if ($this->hookedOrderItems) {
436 $this->orderItems = array_merge($this->orderItems, $this->hookedOrderItems);
437 }
438
439 $this->orderItems = apply_filters_deprecated(
440 'fluentform_submission_order_items',
441 [
442 $this->orderItems,
443 $this->submissionData,
444 $this->form
445 ],
446 FLUENTFORM_FRAMEWORK_UPGRADE,
447 'fluentform/submission_order_items',
448 'Use fluentform/submission_order_items instead of fluentform_submission_order_items.'
449 );
450
451 $this->orderItems = apply_filters('fluentform/submission_order_items', $this->orderItems, $this->submissionData, $this->form, $this->selectedPaymentMethod);
452
453 return $this->orderItems;
454 }
455
456 private function getQuantity($productName)
457 {
458 $quantity = 1;
459 if (!$this->quantityItems) {
460 return $quantity;
461 }
462 if (!isset($this->quantityItems[$productName])) {
463 return $quantity;
464 }
465 $inputName = $this->quantityItems[$productName];
466 $quantity = ArrayHelper::get($this->submissionData['response'], $inputName);
467 if (!$quantity) {
468 return 0;
469 }
470 return intval($quantity);
471 }
472
473 private function pushItem($data)
474 {
475 if (!$data['item_price']) {
476 return;
477 }
478 $data['item_price'] = floatval($data['item_price'] * 100);
479
480 $defaults = [
481 'type' => 'single',
482 'form_id' => $this->form->id,
483 'quantity' => !empty($data['quantity']) ? $data['quantity'] : 1,
484 'created_at' => current_time('mysql'),
485 'updated_at' => current_time('mysql')
486 ];
487
488 $item = wp_parse_args($data, $defaults);
489
490 $item['line_total'] = $item['item_price'] * $item['quantity'];
491
492 if (!$this->orderItems) {
493 $this->orderItems = [];
494 }
495
496 $this->orderItems[] = $item;
497 }
498
499 private function getItemFromVariables($item, $key)
500 {
501 $elementName = $item['element'];
502 $pricingOptions = ArrayHelper::get($item, 'settings.pricing_options');
503 $pricingOptions = apply_filters_deprecated(
504 'fluentform_payment_field_' . $elementName . '_pricing_options',
505 [
506 $pricingOptions,
507 $item,
508 $this->form
509 ],
510 FLUENTFORM_FRAMEWORK_UPGRADE,
511 'fluentform/payment_field_' . $elementName . '_pricing_options',
512 'Use fluentform/payment_field_' . $elementName . '_pricing_options instead of fluentform_payment_field_' . $elementName . '_pricing_options.'
513 );
514 $pricingOptions = apply_filters('fluentform/payment_field_' . $elementName . '_pricing_options', $pricingOptions, $item, $this->form);
515
516 $selectedOption = [];
517 foreach ($pricingOptions as $priceOption) {
518 $label = sanitize_text_field($priceOption['label']);
519 $value = sanitize_text_field($priceOption['value']);
520 if ($label == $key || $value == $key) {
521 $selectedOption = $priceOption;
522 }
523 }
524
525 if (!$selectedOption || empty($selectedOption['value']) || !is_numeric($selectedOption['value'])) {
526 return false;
527 }
528
529 return [
530 'parent_holder' => ArrayHelper::get($item, 'attributes.name'),
531 'item_name' => $selectedOption['label'],
532 'item_price' => $selectedOption['value']
533 ];
534 }
535
536 public function getCalculatedAmount()
537 {
538 $items = $this->getOrderItems();
539
540 $total = 0;
541 foreach ($items as $item) {
542 if ($item['type'] == 'discount') {
543 $total -= $item['line_total'];
544 } else {
545 $total += $item['line_total'];
546 }
547 }
548 return $total;
549 }
550
551 public function getPaymentType()
552 {
553 return count($this->getSubscriptionItems()) ? 'subscription' : 'product'; // return value product|subscription|donation
554 }
555
556 private function getCurrency()
557 {
558 if ($this->currency !== null) {
559 return $this->currency;
560 }
561 $this->currency = 'usd';
562
563 return $this->currency;
564 }
565
566 public function getSubscriptionItems()
567 {
568 if ($this->subscriptionItems) {
569 return $this->subscriptionItems;
570 }
571
572 $data = $this->submissionData['response'];
573 $subscriptionInputs = $this->subscriptionInputs;
574
575 if (!$subscriptionInputs) {
576 return [];
577 }
578
579 foreach ($subscriptionInputs as $subscriptionInput) {
580 $name = ArrayHelper::get($subscriptionInput, 'attributes.name');
581 $quantity = $this->getQuantity($name);
582
583 if (!$name || !isset($data[$name]) || $quantity === 0) {
584 continue;
585 }
586
587 $label = ArrayHelper::get($subscriptionInput, 'settings.label', $name);
588
589 $subscriptionOptions = ArrayHelper::get($subscriptionInput, 'settings.subscription_options');
590
591 $plan = $subscriptionOptions[$data[$name]];
592
593 if (!$plan) {
594 continue;
595 }
596
597 if (ArrayHelper::get($plan, 'user_input') === 'yes') {
598 $plan['subscription_amount'] = ArrayHelper::get($data, $name . '_custom_' . $data[$name]);
599 $plan['subscription_amount'] = $plan['subscription_amount'] ?: 0;
600 }
601
602 $noTrial = ArrayHelper::get($plan, 'has_trial_days') === 'no' ||
603 !ArrayHelper::get($plan, 'trial_days');
604
605 if (!$plan['subscription_amount'] && $noTrial) {
606 continue;
607 }
608
609 if (ArrayHelper::get($plan, 'bill_times') == 1 && ArrayHelper::get($plan, 'has_trial_days') != 'yes') {
610 // Since the billing times is 1 and no trial days,
611 // the subscription acts like as an one time payment.
612 // We'll convert this as a payment item.
613 $signupFee = 0;
614
615 if ($plan['has_signup_fee'] === 'yes') {
616 $signupFee = PaymentHelper::convertToCents($plan['signup_fee']);
617 }
618
619 $onetimeTotal = $signupFee + PaymentHelper::convertToCents($plan['subscription_amount']);
620
621 $this->pushItem([
622 'parent_holder' => $name,
623 'item_name' => $label,
624 'quantity' => $quantity,
625 'item_price' => $onetimeTotal,
626 'line_total' => $quantity * $onetimeTotal,
627 'created_at' => current_time('mysql'),
628 'updated_at' => current_time('mysql')
629 ]);
630 } else {
631 $billTimes = (isset($plan['bill_times'])) ? $plan['bill_times'] : 0;
632
633 // If end date is set, dynamically calculate bill_times from today
634 if (
635 ArrayHelper::get($plan, 'has_end_date') === 'yes'
636 && ($endDateStr = ArrayHelper::get($plan, 'subscription_end_date'))
637 ) {
638 $endDate = strtotime($endDateStr . ' +1 day');
639 $now = current_time('timestamp');
640 if (!$endDate || $endDate <= $now) {
641 if (ArrayHelper::get($plan, 'expire_behavior') === 'hide') {
642 continue;
643 }
644 wp_send_json([
645 'errors' => [__('This subscription plan was expired', 'fluentform')]
646 ], 423);
647 }
648 $diffDays = max(1, ceil(($endDate - $now) / 86400));
649 $intervalMap = ['day' => 1, 'week' => 7, 'month' => 30, 'year' => 365];
650 $interval = isset($intervalMap[$plan['billing_interval']]) ? $intervalMap[$plan['billing_interval']] : 30;
651 $billTimes = max(1, ceil($diffDays / $interval));
652 }
653
654 $subscription = array(
655 'element_id' => $name,
656 'item_name' => $label,
657 'form_id' => $this->form->id,
658 'plan_name' => $plan['name'],
659 'billing_interval' => $plan['billing_interval'],
660 'trial_days' => 0,
661 'recurring_amount' => PaymentHelper::convertToCents($plan['subscription_amount']),
662 'bill_times' => $billTimes,
663 'initial_amount' => 0,
664 'status' => 'pending',
665 'original_plan' => maybe_serialize($plan),
666 'created_at' => current_time('mysql'),
667 'updated_at' => current_time('mysql'),
668 );
669
670 if (ArrayHelper::get($plan, 'has_signup_fee') === 'yes' && ArrayHelper::get($plan, 'signup_fee')) {
671 $subscription['initial_amount'] = PaymentHelper::convertToCents($plan['signup_fee']);
672 }
673
674 if (ArrayHelper::get($plan, 'has_trial_days') === 'yes' && ArrayHelper::get($plan, 'trial_days')) {
675 $subscription['trial_days'] = $plan['trial_days'];
676 $dateTime = current_datetime();
677 $localtime = $dateTime->getTimestamp() + $dateTime->getOffset();
678 $expirationDate = date('Y-m-d H:i:s', $localtime + absint($plan['trial_days']) * 86400);
679 $subscription['expiration_at'] = $expirationDate;
680 }
681
682 if ($quantity > 1) {
683 $subscription['quantity'] = $quantity;
684 }
685
686 $this->subscriptionItems[] = $subscription;
687 }
688 }
689 $this->subscriptionItems = apply_filters_deprecated(
690 'fluentform_submission_subscription_items',
691 [
692 $this->subscriptionItems,
693 $this->submissionData,
694 $this->form
695 ],
696 FLUENTFORM_FRAMEWORK_UPGRADE,
697 'fluentform/submission_subscription_items',
698 'Use fluentform/submission_subscription_items instead of fluentform_submission_subscription_items.'
699 );
700 $this->subscriptionItems = apply_filters('fluentform/submission_subscription_items', $this->subscriptionItems, $this->submissionData, $this->form);
701
702 return $this->subscriptionItems;
703 }
704
705 private function checkForExistingSubmission()
706 {
707 $entryUid = ArrayHelper::get($this->submissionData, 'response.__entry_intermediate_hash');
708
709 if (!$entryUid) {
710 return false;
711 }
712
713 $meta = SubmissionMeta::where('meta_key', '__entry_intermediate_hash')
714 ->where('value', $entryUid)
715 ->where('form_id', $this->form->id)
716 ->first();
717
718 if (!$meta) {
719 return false;
720 }
721
722 $submission = Submission::find($meta->response_id);
723
724 if ($submission && ($submission->payment_status == 'failed' || $submission->payment_status == 'pending' || $submission->payment_status == 'draft')) {
725 return $submission;
726 }
727
728 return false;
729 }
730
731 private function insertOrderItems($items, $existing = false)
732 {
733 if (!$existing) {
734 foreach ($items as $item) {
735 OrderItem::create($item);
736 }
737 return true;
738 }
739
740 if (!$items && $existing) {
741 OrderItem::where('submission_id', $existing->id)->delete();
742 return true;
743 }
744
745 $exitingItems = OrderItem::where('submission_id', $existing->id)->get();
746
747 if (!$exitingItems || count($exitingItems) === 0) {
748 foreach ($items as $item) {
749 OrderItem::create($item);
750 }
751 return true;
752 }
753
754 $existingHashes = [];
755 foreach ($exitingItems as $exitingItem) {
756 $hash = md5($exitingItem->type . ':' . $exitingItem->parent_holder . ':' . $exitingItem->item_name . ':' . $exitingItem->quantity . ':' . $exitingItem->item_price);
757 $existingHashes[$exitingItem->id] = $hash;
758 }
759
760 $verifiedIds = [];
761 $newIds = [];
762 foreach ($items as $item) {
763 $hash = md5($item['type'] . ':' . $item['parent_holder'] . ':' . $item['item_name'] . ':' . $item['quantity'] . ':' . $item['item_price']);
764 if (in_array($hash, $existingHashes)) {
765 // already exist no need to add
766 $verifiedIds[] = array_search($hash, $existingHashes);
767 } else {
768 $newId = OrderItem::create($item)->id;
769 $verifiedIds[] = $newId;
770 $newIds[] = $newId;
771 }
772 }
773
774 if ($verifiedIds) {
775 OrderItem::whereNotIn('id', $verifiedIds)->delete();
776 }
777
778 return true;
779 }
780
781 private function validateSubscriptionInputs()
782 {
783 $subscriptionInputs = $this->subscriptionInputs;
784 if (!$subscriptionInputs) {
785 return;
786 }
787 $data = $this->submissionData['response'];
788
789 $discountCodes = $this->discountCodes;
790
791 foreach ($subscriptionInputs as $inputIndex => $subscriptionInput) {
792 $name = ArrayHelper::get($subscriptionInput, 'attributes.name');
793 $quantity = $this->getQuantity($name);
794
795 if (!$quantity) {
796 continue;
797 }
798
799 if (!$name || !isset($data[$name])) {
800 continue;
801 }
802
803 $subscriptionOptions = ArrayHelper::get($subscriptionInput, 'settings.subscription_options');
804
805 $plan = $subscriptionOptions[$data[$name]];
806
807 if (!$plan) {
808 continue;
809 }
810
811 if ($discountCodes) {
812
813 }
814
815 if (ArrayHelper::get($plan, 'has_trial_days') == 'yes' && ArrayHelper::get($plan, 'trial_days')) {
816 continue; // this is a valid subscription
817 }
818
819 if (ArrayHelper::get($plan, 'bill_times') != 1) {
820 continue;
821 }
822
823 // We have bill times 1 so we have to remove this and push to hooked inputs and later merged to payment inputs
824
825 if (ArrayHelper::get($plan, 'user_input') === 'yes') {
826 $plan['subscription_amount'] = ArrayHelper::get($data, $name . '_custom_' . $data[$name]);
827 $plan['subscription_amount'] = $plan['subscription_amount'] ?: 0;
828 }
829
830 $amount = PaymentHelper::convertToCents($plan['subscription_amount']);
831
832 if (ArrayHelper::get($plan, 'has_signup_fee') === 'yes' && ArrayHelper::get($plan, 'signup_fee')) {
833 $amount += PaymentHelper::convertToCents($plan['signup_fee']);
834 }
835
836 $this->hookedOrderItems[] = [
837 'type' => 'single',
838 'form_id' => $this->form->id,
839 'parent_holder' => $name,
840 'item_name' => ArrayHelper::get($subscriptionInput, 'admin_label') . ' (' . $plan['name'] . ')',
841 'item_price' => $amount,
842 'quantity' => $quantity,
843 'line_total' => $quantity * $amount,
844 'created_at' => current_time('mysql'),
845 'updated_at' => current_time('mysql'),
846 ];
847
848 unset($this->subscriptionInputs[$inputIndex]);
849 }
850
851 }
852
853 protected function applyDiscountCodes()
854 {
855 if (!$this->discountCodes) {
856 return false;
857 }
858
859 $orderItems = $this->getOrderItems(true);
860
861
862 $subTotal = array_sum(array_column($orderItems, 'line_total')) / 100;
863
864 $subscriptionItems = $this->getSubscriptionItems();
865
866 $subInitialTotal = 0;
867 foreach ($subscriptionItems as $subscriptionItem) {
868 if ($subscriptionItem['trial_days']) {
869 continue; // it's a trial
870 }
871 $subInitialTotal += $subscriptionItem['recurring_amount'] + $subscriptionItem['initial_amount'];
872 }
873
874 $grandTotal = $subTotal;
875 if ($subInitialTotal) {
876 $grandTotal += ($subInitialTotal / 100);
877 }
878
879 $fixedAmountApplied = 0; // in cents
880
881 if (Helper::hasPro() && class_exists('FluentFormPro\Payments\Classes\CouponModel')) {
882 $couponModel = new \FluentFormPro\Payments\Classes\CouponModel();
883 $this->discountCodes = $couponModel->getValidCoupons($this->discountCodes, $this->form->id, $grandTotal);
884 } else {
885 $this->discountCodes = [];
886 }
887
888 foreach ($this->discountCodes as $coupon) {
889 $discountAmount = $coupon->amount;
890 if ($coupon->coupon_type == 'percent') {
891 $discountAmount = (floatval($coupon->amount) / 100) * $subTotal;
892 } else {
893 if ($subTotal >= $discountAmount) {
894 $fixedAmountApplied += $discountAmount;
895 } else {
896 $discountAmount = $subTotal;
897 $fixedAmountApplied += $subTotal;
898 }
899 }
900
901 $this->pushItem([
902 'parent_holder' => ArrayHelper::get($this->couponField, 'attributes.name'),
903 'item_name' => $coupon->title,
904 'item_price' => $discountAmount, // this is not cent. We convert to cent at pushItem method
905 'quantity' => 1,
906 'type' => 'discount'
907 ]);
908
909 $subTotal = $subTotal - $discountAmount;
910 }
911
912
913 // let's convert to cents now as all subscriptions calculations are on cents
914 $fixedAmountApplied = intval($fixedAmountApplied * 100);
915
916 if (!$subscriptionItems) {
917 return true;
918 }
919
920 $fixedMaxTotal = 0;
921 $hasFixedDiscounts = false;
922 foreach ($this->discountCodes as $discountCode) {
923 if($discountCode->coupon_type == 'fixed') {
924 $fixedMaxTotal += intval($coupon->amount * 100);
925 $hasFixedDiscounts = true;
926 }
927 }
928
929 $fixedMaxTotal = $fixedMaxTotal - $fixedAmountApplied;
930
931 foreach ($subscriptionItems as $subIndex => $subscriptionItem) {
932 $recurringAmount = $subscriptionItem['recurring_amount'];
933 $signupFee = 0;
934 if ($subscriptionItem['initial_amount']) {
935 $signupFee = $subscriptionItem['initial_amount'];
936 }
937 // Let's process the percentile discounts first
938 foreach ($this->discountCodes as $coupon) {
939 $discountAmount = $coupon->amount;
940 if ($coupon->coupon_type == 'percent') {
941 $discountRecurringAmount = floatval((floatval($discountAmount) / 100) * $recurringAmount);
942 $recurringAmount -= $discountRecurringAmount;
943 if ($signupFee) {
944 $discountSignupDiscountAmount = floatval((floatval($discountAmount) / 100) * $signupFee);
945 $signupFee -= $discountSignupDiscountAmount;
946 }
947 }
948 }
949
950 if($hasFixedDiscounts && $fixedMaxTotal > 0) {
951 if($fixedMaxTotal >= $subInitialTotal) {
952 $recurringAmount = 0;
953 $signupFee = 0;
954 } else {
955 $recurringAmount = $recurringAmount - ($fixedMaxTotal / $subInitialTotal) * $recurringAmount;
956 if($signupFee > 0) {
957 $signupFee = $signupFee - ($fixedMaxTotal / $subInitialTotal) * $signupFee;
958 }
959 }
960 }
961
962 $subscriptionItems[$subIndex]['recurring_amount'] = intval($recurringAmount);
963 $subscriptionItems[$subIndex]['initial_amount'] = intval($signupFee);
964
965 $originalPlan = Helper::safeUnserialize($subscriptionItem['original_plan']);
966
967 $originalPlan['subscription_amount'] = round($recurringAmount / 100, 2);
968 $originalPlan['signup_fee'] = round($recurringAmount / 100, 2);
969 $subscriptionItems[$subIndex]['original_plan'] = maybe_serialize($originalPlan);
970 }
971
972 $this->subscriptionItems = $subscriptionItems;
973 return true;
974 }
975 }
976