PluginProbe
Stripe Payment Forms by WP Simple Pay – Accept Credit Card Payments + Subscriptions with Stripe / trunk
Stripe Payment Forms by WP Simple Pay – Accept Credit Card Payments + Subscriptions with Stripe vtrunk
4.17.3 trunk 2.2.0 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.6.0 2.6.1 2.6.2 2.6.3 4.10.0 4.11.1 4.12.2 4.14.1 4.14.2 4.14.3 4.15.0 4.16.0 All 59 releases
stripe / src / Transaction / TransactionObserver.php

TransactionObserver.php in Stripe Payment Forms by WP Simple Pay – Accept Credit Card Payments + Subscriptions with Stripe trunk, at src/Transaction/TransactionObserver.php

1,102 lines 34.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Transactions: Observer
4 *
5 * @package SimplePay
6 * @subpackage Core
7 * @copyright Copyright (c) 2022, Sandhills Development, LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 4.4.6
10 */
11
12 namespace SimplePay\Core\Transaction;
13
14 use Exception;
15 use SimplePay\Core\API;
16 use SimplePay\Core\EventManagement\SubscriberInterface;
17 use SimplePay\Core\License\LicenseAwareInterface;
18 use SimplePay\Core\License\LicenseAwareTrait;
19 use SimplePay\Core\StripeConnect\ApplicationFee;
20 use stdClass;
21
22 /**
23 * TransactionObserver class.
24 *
25 * @since 4.4.6
26 */
27 class TransactionObserver implements SubscriberInterface, LicenseAwareInterface {
28
29 use LicenseAwareTrait;
30
31 /**
32 * Transaction repository.
33 *
34 * @since 4.4.6
35 * @var \SimplePay\Core\Transaction\TransactionRepository
36 */
37 private $transactions;
38
39 /**
40 * Application fee helper.
41 *
42 * @since 4.4.6
43 * @var \SimplePay\Core\StripeConnect\ApplicationFee
44 */
45 private $application_fee;
46
47 /**
48 * TransactionObserver.
49 *
50 * @since 4.4.6
51 *
52 * @param \SimplePay\Core\Transaction\TransactionRepository $transactions Transaction repository.
53 * @param \SimplePay\Core\StripeConnect\ApplicationFee $application_fee Appilcation fee helper.
54 */
55 public function __construct(
56 TransactionRepository $transactions,
57 ApplicationFee $application_fee
58 ) {
59 $this->transactions = $transactions;
60 $this->application_fee = $application_fee;
61 }
62
63 /**
64 * {@inheritdoc}
65 */
66 public function get_subscribed_events() {
67 $observe = array(
68 // Initial one time payment.
69 'simpay_after_paymentintent_from_payment_form_request' =>
70 array(
71 array( 'add_on_payment_intent', 10, 4 ),
72 // @todo This might not be the best place for this,
73 // but it makes enough sense for now.
74 array( 'maybe_decrement_stock', 10, 4 ),
75 ),
76 // Initial recurring payment.
77 'simpay_after_subscription_from_payment_form_request' =>
78 array(
79 array( 'add_on_subscription', 10, 2 ),
80 // @todo This might not be the best place for this,
81 // but it makes enough sense for now.
82 array( 'maybe_decrement_stock', 10, 4 ),
83 ),
84 // Initial invoice payment.
85 'simpay_after_invoice_from_payment_form_request' =>
86 array(
87 array( 'add_on_multiple_line_invoice', 10, 2 ),
88 array( 'maybe_decrement_stock', 10, 4 ),
89 ),
90 'simpay_webhook_invoice_paid' => array(
91 array( 'update_on_multiple_line_invoice', 10, 2 ),
92 ),
93 // Initial Checkout Session.
94 'simpay_after_checkout_session_from_payment_form_request' =>
95 array( 'add_on_checkout_session', 10, 2 ),
96 // Update one time payment.
97 'simpay_webhook_payment_intent_succeeded' =>
98 array( 'update_on_payment_intent', 10, 2 ),
99 // Update Checkout Session.
100 'simpay_webhook_checkout_session_completed' =>
101 array( 'update_on_checkout_session', 10, 4 ),
102 'simpay_webhook_invoice_payment_succeeded' =>
103 array(
104 // Initial recurring subsequent invoice.
105 array( 'add_on_invoice', 10, 3 ),
106 // Update recurring subsequent invoice.
107 array( 'update_on_invoice', 10, 3 ),
108 ),
109
110 // Update on charge failed.
111 // @todo This might not be the best place for this, but it makes
112 // enough sense for now.
113 'simpay_webhook_charge_failed' =>
114 array(
115 array( 'update_on_failed', 10, 2 ),
116 array( 'maybe_increment_stock', 10, 2 ),
117 ),
118 'simpay_webhook_charge_refunded' => array( 'add_refund_log', 10, 3 ),
119 );
120
121 // Update Checkout Session in Lite when viewing confirmation.
122 if ( true === $this->license->is_lite() ) {
123 $observe['_simpay_payment_confirmation'] =
124 array( 'update_on_checkout_session_lite' );
125 }
126
127 return $observe;
128 }
129
130 /**
131 * Logs a transaction when a refund is created.
132 *
133 * @since 4.10.0
134 *
135 * @param \SimplePay\Vendor\Stripe\Event $event Stripe Event object.
136 * @param \SimplePay\Vendor\Stripe\Charge $charge Stripe Charge object.
137 * @param \SimplePay\Core\Abstracts\Form $form Payment Form.
138 * @return void
139 */
140 public function add_refund_log( $event, $charge, $form ) {
141 // Check if the payment made form WP Simple Pay.
142 if ( ! isset( $charge->payment_intent->metadata->simpay_form_id ) ) {
143 return;
144 }
145
146 $transaction = $this->transactions->get_by_object_id(
147 $charge->payment_intent->id
148 );
149
150 // Update the transaction status if it exists.
151 if ( $transaction instanceof Transaction ) {
152 $this->transactions->update(
153 $transaction->id,
154 array(
155 'status' => 'refunded',
156 'amount_refunded' => $charge->amount_refunded,
157 )
158 );
159 } else {
160 // Create a new transaction log if it doesn't exist.
161 $this->transactions->add(
162 array(
163 'form_id' => $charge->payment_intent->metadata->simpay_form_id,
164 'object' => 'payment_intent',
165 'object_id' => $charge->payment_intent->id,
166 'livemode' => (bool) $charge->livemode,
167 'amount_total' => $charge->amount,
168 'amount_subtotal' => $charge->amount_refunded,
169 'amount_shipping' => 0,
170 'amount_discount' => 0,
171 'amount_refunded' => $charge->amount_refunded,
172 'currency' => $charge->currency,
173 'payment_method_type' => null,
174 'email' => $charge->billing_details->email, // @phpstan-ignore-line
175 'customer_id' => $charge->customer->id, // @phpstan-ignore-line
176 'subscription_id' => null,
177 'status' => 'refunded',
178 'application_fee' => $this->application_fee->has_application_fee(),
179 )
180 );
181 }
182 }
183
184 /**
185 * Logs a transaction when a PaymentIntent is created.
186 *
187 * @since 4.4.6
188 *
189 * @param \SimplePay\Vendor\Stripe\PaymentIntent $payment_intent PaymentIntent object.
190 * @param \SimplePay\Core\Abstracts\Form $form Payment Form.
191 * @param array<mixed> $form_data Form data generated by the client.
192 * @param array<mixed> $form_values Form values.
193 * @return void
194 */
195 public function add_on_payment_intent( $payment_intent, $form, $form_data, $form_values ) {
196 // Retrieve from metadata if it exists (UPE only).
197 if ( isset(
198 $payment_intent->metadata->simpay_unit_amount,
199 $payment_intent->metadata->simpay_quantity
200 ) ) {
201 $quantity = $payment_intent->metadata->simpay_quantity;
202 $unit_amount = $payment_intent->metadata->simpay_unit_amount;
203
204 $subtotal = $unit_amount * $quantity;
205
206 // Calculate based on the form data (non-UPE).
207 } else {
208 $price = simpay_payment_form_prices_get_price_by_id(
209 $form,
210 $form_data['price']['id'] // @phpstan-ignore-line
211 );
212
213 if ( false === $price ) {
214 return;
215 }
216
217 // Custom amount. Verify minimum amount.
218 if ( false === simpay_payment_form_prices_is_defined_price( $price->id ) ) {
219 // Ensure custom amount meets minimum requirement.
220 $unit_amount = $form_data['customAmount'];
221
222 if ( $unit_amount < $price->unit_amount_min ) {
223 $unit_amount = $price->unit_amount_min;
224 }
225 } else {
226 $unit_amount = $price->unit_amount;
227 }
228
229 /** @var int $unit_amount */
230
231 // Backwards compatibility amount filter.
232 if ( has_filter( 'simpay_form_' . $form->id . '_amount' ) ) {
233 /** @var int $unit_amount */
234 $unit_amount = simpay_get_filtered(
235 'amount',
236 simpay_convert_amount_to_dollars( $unit_amount ),
237 $form->id
238 );
239
240 $unit_amount = simpay_convert_amount_to_cents( $unit_amount );
241 }
242
243 // Calculate quantity.
244 $quantity = isset( $form_values['simpay_quantity'] )
245 ? intval( $form_values['simpay_quantity'] ) // @phpstan-ignore-line
246 : 1;
247
248 $subtotal = $unit_amount * $quantity;
249 }
250
251 /** @var \SimplePay\Vendor\Stripe\Customer $customer */
252 $customer = $payment_intent->customer;
253
254 $this->transactions->add(
255 array(
256 'form_id' => $form->id,
257 'object' => $payment_intent->object,
258 'object_id' => $payment_intent->id,
259 'livemode' => (bool) $payment_intent->livemode,
260 'amount_total' => $payment_intent->amount,
261 'amount_subtotal' => $subtotal,
262 'amount_shipping' => 0,
263 'amount_discount' => 0,
264 'amount_tax' => isset( $payment_intent->metadata->simpay_tax_unit_amount )
265 ? (int) $payment_intent->metadata->simpay_tax_unit_amount
266 : 0,
267 'currency' => $payment_intent->currency,
268 'payment_method_type' => null,
269 'email' => $customer->email,
270 'customer_id' => $customer->id,
271 'subscription_id' => null,
272 'status' => $payment_intent->status,
273 'application_fee' => $this->application_fee->has_application_fee(),
274 )
275 );
276 }
277
278 /**
279 * Logs a transaction when a Subscription is created.
280 *
281 * Data will later be updated via the `invoice.payment_succeeded` webhook event.
282 *
283 * @since 4.4.6
284 *
285 * @param \SimplePay\Vendor\Stripe\Subscription $subscription Subscription object.
286 * @param \SimplePay\Core\Abstracts\Form $form Payment Form.
287 * @return void
288 */
289 public function add_on_subscription( $subscription, $form ) {
290 /** @var \SimplePay\Vendor\Stripe\Customer $customer */
291 $customer = $subscription->customer;
292
293 $this->transactions->add(
294 array(
295 'form_id' => $form->id,
296 'object' => 'subscription',
297 'object_id' => $subscription->id,
298 'livemode' => (bool) $subscription->livemode,
299 'amount_total' => 0,
300 'amount_subtotal' => 0,
301 'amount_shipping' => 0,
302 'amount_discount' => 0,
303 'amount_tax' => 0,
304 'currency' => $customer->currency,
305 'payment_method_type' => null,
306 'email' => $customer->email,
307 'customer_id' => $customer->id,
308 'subscription_id' => $subscription->id,
309 'status' => $subscription->status,
310 'application_fee' => $this->application_fee->has_application_fee(),
311 )
312 );
313 }
314
315 /**
316 * Logs a transaction when a subsequent Invoice is created.
317 *
318 * @since 4.11.0
319 *
320 * @param \SimplePay\Vendor\Stripe\Invoice $invoice Invoice object.
321 *
322 * @return void
323 */
324 public function add_on_multiple_line_invoice( $invoice ) {
325 if ( ! is_string( $invoice->payment_intent ) ) {
326 return;
327 }
328
329 $transaction = $this->transactions->get_by_object_id(
330 $invoice->payment_intent
331 );
332
333 // Do nothing if the PaymentIntent has already been logged.
334 if ( $transaction instanceof Transaction ) {
335 return;
336 }
337
338 if ( null === $invoice->total_discount_amounts ) {
339 $total_discount = 0;
340 } else {
341 $total_discount = array_reduce(
342 $invoice->total_discount_amounts,
343 /**
344 * Adds a discount amount to the total discount.
345 *
346 * @since 4.4.6
347 *
348 * @param int $total_discount Total discount, so far.
349 * @param \stdClass $discount Discount object.
350 */
351 function ( $total_discount, $discount ) {
352 /** @var \stdClass $discount */
353 return $total_discount + $discount->amount;
354 },
355 0
356 );
357 }
358
359 $metadata = $invoice->metadata;
360
361 if ( ! isset( $metadata->simpay_form_id ) ) {
362 return;
363 }
364
365 $this->transactions->add(
366 array(
367 'form_id' => (int) $metadata->simpay_form_id,
368 'object' => 'payment_intent',
369 'object_id' => $invoice->payment_intent,
370 'livemode' => (bool) $invoice->livemode,
371 'amount_total' => $invoice->total,
372 'amount_subtotal' => $invoice->subtotal,
373 'amount_discount' => $total_discount,
374 'amount_shipping' => 0,
375 'amount_tax' => null === $invoice->tax
376 ? 0
377 : $invoice->tax,
378 'currency' => $invoice->currency,
379 'payment_method_type' => null,
380 'email' => $invoice->customer_email,
381 'customer_id' => $invoice->customer,
382 'subscription_id' => $invoice->subscription,
383 'status' => $invoice->status,
384 'application_fee' => $this->application_fee->has_application_fee(),
385 )
386 );
387 }
388
389 /**
390 * Updates a transaction's status when a subsequent Invoice is paid.
391 *
392 * @since 4.11.0
393 *
394 * @param \SimplePay\Vendor\Stripe\Event $event Event object.
395 * @param \SimplePay\Vendor\Stripe\Invoice $invoice Invoice object.
396 * @return void
397 */
398 public function update_on_multiple_line_invoice( $event, $invoice ) {
399 if ( ! is_string( $invoice->payment_intent ) ) {
400 return;
401 }
402
403 $transaction = $this->transactions->get_by_object_id(
404 $invoice->payment_intent
405 );
406
407 if ( ! $transaction instanceof Transaction ) {
408 return;
409 }
410
411 $form = simpay_get_form( $transaction->form_id );
412
413 if ( false === $form ) {
414 return;
415 }
416
417 $invoice = API\Invoices\retrieve(
418 array(
419 'id' => (string) $invoice->id,
420 'expand' => array(
421 'payment_intent.payment_method',
422 ),
423 ),
424 $form->get_api_request_args()
425 );
426
427 /** @var \SimplePay\Vendor\Stripe\PaymentIntent $payment_intent */
428 $payment_intent = $invoice->payment_intent;
429
430 /** @var \SimplePay\Vendor\Stripe\PaymentMethod $payment_method */
431 $payment_method = $payment_intent->payment_method;
432
433 $this->transactions->update(
434 $transaction->id,
435 array(
436 'payment_method_type' => $payment_method->type,
437 'status' => 'paid' === $invoice->status
438 ? 'succeeded'
439 : 'canceled',
440 )
441 );
442 }
443
444 /**
445 * Logs a transaction when a subsequent Subscription Invoice is created.
446 *
447 * @since 4.4.6
448 *
449 * @param \SimplePay\Vendor\Stripe\Event $event Event object.
450 * @param \SimplePay\Vendor\Stripe\Invoice $invoice Invoice object.
451 * @param \SimplePay\Vendor\Stripe\Subscription $subscription Subscription object.
452 * @return void
453 */
454 public function add_on_invoice( $event, $invoice, $subscription ) {
455 // Do nothing if this invoice event was triggered by creating a Subscription.
456 // A previously inserted record will be updated instead.
457 if ( 'subscription_cycle' !== $invoice->billing_reason ) {
458 return;
459 }
460
461 if ( ! is_string( $invoice->payment_intent ) ) {
462 return;
463 }
464
465 $transaction = $this->transactions->get_by_object_id(
466 $invoice->payment_intent
467 );
468
469 // Do nothing if the PaymentIntent has already been logged.
470 if ( $transaction instanceof Transaction ) {
471 return;
472 }
473
474 if ( null === $invoice->total_discount_amounts ) {
475 $total_discount = 0;
476 } else {
477 $total_discount = array_reduce(
478 $invoice->total_discount_amounts,
479 /**
480 * Adds a discount amount to the total discount.
481 *
482 * @since 4.4.6
483 *
484 * @param int $total_discount Total discount, so far.
485 * @param \stdClass $discount Discount object.
486 */
487 function ( $total_discount, $discount ) {
488 /** @var \stdClass $discount */
489 return $total_discount + $discount->amount;
490 },
491 0
492 );
493 }
494
495 $metadata = $subscription->metadata;
496
497 if ( ! isset( $metadata->simpay_form_id ) ) {
498 return;
499 }
500
501 /** @var \SimplePay\Vendor\Stripe\PaymentMethod $payment_method */
502 $payment_method = $subscription->default_payment_method;
503 $payment_method_type = $payment_method ? $payment_method->type : null;
504
505 $this->transactions->add(
506 array(
507 'form_id' => (int) $metadata->simpay_form_id,
508 'object' => 'payment_intent',
509 'object_id' => $invoice->payment_intent,
510 'livemode' => (bool) $invoice->livemode,
511 'amount_total' => $invoice->total,
512 'amount_subtotal' => $invoice->subtotal,
513 'amount_discount' => $total_discount,
514 'amount_shipping' => 0,
515 'amount_tax' => null === $invoice->tax
516 ? 0
517 : $invoice->tax,
518 'currency' => $invoice->currency,
519 'payment_method_type' => $payment_method_type,
520 'email' => $invoice->customer_email,
521 'customer_id' => $invoice->customer,
522 'subscription_id' => $invoice->subscription,
523 'status' => 'paid' === $invoice->status
524 ? 'succeeded'
525 : 'canceled',
526 'application_fee' => $this->application_fee->has_application_fee(),
527 )
528 );
529 }
530
531 /**
532 * Logs a transaction when a Checkout Session is created. Either a payment_intent
533 * or invoice object will be created.
534 *
535 * Data will later be updated via the `checkout.session.completed` webhook event.
536 *
537 * @since 4.4.6
538 *
539 * @param \SimplePay\Vendor\Stripe\Checkout\Session $checkout_session Checkout Session object.
540 * @param \SimplePay\Core\Abstracts\Form $form Payment Form.
541 * @return void
542 */
543 public function add_on_checkout_session( $checkout_session, $form ) {
544 $this->transactions->add(
545 array(
546 'form_id' => $form->id,
547 'object' => 'checkout_session',
548 'object_id' => $checkout_session->id,
549 'livemode' => (bool) $checkout_session->livemode,
550 'amount_total' => 0,
551 'amount_subtotal' => 0,
552 'amount_shipping' => 0,
553 'amount_discount' => 0,
554 'amount_tax' => 0,
555 'currency' => $checkout_session->currency,
556 'payment_method_type' => null,
557 'email' => $checkout_session->customer_email,
558 'customer_id' => $checkout_session->customer,
559 'subscription_id' => $checkout_session->subscription,
560 'status' => $checkout_session->status,
561 'application_fee' => $this->application_fee->has_application_fee(),
562 )
563 );
564 }
565
566 /**
567 * Updates a transaction's totals when receiving the `payment_intent.succeeded`
568 * webhook event.
569 *
570 * @since 4.4.6
571 *
572 * @param \SimplePay\Vendor\Stripe\Event $event Event object.
573 * @param \SimplePay\Vendor\Stripe\PaymentIntent $payment_intent PaymentIntent object.
574 * @return void
575 */
576 public function update_on_payment_intent( $event, $payment_intent ) {
577 $transaction = $this->transactions->get_by_object_id(
578 $payment_intent->id
579 );
580
581 if ( ! $transaction instanceof Transaction ) {
582 return;
583 }
584
585 /** @var \SimplePay\Vendor\Stripe\PaymentMethod $payment_method */
586 $payment_method = $payment_intent->payment_method;
587 $payment_method_type = $payment_method ? $payment_method->type : null;
588
589 $this->transactions->update(
590 $transaction->id,
591 array(
592 'payment_method_type' => $payment_method_type,
593 'status' => $payment_intent->status,
594 'application_fee' => $this->application_fee->has_application_fee(),
595 )
596 );
597 }
598
599 /**
600 * Updates a transaction's totals when receiving the `checkout.session.completed`
601 * webhook event. This is used to avoid making manual calculations for the totals
602 * when creating the original transaction.
603 *
604 * @since 4.4.6
605 *
606 * @param \SimplePay\Vendor\Stripe\Event $event Event object.
607 * @param \SimplePay\Vendor\Stripe\Customer $customer Customer object.
608 * @param \SimplePay\Vendor\Stripe\PaymentIntent|null $payment_intent PaymentIntent object, if available.
609 * @param \SimplePay\Vendor\Stripe\Subscription|null $subscription Subscription object, if available.
610 * @return void
611 */
612 public function update_on_checkout_session( $event, $customer, $payment_intent, $subscription ) {
613 /** @var \SimplePay\Vendor\Stripe\Checkout\Session $checkout_session */
614 $checkout_session = $event->data->object; // @phpstan-ignore-line
615 $transaction = $this->transactions->get_by_object_id(
616 $checkout_session->id
617 );
618
619 if ( ! $transaction instanceof Transaction ) {
620 return;
621 }
622
623 // One time payment.
624 if (
625 'payment' === $checkout_session->mode &&
626 null !== $payment_intent
627 ) {
628 $object = 'payment_intent';
629 $object_id = $payment_intent->id;
630
631 /** @var \SimplePay\Vendor\Stripe\PaymentMethod $payment_method */
632 $payment_method = $payment_intent->payment_method;
633 $payment_method_type = $payment_method->type;
634
635 // Recurring payment, paid today.
636 } elseif (
637 'subscription' === $checkout_session->mode &&
638 null !== $subscription &&
639 null !== $subscription->latest_invoice &&
640 null === $checkout_session->setup_intent
641 ) {
642 /** @var \SimplePay\Vendor\Stripe\Invoice $latest_invoice */
643 $latest_invoice = $subscription->latest_invoice;
644 /** @var \SimplePay\Vendor\Stripe\PaymentIntent $payment_intent */
645 $payment_intent = $latest_invoice->payment_intent;
646
647 $object = 'payment_intent';
648 $object_id = $payment_intent->id;
649
650 /** @var \SimplePay\Vendor\Stripe\PaymentMethod $payment_method */
651 $payment_method = $subscription->default_payment_method;
652 $payment_method_type = $payment_method->type;
653
654 // Recurring payment, trial.
655 // Free trials/non-payment invoices for non-Stripe Checkout payment
656 // forms do not have access to the SetupIntent.
657 // @todo maybe set the object_id to null for consistent behavior?
658 } elseif (
659 'subscription' === $checkout_session->mode &&
660 null !== $subscription &&
661 null !== $checkout_session->setup_intent
662 ) {
663 $object = 'setup_intent';
664 $object_id = $checkout_session->setup_intent;
665
666 /** @var \SimplePay\Vendor\Stripe\PaymentMethod $payment_method */
667 $payment_method = $subscription->default_payment_method;
668 $payment_method_type = $payment_method ? $payment_method->type : null;
669
670 // Something else.
671 } else {
672 $object_id = null;
673 $object = null;
674 $payment_method_type = null;
675 }
676
677 /**
678 * @var \stdClass $default_totals
679 * @property int $amount_shipping
680 * @property int $amount_discount
681 * @property int $amount_tax
682 */
683 $default_totals = new stdClass();
684 $default_totals->amount_shipping = 0;
685 $default_totals->amount_discount = 0;
686 $default_totals->amount_tax = 0;
687
688 /**
689 * @var \stdClass $totals
690 * @property int $amount_shipping
691 * @property int $amount_discount
692 * @property int $amount_tax
693 */
694 $totals = null !== $checkout_session->total_details
695 ? $checkout_session->total_details
696 : $default_totals;
697
698 $this->transactions->update(
699 $transaction->id,
700 array(
701 'object' => $object,
702 'object_id' => $object_id,
703 'amount_total' => $checkout_session->amount_total,
704 'amount_subtotal' => $checkout_session->amount_subtotal,
705 'amount_shipping' => $totals->amount_shipping,
706 'amount_discount' => $totals->amount_discount,
707 'amount_tax' => $totals->amount_tax,
708 'payment_method_type' => $payment_method_type,
709 'email' => $customer->email,
710 'customer_id' => $customer->id,
711 'subscription_id' => $checkout_session->subscription,
712 'status' => 'succeeded',
713 'application_fee' => $this->application_fee->has_application_fee(),
714 )
715 );
716 }
717
718 /**
719 * Updates a transaction's totals when viewing the [simpay_payment_receipt]
720 * shortcode in Lite, which cannot rely on webhooks.
721 *
722 * @since 4.4.6
723 *
724 * @param array<string, mixed> $payment_confirmation_data Payment confirmation data.
725 * @return void
726 */
727 public function update_on_checkout_session_lite( $payment_confirmation_data ) {
728 // Session ID is not available, do nothing.
729 if ( ! isset( $_GET['session_id'] ) ) {
730 return;
731 }
732
733 $session_id = sanitize_text_field( $_GET['session_id'] );
734 $transaction = $this->transactions->get_by_object_id( $session_id );
735
736 // Transaction cannot be found, do nothing.
737 if ( ! $transaction instanceof Transaction ) {
738 return;
739 }
740
741 /** @var \SimplePay\Core\Abstracts\Form|false $form */
742 $form = $payment_confirmation_data['form'];
743
744 if ( false === $form ) {
745 return;
746 }
747
748 try {
749 $session = API\CheckoutSessions\retrieve(
750 array(
751 'id' => $session_id,
752 'expand' => array(
753 'customer',
754 'payment_intent',
755 'subscription',
756 'subscription.latest_invoice',
757 'subscription.default_payment_method',
758 ),
759 ),
760 $form->get_api_request_args()
761 );
762
763 /** @var \SimplePay\Vendor\Stripe\Customer $customer */
764 $customer = $session->customer;
765
766 // One time payment.
767 if ( 'payment' === $session->mode && null !== $session->payment_intent ) {
768 $object = 'payment_intent';
769 $object_id = is_object( $session->payment_intent ) ? $session->payment_intent->id : $session->payment_intent;
770
771 /** @var \SimplePay\Vendor\Stripe\PaymentMethod $payment_method */
772 $payment_method = is_object( $session->payment_intent ) && property_exists( $session->payment_intent, 'payment_method' ) ?
773 $session->payment_intent->payment_method : null;
774 $payment_method_type = $payment_method ? $payment_method->type : 'card';
775
776 // Recurring payment, paid today.
777 } elseif (
778 'subscription' === $session->mode &&
779 null !== $session->subscription &&
780 null !== $session->subscription &&
781 is_object( $session->subscription ) &&
782 property_exists( $session->subscription, 'latest_invoice' ) &&
783 null !== $session->subscription->latest_invoice &&
784 null === $session->setup_intent
785 ) {
786 /** @var \SimplePay\Vendor\Stripe\Invoice $latest_invoice */
787 $latest_invoice = $session->subscription->latest_invoice;
788 /** @var \SimplePay\Vendor\Stripe\PaymentIntent $payment_intent */
789 $payment_intent = $latest_invoice->payment_intent;
790
791 $object = 'payment_intent';
792 $object_id = is_string( $payment_intent ) ? $payment_intent : $payment_intent->id;
793
794 /** @var \SimplePay\Vendor\Stripe\PaymentMethod $payment_method */
795 $payment_method = is_object( $session->subscription ) && property_exists( $session->subscription, 'default_payment_method' ) ?
796 $session->subscription->default_payment_method : null;
797 $payment_method_type = $payment_method ? $payment_method->type : 'card';
798
799 // Recurring payment.
800 } elseif (
801 'subscription' === $session->mode &&
802 null !== $session->subscription &&
803 null !== $session->setup_intent
804 ) {
805 $object = 'setup_intent';
806 $object_id = $session->setup_intent;
807
808 /** @var \SimplePay\Vendor\Stripe\PaymentMethod $payment_method */
809 $payment_method = is_object( $session->subscription ) && property_exists( $session->subscription, 'default_payment_method' ) ?
810 $session->subscription->default_payment_method : null;
811 $payment_method_type = $payment_method ? $payment_method->type : 'card';
812
813 // Something else.
814 } else {
815 $object_id = null;
816 $object = null;
817 $payment_method_type = 'card';
818 }
819
820 /**
821 * @var \stdClass $default_totals
822 * @property int $amount_shipping
823 * @property int $amount_discount
824 * @property int $amount_tax
825 */
826 $default_totals = new stdClass();
827 $default_totals->amount_shipping = 0;
828 $default_totals->amount_discount = 0;
829 $default_totals->amount_tax = 0;
830
831 /**
832 * @var \stdClass $totals
833 * @property int $amount_shipping
834 * @property int $amount_discount
835 * @property int $amount_tax
836 */
837 $totals = null !== $session->total_details
838 ? $session->total_details
839 : $default_totals;
840
841 $this->transactions->update(
842 $transaction->id,
843 array(
844 'object' => $object,
845 'object_id' => $object_id,
846 'amount_total' => $session->amount_total,
847 'amount_subtotal' => $session->amount_subtotal,
848 'amount_shipping' => $totals->amount_shipping,
849 'amount_discount' => $totals->amount_discount,
850 'amount_tax' => $totals->amount_tax,
851 'payment_method_type' => $payment_method_type,
852 'email' => $customer->email,
853 'customer_id' => $customer->id,
854 'subscription_id' => $session->subscription ? ( is_string( $session->subscription ) ? $session->subscription : $session->subscription->id ) : null,
855 'status' => 'succeeded',
856 'application_fee' => $this->application_fee->has_application_fee(),
857 )
858 );
859 } catch ( Exception $e ) {
860 // Do nothing if Session cannot be found.
861 }
862 }
863
864 /**
865 * Updates a transaction's totals when receiving the `invoice.payment_succeeded`
866 * webhook event, only for the first invoice created for a Subscription.
867 *
868 * Subsequent invoices are handled in `self::add_on_invoice()`.
869 *
870 * @since 4.4.6
871 *
872 * @param \SimplePay\Vendor\Stripe\Event $event Event object.
873 * @param \SimplePay\Vendor\Stripe\Invoice $invoice Invoice object.
874 * @param \SimplePay\Vendor\Stripe\Subscription $subscription Subscription object.
875 * @return void
876 */
877 public function update_on_invoice( $event, $invoice, $subscription ) {
878 // Do nothing if this invoice event was not triggered by creating a Subscription.
879 // A new record will be created instead for subsequent invoices.
880 if ( 'subscription_create' !== $invoice->billing_reason ) {
881 return;
882 }
883
884 $transaction = $this->transactions->get_by_object_id(
885 $subscription->id
886 );
887
888 if ( ! $transaction instanceof Transaction ) {
889 return;
890 }
891
892 // No trial, or there was a setup fee, so there is a charge.
893 if ( null !== $invoice->payment_intent ) {
894 $object = 'payment_intent';
895 $object_id = $invoice->payment_intent;
896
897 // Trial and no setup fee.
898 // Stripe doesn't create a SetupIntent or PaymentIntent if the first
899 // invoice is free. Set to a setup_intent and do not link anything.
900 // This creates slightly different behavior than trials with Stripe
901 // Checkout, which do have an accessible SetupIntent.
902 } else {
903 $object = 'setup_intent';
904 $object_id = null;
905 }
906
907 if ( null === $invoice->total_discount_amounts ) {
908 $total_discount = 0;
909 } else {
910 $total_discount = array_reduce(
911 $invoice->total_discount_amounts,
912 /**
913 * Adds a discount amount to the total discount.
914 *
915 * @since 4.4.6
916 *
917 * @param int $total_discount Total discount, so far.
918 * @param \stdClass $discount Discount object.
919 */
920 function ( $total_discount, $discount ) {
921 /** @var \stdClass $discount */
922 return $total_discount + $discount->amount;
923 },
924 0
925 );
926 }
927
928 /** @var \SimplePay\Vendor\Stripe\PaymentMethod $payment_method */
929 $payment_method = $subscription->default_payment_method;
930 $payment_method_type = $payment_method ? $payment_method->type : null;
931
932 $this->transactions->update(
933 $transaction->id,
934 array(
935 'object' => $object,
936 'object_id' => $object_id,
937 'amount_total' => $invoice->total,
938 'amount_subtotal' => $invoice->subtotal,
939 'amount_discount' => $total_discount,
940 'amount_tax' => null === $invoice->tax
941 ? 0
942 : $invoice->tax,
943 'payment_method_type' => $payment_method_type,
944 'email' => $invoice->customer_email,
945 'customer_id' => $invoice->customer,
946 'subscription_id' => $invoice->subscription,
947 'status' => in_array(
948 $subscription->status,
949 array( 'active', 'trialing' ),
950 true
951 )
952 ? 'succeeded'
953 : 'canceled',
954 'application_fee' => $this->application_fee->has_application_fee(),
955 )
956 );
957 }
958
959 /**
960 * Updates a transaction's status on `charge.failed`.
961 *
962 * @since 4.6.7
963 *
964 * @param \SimplePay\Vendor\Stripe\Event $event Event object.
965 * @param \SimplePay\Vendor\Stripe\Charge $charge Charge object.
966 * @return void
967 */
968 public function update_on_failed( $event, $charge ) {
969 /** @var \SimplePay\Vendor\Stripe\PaymentIntent $payment_intent */
970 $payment_intent = $charge->payment_intent;
971
972 /** @var \SimplePay\Vendor\Stripe\Invoice $invoice */
973 $invoice = $charge->invoice;
974
975 // Find from a Subscription.
976 if ( $invoice && $invoice->subscription ) {
977 /** @var \SimplePay\Vendor\Stripe\Subscription $subscription */
978 $subscription = $invoice->subscription;
979 $transaction_id = $subscription->id;
980
981 // Find from a one time payment.
982 } else {
983 $transaction_id = $payment_intent->id;
984 }
985
986 $transaction = $this->transactions->get_by_object_id( $transaction_id );
987
988 if ( ! $transaction instanceof Transaction ) {
989 return;
990 }
991
992 /**
993 * @var \stdClass $payment_method_details
994 * @property string $type
995 */
996 $payment_method_details = $charge->payment_method_details;
997
998 $this->transactions->update(
999 $transaction->id,
1000 array(
1001 'object' => 'payment_intent',
1002 'object_id' => $payment_intent->id,
1003 'status' => $charge->status,
1004 'payment_method_type' => $payment_method_details->type,
1005 )
1006 );
1007 }
1008
1009 /**
1010 * Possibly decremeents available stock/inventory if the price option requires it.
1011 *
1012 * @since 4.6.4
1013 *
1014 * @param stdClass $object Stripe object.
1015 * @param \SimplePay\Core\Abstracts\Form $form Payment Form.
1016 * @param array<mixed> $form_data Form data generated by the client.
1017 * @param array<mixed> $form_values Form values.
1018 * @return void
1019 */
1020 public function maybe_decrement_stock( $object, $form, $form_data, $form_values ) {
1021 if ( false === $form->is_managing_inventory() ) {
1022 return;
1023 }
1024
1025 $behavior = $form->get_inventory_behavior();
1026 $prices = $object->metadata->simpay_price_instances;
1027 $prices = explode( '|', $prices );
1028
1029 switch ( $behavior ) {
1030 case 'combined':
1031 $price_option = current( $prices );
1032 $price_option_parts = explode( ':', $price_option );
1033 $quantity = intval( $price_option_parts[1] );
1034 $form->adjust_inventory( 'decrement', $quantity, null );
1035
1036 break;
1037 case 'individual':
1038 foreach ( $prices as $price_option ) {
1039 $price_option_parts = explode( ':', $price_option );
1040 $instance_id = $price_option_parts[0];
1041 $quantity = intval( $price_option_parts[1] );
1042
1043 $form->adjust_inventory( 'decrement', $quantity, $instance_id );
1044 }
1045
1046 break;
1047 }
1048 }
1049
1050 /**
1051 * Increments inventory when a payment fails to process.
1052 *
1053 * @since 4.6.4
1054 *
1055 * @param \SimplePay\Vendor\Stripe\Event $event Webhook event.
1056 * @param \SimplePay\Vendor\Stripe\Charge $charge Charge object.
1057 * @return void
1058 */
1059 public function maybe_increment_stock( $event, $charge ) {
1060 // Subscription.
1061 if ( $charge->invoice ) {
1062 /** @var \SimplePay\Vendor\Stripe\Invoice $invoice Expanded by event receiver. */
1063 $invoice = $charge->invoice;
1064
1065 if ( 'subscription_create' !== $invoice->billing_reason ) {
1066 return;
1067 }
1068
1069 $object = $invoice->subscription;
1070
1071 // One-time.
1072 } else {
1073 $object = $charge->payment_intent;
1074 }
1075
1076 if ( ! isset( $object->metadata->simpay_form_id ) ) {
1077 return;
1078 }
1079
1080 $form = simpay_get_form( $object->metadata->simpay_form_id );
1081
1082 if ( false === $form ) {
1083 return;
1084 }
1085
1086 if ( ! isset( $object->metadata->simpay_price_instances ) ) {
1087 return;
1088 }
1089
1090 $prices = $object->metadata->simpay_price_instances;
1091 $prices = explode( '|', $prices );
1092
1093 foreach ( $prices as $price_option ) {
1094 $price_option_parts = explode( ':', $price_option );
1095 $instance_id = $price_option_parts[0];
1096 $quantity = intval( $price_option_parts[1] );
1097
1098 $form->adjust_inventory( 'increment', $quantity, $instance_id );
1099 }
1100 }
1101 }
1102