diff --git a/src/Concrete/WoocommercePlatformOrderDecorator.php b/src/Concrete/WoocommercePlatformOrderDecorator.php index 4ad58805..e95d490b 100644 --- a/src/Concrete/WoocommercePlatformOrderDecorator.php +++ b/src/Concrete/WoocommercePlatformOrderDecorator.php @@ -846,6 +846,7 @@ private function extractBasePaymentData() $newPaymentData->identifier = $identifier; $newPaymentData->installments = intval($this->formData["installments"]); $newPaymentData->recurrenceCycle = $this->formData["recurrence_cycle"] ?? null; + $newPaymentData->recurrenceModel = $this->formData["recurrence_model"] ?? null; $newPaymentData->paymentOrigin = $this->formData["payment_origin"] ?? null; $newPaymentData->saveOnSuccess = isset($this->formData["save_credit_card"]); $amount = $this->formData["card_order_value"] ?? $this->getGrandTotal(); diff --git a/src/Model/Checkout.php b/src/Model/Checkout.php index 39b71fcc..89864994 100644 --- a/src/Model/Checkout.php +++ b/src/Model/Checkout.php @@ -130,6 +130,7 @@ public function process(WC_Order $wc_order = null, string $type = CheckoutTypes: if ($type === CheckoutTypes::TRANSPARENT_VALUE) { $fields = $this->convertCheckoutObject($_POST[PaymentRequestInterface::PAGARME_PAYMENT_REQUEST_KEY]); $fields['recurrence_cycle'] = Subscription::getRecurrenceCycle(); + $fields['recurrence_model'] = Subscription::getRecurrenceModel(); $this->formatFieldsWhenIsSubscription($fields, $wc_order); $attempts = intval($wc_order->get_meta('_pagarme_attempts') ?? 0) + 1; $wc_order->update_meta_data("_pagarme_attempts", $attempts); diff --git a/src/Model/Subscription.php b/src/Model/Subscription.php index ceeb3b1d..e4125d5c 100644 --- a/src/Model/Subscription.php +++ b/src/Model/Subscription.php @@ -295,6 +295,7 @@ private function convertOrderObject(Order $order) $fields['card_id'] = $card['cardId']; $fields['pagarmetoken'] = $card['cardId']; $fields['recurrence_cycle'] = "subsequent"; + $fields['recurrence_model'] = self::getRecurrenceModelFromOrder($order->getWcOrder()); $fields['payment_origin'] = isset($card['chargeId']) ? ["charge_id" => $card['chargeId']] : null; } @@ -374,6 +375,90 @@ protected function getCardDataByTransaction($transactions) return $transactions->getCardData(); } + /** + * Determines whether the subscription has a fixed end date (finite billing cycles) + * @param int $productId + * @return bool + */ + private static function hasFixedEndDate(int $productId): bool + { + $length = (int) WC_Subscriptions_Product::get_length($productId); + return $length > 0; + } + + /** + * Determines the recurrence model for a subscription product + * @param int $productId + * @return string 'subscription' for fixed-duration or 'standing_order' for indefinite + */ + private static function getRecurrenceModelFromProductId(int $productId): string + { + if (self::hasFixedEndDate($productId)) { + return 'subscription'; + } + return 'standing_order'; + } + + /** + * Extracts the recurrence model from the first subscription product in the cart + * @return string|null 'subscription', 'standing_order', or null if no subscription + */ + public static function getRecurrenceModel(): ?string + { + if (!self::canProcessSubscriptions()) { + return null; + } + return self::extractRecurrenceModelFromCartItems(); + } + + /** + * Determines whether subscriptions can be processed in the current context + * @return bool + */ + private static function canProcessSubscriptions(): bool + { + return self::hasSubscriptionPlugin() && self::hasSubscriptionProductInCart(); + } + + /** + * Extracts recurrence model from the first subscription item in cart + * @return string|null + */ + private static function extractRecurrenceModelFromCartItems(): ?string + { + foreach (WC()->cart->cart_contents ?? [] as $item) { + return self::getRecurrenceModelFromProductId((int) $item['product_id']); + } + return null; + } + + /** + * Extracts the recurrence model from the first subscription product in an order + * Used when processing subscription renewals + * @param WC_Order $wcOrder + * @return string|null 'subscription', 'standing_order', or null if no subscription + */ + public static function getRecurrenceModelFromOrder(WC_Order $wcOrder): ?string + { + if (!self::hasSubscriptionPlugin()) { + return null; + } + return self::extractRecurrenceModelFromOrderItems($wcOrder); + } + + /** + * Extracts recurrence model from the first subscription item in order + * @param WC_Order $wcOrder + * @return string|null + */ + private static function extractRecurrenceModelFromOrderItems(WC_Order $wcOrder): ?string + { + foreach ($wcOrder->get_items() as $item) { + return self::getRecurrenceModelFromProductId((int) $item->get_product_id()); + } + return null; + } + /** * @return string|null */ diff --git a/tests/Model/SubscriptionRecurrenceModelTest.php b/tests/Model/SubscriptionRecurrenceModelTest.php new file mode 100644 index 00000000..259b82b2 --- /dev/null +++ b/tests/Model/SubscriptionRecurrenceModelTest.php @@ -0,0 +1,396 @@ +returnArg(0) + ->times(1); + + $reflection = new \ReflectionClass(Subscription::class); + $method = $reflection->getMethod('hasFixedEndDate'); + $method->setAccessible(true); + + $result = $method->invoke(null, $productId); + + $this->assertTrue($result); + } + + /** + * @test + * @group recurrence + */ + public function hasFixedEndDateReturnsFalseWhenProductHasNoDefinedLength() + { + $productId = 124; + + Brain\Monkey\Functions\when('WC_Subscriptions_Product::get_length') + ->returnArg(0) + ->times(1); + + $reflection = new \ReflectionClass(Subscription::class); + $method = $reflection->getMethod('hasFixedEndDate'); + $method->setAccessible(true); + + // Simulate get_length returning 0 + \Mockery::mock('overload:WC_Subscriptions_Product') + ->shouldReceive('get_length') + ->with($productId) + ->andReturn(0); + + $result = $method->invoke(null, $productId); + + $this->assertFalse($result); + } + + /** + * @test + * @group recurrence + */ + public function getRecurrenceModelFromProductIdReturnsSubscriptionWhenFixedDuration() + { + $productId = 125; + + // Mock WC_Subscriptions_Product::get_length to return 5 (fixed duration) + $wcsProductMock = Mockery::mock('overload:WC_Subscriptions_Product'); + $wcsProductMock->shouldReceive('get_length') + ->with($productId) + ->andReturn(5); + + $reflection = new \ReflectionClass(Subscription::class); + $method = $reflection->getMethod('getRecurrenceModelFromProductId'); + $method->setAccessible(true); + + $result = $method->invoke(null, $productId); + + $this->assertEquals('subscription', $result); + } + + /** + * @test + * @group recurrence + */ + public function getRecurrenceModelFromProductIdReturnsStandingOrderWhenNoFixedDuration() + { + $productId = 126; + + // Mock WC_Subscriptions_Product::get_length to return 0 (indefinite) + $wcsProductMock = Mockery::mock('overload:WC_Subscriptions_Product'); + $wcsProductMock->shouldReceive('get_length') + ->with($productId) + ->andReturn(0); + + $reflection = new \ReflectionClass(Subscription::class); + $method = $reflection->getMethod('getRecurrenceModelFromProductId'); + $method->setAccessible(true); + + $result = $method->invoke(null, $productId); + + $this->assertEquals('standing_order', $result); + } + + /** + * @test + * @group recurrence + */ + public function canProcessSubscriptionsReturnsFalseWhenPluginNotPresent() + { + Brain\Monkey\Functions\expect('class_exists') + ->with('WC_Subscriptions') + ->andReturn(false); + + $reflection = new \ReflectionClass(Subscription::class); + $method = $reflection->getMethod('canProcessSubscriptions'); + $method->setAccessible(true); + + $result = $method->invoke(null); + + $this->assertFalse($result); + } + + /** + * @test + * @group recurrence + */ + public function canProcessSubscriptionsReturnsFalseWhenNoSubscriptionInCart() + { + Brain\Monkey\Functions\expect('class_exists') + ->with('WC_Subscriptions') + ->andReturn(true); + + Brain\Monkey\Functions\expect('function_exists') + ->with('wcs_cart_contains_renewal') + ->andReturn(true); + + Brain\Monkey\Functions\expect('wcs_cart_contains_renewal') + ->andReturn(false); + + $wcsCartMock = Mockery::mock('alias:WC_Subscriptions_Cart'); + $wcsCartMock->shouldReceive('cart_contains_subscription') + ->andReturn(false); + + $reflection = new \ReflectionClass(Subscription::class); + $method = $reflection->getMethod('canProcessSubscriptions'); + $method->setAccessible(true); + + $result = $method->invoke(null); + + $this->assertFalse($result); + } + + /** + * @test + * @group recurrence + */ + public function getRecurrenceModelReturnsNullWhenPluginNotPresent() + { + Brain\Monkey\Functions\expect('class_exists') + ->with('WC_Subscriptions') + ->andReturn(false); + + $result = Subscription::getRecurrenceModel(); + + $this->assertNull($result); + } + + /** + * @test + * @group recurrence + */ + public function getRecurrenceModelReturnsNullWhenNoSubscriptionInCart() + { + Brain\Monkey\Functions\expect('class_exists') + ->with('WC_Subscriptions') + ->andReturn(true); + + Brain\Monkey\Functions\expect('function_exists') + ->with('wcs_cart_contains_renewal') + ->andReturn(true); + + Brain\Monkey\Functions\expect('wcs_cart_contains_renewal') + ->andReturn(false); + + $wcsCartMock = Mockery::mock('alias:WC_Subscriptions_Cart'); + $wcsCartMock->shouldReceive('cart_contains_subscription') + ->andReturn(false); + + $result = Subscription::getRecurrenceModel(); + + $this->assertNull($result); + } + + /** + * @test + * @group recurrence + */ + public function getRecurrenceModelReturnsSubscriptionWhenCartContainsFixedDurationProduct() + { + $productId = 127; + $cartItem = [ + 'product_id' => $productId, + 'quantity' => 1, + ]; + + Brain\Monkey\Functions\expect('class_exists') + ->with('WC_Subscriptions') + ->andReturn(true); + + Brain\Monkey\Functions\expect('function_exists') + ->with('wcs_cart_contains_renewal') + ->andReturn(true); + + Brain\Monkey\Functions\expect('wcs_cart_contains_renewal') + ->andReturn(false); + + $wcsCartMock = Mockery::mock('alias:WC_Subscriptions_Cart'); + $wcsCartMock->shouldReceive('cart_contains_subscription') + ->andReturn(true); + + Brain\Monkey\Functions\when('WC()') + ->returnArg(0); + + $wcsProductMock = Mockery::mock('overload:WC_Subscriptions_Product'); + $wcsProductMock->shouldReceive('get_length') + ->with($productId) + ->andReturn(12); // 12 billing cycles + + $wcMock = Mockery::mock(); + $wcMock->cart = Mockery::mock(); + $wcMock->cart->cart_contents = [$cartItem]; + + Brain\Monkey\Functions\when('WC()') + ->return($wcMock); + + $result = Subscription::getRecurrenceModel(); + + $this->assertEquals('subscription', $result); + } + + /** + * @test + * @group recurrence + */ + public function getRecurrenceModelReturnsStandingOrderWhenCartContainsIndefiniteProduct() + { + $productId = 128; + $cartItem = [ + 'product_id' => $productId, + 'quantity' => 1, + ]; + + Brain\Monkey\Functions\expect('class_exists') + ->with('WC_Subscriptions') + ->andReturn(true); + + Brain\Monkey\Functions\expect('function_exists') + ->with('wcs_cart_contains_renewal') + ->andReturn(true); + + Brain\Monkey\Functions\expect('wcs_cart_contains_renewal') + ->andReturn(false); + + $wcsCartMock = Mockery::mock('alias:WC_Subscriptions_Cart'); + $wcsCartMock->shouldReceive('cart_contains_subscription') + ->andReturn(true); + + $wcsProductMock = Mockery::mock('overload:WC_Subscriptions_Product'); + $wcsProductMock->shouldReceive('get_length') + ->with($productId) + ->andReturn(0); // 0 = indefinite + + $wcMock = Mockery::mock(); + $wcMock->cart = Mockery::mock(); + $wcMock->cart->cart_contents = [$cartItem]; + + Brain\Monkey\Functions\when('WC()') + ->return($wcMock); + + $result = Subscription::getRecurrenceModel(); + + $this->assertEquals('standing_order', $result); + } + + /** + * @test + * @group recurrence + */ + public function getRecurrenceModelFromOrderReturnsNullWhenPluginNotPresent() + { + $orderMock = Mockery::mock('WC_Order'); + + Brain\Monkey\Functions\expect('class_exists') + ->with('WC_Subscriptions') + ->andReturn(false); + + $result = Subscription::getRecurrenceModelFromOrder($orderMock); + + $this->assertNull($result); + } + + /** + * @test + * @group recurrence + */ + public function getRecurrenceModelFromOrderReturnsSubscriptionWhenOrderContainsFixedDurationProduct() + { + $productId = 129; + + $itemMock = Mockery::mock(); + $itemMock->shouldReceive('get_product_id') + ->andReturn($productId); + + $orderMock = Mockery::mock('WC_Order'); + $orderMock->shouldReceive('get_items') + ->andReturn([$itemMock]); + + Brain\Monkey\Functions\expect('class_exists') + ->with('WC_Subscriptions') + ->andReturn(true); + + $wcsProductMock = Mockery::mock('overload:WC_Subscriptions_Product'); + $wcsProductMock->shouldReceive('get_length') + ->with($productId) + ->andReturn(6); // 6 billing cycles + + $result = Subscription::getRecurrenceModelFromOrder($orderMock); + + $this->assertEquals('subscription', $result); + } + + /** + * @test + * @group recurrence + */ + public function getRecurrenceModelFromOrderReturnsStandingOrderWhenOrderContainsIndefiniteProduct() + { + $productId = 130; + + $itemMock = Mockery::mock(); + $itemMock->shouldReceive('get_product_id') + ->andReturn($productId); + + $orderMock = Mockery::mock('WC_Order'); + $orderMock->shouldReceive('get_items') + ->andReturn([$itemMock]); + + Brain\Monkey\Functions\expect('class_exists') + ->with('WC_Subscriptions') + ->andReturn(true); + + $wcsProductMock = Mockery::mock('overload:WC_Subscriptions_Product'); + $wcsProductMock->shouldReceive('get_length') + ->with($productId) + ->andReturn(0); // indefinite + + $result = Subscription::getRecurrenceModelFromOrder($orderMock); + + $this->assertEquals('standing_order', $result); + } + + /** + * @test + * @group recurrence + */ + public function getRecurrenceModelFromOrderReturnsNullWhenOrderHasNoItems() + { + $orderMock = Mockery::mock('WC_Order'); + $orderMock->shouldReceive('get_items') + ->andReturn([]); + + Brain\Monkey\Functions\expect('class_exists') + ->with('WC_Subscriptions') + ->andReturn(true); + + $result = Subscription::getRecurrenceModelFromOrder($orderMock); + + $this->assertNull($result); + } +} diff --git a/vendor/pagarme/ecommerce-module-core/src/Payment/Aggregates/Payments/AbstractCreditCardPayment.php b/vendor/pagarme/ecommerce-module-core/src/Payment/Aggregates/Payments/AbstractCreditCardPayment.php index a3d5dbf2..c1c64c6f 100644 --- a/vendor/pagarme/ecommerce-module-core/src/Payment/Aggregates/Payments/AbstractCreditCardPayment.php +++ b/vendor/pagarme/ecommerce-module-core/src/Payment/Aggregates/Payments/AbstractCreditCardPayment.php @@ -24,6 +24,8 @@ abstract class AbstractCreditCardPayment extends AbstractPayment /** @var string */ protected $recurrenceCycle; /** @var string */ + protected $recurrenceModel; + /** @var string */ protected $paymentOrigin; /** @var string */ protected $statementDescriptor; @@ -142,6 +144,16 @@ public function setRecurrenceCycle($recurrenceCycle) $this->recurrenceCycle = $recurrenceCycle; } + public function getRecurrenceModel() + { + return $this->recurrenceModel; + } + + public function setRecurrenceModel($recurrenceModel) + { + $this->recurrenceModel = $recurrenceModel; + } + public function getPaymentOrigin() { return $this->paymentOrigin; @@ -274,6 +286,7 @@ protected function convertToPrimitivePaymentRequest() $cardRequest->capture = $this->isCapture(); $cardRequest->installments = $this->getInstallments(); $cardRequest->recurrenceCycle = $this->getRecurrenceCycle(); + $cardRequest->recurrenceModel = $this->getRecurrenceModel(); $cardRequest->paymentOrigin = $this->getPaymentOrigin(); $cardRequest->statementDescriptor = $this->getStatementDescriptor(); if (!empty($this->getAuthentication())) { diff --git a/vendor/pagarme/ecommerce-module-core/src/Payment/Factories/PaymentFactory.php b/vendor/pagarme/ecommerce-module-core/src/Payment/Factories/PaymentFactory.php index 74f86d7f..8b32dda4 100644 --- a/vendor/pagarme/ecommerce-module-core/src/Payment/Factories/PaymentFactory.php +++ b/vendor/pagarme/ecommerce-module-core/src/Payment/Factories/PaymentFactory.php @@ -128,6 +128,7 @@ private function createBasePayments( $payment->setAmount($cardData->amount); $payment->setInstallments($cardData->installments); $payment->setRecurrenceCycle($cardData->recurrenceCycle ?? null); + $payment->setRecurrenceModel($cardData->recurrenceModel ?? null); $payment->setPaymentOrigin($cardData->paymentOrigin ?? null); if (!empty($cardData->authentication)) { $payment->setAuthentication(Authentication::createFromStdClass($cardData->authentication)); diff --git a/vendor/pagarme/pagarmecoreapi/src/Models/CreateCreditCardPaymentRequest.php b/vendor/pagarme/pagarmecoreapi/src/Models/CreateCreditCardPaymentRequest.php index 7c665ca4..ebd232ce 100644 --- a/vendor/pagarme/pagarmecoreapi/src/Models/CreateCreditCardPaymentRequest.php +++ b/vendor/pagarme/pagarmecoreapi/src/Models/CreateCreditCardPaymentRequest.php @@ -127,6 +127,13 @@ class CreateCreditCardPaymentRequest implements JsonSerializable */ public $paymentOrigin; + /** + * Defines the recurrence model for external recurrences + * @maps recurrence_model + * @var string|null $recurrenceModel public property + */ + public $recurrenceModel; + /** * Constructor to set initial or default values of member properties * @param integer $installments Initialization value for $this->installments @@ -154,10 +161,33 @@ class CreateCreditCardPaymentRequest implements JsonSerializable * >recurrenceCycle * @param string $paymentOrigin Initialization value for $this- * >paymentOrigin + * @param string $recurrenceModel Initialization value for $this- + * >recurrenceModel */ public function __construct() { switch (func_num_args()) { + case 18: + $this->installments = func_get_arg(0); + $this->statementDescriptor = func_get_arg(1); + $this->card = func_get_arg(2); + $this->cardId = func_get_arg(3); + $this->cardToken = func_get_arg(4); + $this->recurrence = func_get_arg(5); + $this->capture = func_get_arg(6); + $this->extendedLimitEnabled = func_get_arg(7); + $this->extendedLimitCode = func_get_arg(8); + $this->merchantCategoryCode = func_get_arg(9); + $this->authentication = func_get_arg(10); + $this->contactless = func_get_arg(11); + $this->autoRecovery = func_get_arg(12); + $this->operationType = func_get_arg(13); + $this->recurrencyCycle = func_get_arg(14); + $this->recurrenceCycle = func_get_arg(15); + $this->paymentOrigin = func_get_arg(16); + $this->recurrenceModel = func_get_arg(17); + break; + case 17: $this->installments = func_get_arg(0); $this->statementDescriptor = func_get_arg(1); @@ -209,6 +239,7 @@ public function jsonSerialize() $json['recurrency_cycle'] = $this->recurrencyCycle; $json['recurrence_cycle'] = $this->recurrenceCycle; $json['payment_origin'] = $this->paymentOrigin; + $json['recurrence_model'] = $this->recurrenceModel; return $json; }