PluginProbe
The Innovative Form Builder – IvyForms / 1.5
The Innovative Form Builder – IvyForms v1.5
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / src / Services / Payment / PaymentAmountCalculator.php

PaymentAmountCalculator.php in The Innovative Form Builder – IvyForms 1.5, at backend/src/Services/Payment/PaymentAmountCalculator.php

171 lines 5.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace IvyForms\Services\Payment;
4
5 /**
6 * Server-side payment total calculation from product/quantity fields.
7 */
8 class PaymentAmountCalculator
9 {
10 private ProductSelectionSubtotalCalculator $productSubtotalCalculator;
11
12 public function __construct(?ProductSelectionSubtotalCalculator $productSubtotalCalculator = null)
13 {
14 $this->productSubtotalCalculator = $productSubtotalCalculator ?? new ProductSelectionSubtotalCalculator();
15 }
16
17 /**
18 * @param array<int, array<string, mixed>> $formFields
19 * @param array<string, mixed> $submissionData
20 */
21 public function calculateTotal(array $formFields, array $submissionData, string $currency = 'USD'): float
22 {
23 return MoneyMinorUnits::fromMinor(
24 $this->calculateTotalMinor($formFields, $submissionData, $currency),
25 $currency
26 );
27 }
28
29 /**
30 * Sum line totals in integer minor units so JPY / rounding cannot drift.
31 *
32 * @param array<int, array<string, mixed>> $formFields
33 * @param array<string, mixed> $submissionData
34 */
35 public function calculateTotalMinor(
36 array $formFields,
37 array $submissionData,
38 string $currency = 'USD'
39 ): int {
40 $lines = $this->buildPaymentCartLines($formFields, $submissionData, $currency);
41 $total = 0;
42
43 foreach ($lines as $line) {
44 $total += (int) $line['lineTotalMinor'];
45 }
46
47 return $total;
48 }
49
50 /**
51 * @param array<int, array<string, mixed>> $formFields
52 * @param array<string, mixed> $submissionData
53 * @return array<int, array<string, mixed>>
54 */
55 public function buildPaymentCartLines(
56 array $formFields,
57 array $submissionData,
58 string $currency = 'USD'
59 ): array {
60 $products = [];
61 $quantities = [];
62
63 foreach ($formFields as $field) {
64 $type = (string) ($field['type'] ?? '');
65 if ($type === 'product') {
66 $products[] = $field;
67 } elseif ($type === 'quantity') {
68 $quantities[] = $field;
69 }
70 }
71
72 usort(
73 $products,
74 static fn(array $left, array $right): int =>
75 ((int) ($left['fieldIndex'] ?? 0)) <=> ((int) ($right['fieldIndex'] ?? 0))
76 );
77 usort(
78 $quantities,
79 static fn(array $left, array $right): int =>
80 ((int) ($left['fieldIndex'] ?? 0)) <=> ((int) ($right['fieldIndex'] ?? 0))
81 );
82
83 $usedQuantityIndexes = [];
84 $lines = [];
85
86 foreach ($products as $position => $productField) {
87 $productIndex = (int) ($productField['fieldIndex'] ?? 0);
88 $productKey = 'product_' . $productIndex;
89 $rawProduct = $submissionData[$productKey] ?? null;
90
91 $quantityField = $this->findLinkedQuantityField(
92 $productIndex,
93 $quantities,
94 $usedQuantityIndexes,
95 $position
96 );
97
98 if ($quantityField !== null) {
99 $usedQuantityIndexes[] = (int) ($quantityField['fieldIndex'] ?? 0);
100 }
101
102 $unitSubtotal = $this->productSubtotalCalculator->compute($productField, $rawProduct);
103 $quantity = $quantityField !== null
104 ? MoneyParser::parseQuantity(
105 $submissionData['quantity_' . (int) ($quantityField['fieldIndex'] ?? 0)] ?? null
106 )
107 : 1;
108
109 $safeQty = $quantity > 0 ? $quantity : 0;
110 $lineTotalMinor = MoneyMinorUnits::toMinor($unitSubtotal, $currency) * $safeQty;
111 $lines[] = [
112 'productFieldIndex' => $productIndex,
113 'lineTotalMinor' => $lineTotalMinor,
114 'lineTotal' => MoneyMinorUnits::fromMinor($lineTotalMinor, $currency),
115 ];
116 }
117
118 return $lines;
119 }
120
121 /**
122 * @param array<int, array<string, mixed>> $quantities
123 * @param array<int, int> $usedQuantityIndexes
124 * @return array<string, mixed>|null
125 */
126 private function findLinkedQuantityField(
127 int $productIndex,
128 array $quantities,
129 array $usedQuantityIndexes,
130 int $position
131 ): ?array {
132 foreach ($quantities as $quantityField) {
133 $qIndex = (int) ($quantityField['fieldIndex'] ?? 0);
134 if (in_array($qIndex, $usedQuantityIndexes, true)) {
135 continue;
136 }
137
138 $config = $this->parseQuantityConfig($quantityField);
139 if (($config['linkedProductFieldIndex'] ?? null) === $productIndex) {
140 return $quantityField;
141 }
142 }
143
144 $unused = array_values(
145 array_filter(
146 $quantities,
147 static fn(array $qtyField): bool =>
148 !in_array((int) ($qtyField['fieldIndex'] ?? 0), $usedQuantityIndexes, true)
149 )
150 );
151
152 return $unused[$position] ?? null;
153 }
154
155 /**
156 * @param array<string, mixed> $field
157 * @return array<string, mixed>
158 */
159 private function parseQuantityConfig(array $field): array
160 {
161 $raw = (string) ($field['htmlContent'] ?? '');
162 if ($raw === '') {
163 return [];
164 }
165
166 $decoded = json_decode($raw, true);
167
168 return is_array($decoded) ? $decoded : [];
169 }
170 }
171