PluginProbe
The Innovative Form Builder – IvyForms / 1.5.1
The Innovative Form Builder – IvyForms v1.5.1
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 / SquarePaymentGateway.php

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

245 lines 7.5 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 use IvyForms\Entity\Form\Form;
6 use IvyForms\Services\Translations\BackendStrings;
7
8 /**
9 * Square card charge during public form submission.
10 */
11 class SquarePaymentGateway implements PaymentGatewayInterface
12 {
13 private SquarePaymentService $squarePaymentService;
14
15 private SquareOrderService $squareOrderService;
16
17 public function __construct(
18 SquarePaymentService $squarePaymentService,
19 SquareOrderService $squareOrderService
20 ) {
21 $this->squarePaymentService = $squarePaymentService;
22 $this->squareOrderService = $squareOrderService;
23 }
24
25 public function getId(): string
26 {
27 return 'square';
28 }
29
30 /**
31 * @param array<string, mixed> $rawParams
32 * @param array<int, object> $formFields
33 * @param array<string, mixed> $submissionData
34 * @param object $paymentField
35 */
36 public function processCharge(
37 array $rawParams,
38 Form $form,
39 array $formFields,
40 array $submissionData,
41 object $paymentField,
42 float $total
43 ): PaymentChargeResult {
44 $tokenError = $this->validatePaymentTokens($rawParams);
45 if ($tokenError !== null) {
46 return PaymentChargeResult::failed($this->getId(), $tokenError);
47 }
48
49 $squareSettings = $this->squarePaymentService->getSquareSettings();
50 $connectionError = $this->validateSquareConnection($squareSettings, $form);
51 if ($connectionError !== null) {
52 return PaymentChargeResult::failed($this->getId(), $connectionError);
53 }
54
55 $squareCurrency = (string) ($squareSettings['currency'] ?? 'USD');
56 if (!MoneyMinorUnits::isChargeable($total, $squareCurrency)) {
57 return PaymentChargeResult::failed(
58 $this->getId(),
59 BackendStrings::getPaymentStrings()['invalid_payment_amount']
60 );
61 }
62
63 $submissionByTypeIndex = $this->mapSubmissionToTypeIndexKeys($submissionData, $formFields);
64 $locationId = (string) ($squareSettings['locationId'] ?? '');
65 $amountCents = MoneyMinorUnits::toMinor($total, $squareCurrency);
66 $sourceId = sanitize_text_field((string) ($rawParams['sourceId'] ?? ''));
67 $idempotencyKey = $this->serverIdempotencyKey(
68 $form->getId(),
69 $sourceId,
70 $amountCents,
71 $squareCurrency
72 );
73 $squareFormSettings = $this->resolveSquareFormSettings($form);
74 $buyerInfo = SquareBuyerInfoBuilder::build(
75 $squareFormSettings,
76 $submissionByTypeIndex,
77 $formFields,
78 $submissionData
79 );
80 $uniqueRef = $this->buildUniqueRef();
81
82 $customerId = $this->squareOrderService->getOrCreateCustomerFromEmail(
83 (string) ($buyerInfo['email'] ?? '')
84 );
85
86 $orderResult = $this->squareOrderService->createOrder(
87 $locationId,
88 $amountCents,
89 $squareCurrency,
90 $idempotencyKey,
91 $form->getName(),
92 $uniqueRef,
93 $customerId
94 );
95
96 if (empty($orderResult['success'])) {
97 return PaymentChargeResult::failed(
98 $this->getId(),
99 $orderResult['message']
100 );
101 }
102
103 $squareOrderId = (string) $orderResult['orderId'];
104
105 $paymentResult = $this->squarePaymentService->processPayment(
106 $sourceId,
107 $idempotencyKey,
108 $amountCents,
109 $squareCurrency,
110 $locationId,
111 $this->buildPaymentNote(
112 (string) ($squareFormSettings['paymentDescription'] ?? ''),
113 $form->getName(),
114 $uniqueRef
115 ),
116 $buyerInfo,
117 $squareOrderId,
118 $customerId
119 );
120
121 if (empty($paymentResult['success'])) {
122 $this->squareOrderService->cancelOrder($squareOrderId, $locationId);
123
124 return PaymentChargeResult::failed(
125 $this->getId(),
126 (string) ($paymentResult['message'] ?? BackendStrings::getPaymentStrings()['payment_failed'])
127 );
128 }
129
130 return PaymentChargeResult::succeeded(
131 $this->getId(),
132 $paymentResult,
133 $total,
134 $squareCurrency,
135 SquareBuyerInfoBuilder::customerName($buyerInfo),
136 $squareOrderId,
137 $customerId
138 );
139 }
140
141 /**
142 * @param array<string, mixed> $rawParams
143 */
144 private function validatePaymentTokens(array $rawParams): ?string
145 {
146 $sourceId = sanitize_text_field((string) ($rawParams['sourceId'] ?? ''));
147
148 return $sourceId !== ''
149 ? null
150 : BackendStrings::getPaymentStrings()['payment_token_missing'];
151 }
152
153 /**
154 * Square caps idempotency keys at 45 characters; createOrder appends "-order".
155 */
156 private function serverIdempotencyKey(
157 int $formId,
158 string $sourceId,
159 int $amountCents,
160 string $currency
161 ): string {
162 return substr(
163 hash(
164 'sha256',
165 $formId . '|' . $sourceId . '|' . $amountCents . '|' . strtoupper($currency)
166 ),
167 0,
168 32
169 );
170 }
171
172 /**
173 * @param array<string, mixed> $squareSettings
174 */
175 private function validateSquareConnection(array $squareSettings, Form $form): ?string
176 {
177 if (empty($squareSettings['connected'])) {
178 return BackendStrings::getPaymentStrings()['square_not_connected'];
179 }
180
181 $formCurrency = $form->getPaymentSettings()->getCurrency();
182 $squareCurrency = (string) ($squareSettings['currency'] ?? 'USD');
183 if (strtoupper($formCurrency) !== strtoupper($squareCurrency)) {
184 return BackendStrings::getPaymentStrings()['currency_mismatch'];
185 }
186
187 return null;
188 }
189
190 /**
191 * @return array<string, mixed>
192 */
193 private function resolveSquareFormSettings(Form $form): array
194 {
195 $integrationSettings = $form->getIntegrationSettings();
196 if ($integrationSettings === null) {
197 return [];
198 }
199
200 return $integrationSettings->get('square') ?? [];
201 }
202
203 /**
204 * @param array<string, mixed> $submissionData
205 * @param array<int, object> $formFields
206 * @return array<string, mixed>
207 */
208 private function mapSubmissionToTypeIndexKeys(array $submissionData, array $formFields): array
209 {
210 $mapped = [];
211
212 foreach ($formFields as $field) {
213 if (!method_exists($field, 'getType') || !method_exists($field, 'getIndex')) {
214 continue;
215 }
216
217 $fieldId = method_exists($field, 'getId') ? $field->getId() : null;
218 $type = (string) $field->getType();
219 $index = (int) $field->getIndex();
220 $key = $type . '_' . $index;
221
222 if ($fieldId !== null && isset($submissionData[$fieldId])) {
223 $mapped[$key] = $submissionData[$fieldId];
224 } elseif (isset($submissionData[$key])) {
225 $mapped[$key] = $submissionData[$key];
226 }
227 }
228
229 return $mapped;
230 }
231
232 private function buildPaymentNote(string $template, string $formName, string $uniqueRef): string
233 {
234 $custom = str_replace('{form_name}', $formName, $template);
235 $base = sanitize_text_field($custom !== '' ? $custom : $formName);
236
237 return sprintf('IvyForms - %s [%s]', $base, $uniqueRef);
238 }
239
240 private function buildUniqueRef(): string
241 {
242 return strtoupper(substr(md5(wp_generate_uuid4()), 0, 8));
243 }
244 }
245