diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 93d4d2b0b..e675e31dd 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -12,6 +12,10 @@ namespace WooCommerce\PayPalCommerce\ApiClient; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans; +use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer; @@ -213,6 +217,7 @@ return array( return new CatalogProducts( $container->get( 'api.host' ), $container->get( 'api.bearer' ), + $container->get('api.factory.product'), $container->get( 'woocommerce.logger.woocommerce' ) ); }, @@ -220,6 +225,8 @@ return array( return new BillingPlans( $container->get( 'api.host' ), $container->get( 'api.bearer' ), + $container->get('api.factory.billing-cycle'), + $container->get('api.factory.plan'), $container->get( 'woocommerce.logger.woocommerce' ) ); }, @@ -378,6 +385,21 @@ return array( 'api.factory.fraud-processor-response' => static function ( ContainerInterface $container ): FraudProcessorResponseFactory { return new FraudProcessorResponseFactory(); }, + 'api.factory.product' => static function(ContainerInterface $container): ProductFactory { + return new ProductFactory(); + }, + 'api.factory.billing-cycle' => static function(ContainerInterface $container): BillingCycleFactory { + return new BillingCycleFactory(); + }, + 'api.factory.payment-preferences' => static function(ContainerInterface $container):PaymentPreferencesFactory { + return new PaymentPreferencesFactory(); + }, + 'api.factory.plan' => static function(ContainerInterface $container): PlanFactory { + return new PlanFactory( + $container->get('api.factory.billing-cycle'), + $container->get('api.factory.payment-preferences') + ); + }, 'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies { return new DccApplies( $container->get( 'api.dcc-supported-country-currency-matrix' ), diff --git a/modules/ppcp-api-client/src/Endpoint/BillingPlans.php b/modules/ppcp-api-client/src/Endpoint/BillingPlans.php index 285203d06..3ae86b747 100644 --- a/modules/ppcp-api-client/src/Endpoint/BillingPlans.php +++ b/modules/ppcp-api-client/src/Endpoint/BillingPlans.php @@ -10,10 +10,12 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; use Psr\Log\LoggerInterface; -use stdClass; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Plan; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory; /** * Class BillingPlans @@ -36,6 +38,16 @@ class BillingPlans { */ private $bearer; + /** + * @var BillingCycleFactory + */ + private $billing_cycle_factory; + + /** + * @var PlanFactory + */ + private $plan_factory; + /** * The logger. * @@ -53,10 +65,14 @@ class BillingPlans { public function __construct( string $host, Bearer $bearer, + BillingCycleFactory $billing_cycle_factory, + PlanFactory $plan_factory, LoggerInterface $logger ) { $this->host = $host; $this->bearer = $bearer; + $this->billing_cycle_factory = $billing_cycle_factory; + $this->plan_factory = $plan_factory; $this->logger = $logger; } @@ -65,7 +81,7 @@ class BillingPlans { * * @param string $product_id The product id. * - * @return stdClass + * @return Plan * * @throws RuntimeException If the request fails. * @throws PayPalApiException If the request fails. @@ -74,12 +90,12 @@ class BillingPlans { string $product_id, array $billing_cycles, array $payment_preferences - ): stdClass { + ): Plan { $data = array( 'product_id' => $product_id, 'name' => 'Testing Plan', - 'billing_cycles' => array($billing_cycles), + 'billing_cycles' => $billing_cycles, 'payment_preferences' => $payment_preferences ); @@ -110,7 +126,7 @@ class BillingPlans { ); } - return $json; + return $this->plan_factory->from_paypal_response($json); } /** diff --git a/modules/ppcp-api-client/src/Endpoint/CatalogProducts.php b/modules/ppcp-api-client/src/Endpoint/CatalogProducts.php index cfbeabc56..8ed776e9d 100644 --- a/modules/ppcp-api-client/src/Endpoint/CatalogProducts.php +++ b/modules/ppcp-api-client/src/Endpoint/CatalogProducts.php @@ -10,10 +10,11 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; use Psr\Log\LoggerInterface; -use stdClass; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; +use WooCommerce\PayPalCommerce\ApiClient\Entity\Product; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory; /** * Class CatalogProduct @@ -41,6 +42,10 @@ class CatalogProducts { * @var LoggerInterface */ private $logger; + /** + * @var ProductFactory + */ + private $product_factory; /** * CatalogProducts constructor. @@ -52,26 +57,35 @@ class CatalogProducts { public function __construct( string $host, Bearer $bearer, + ProductFactory $product_factory, LoggerInterface $logger ) { $this->host = $host; $this->bearer = $bearer; + $this->product_factory = $product_factory; $this->logger = $logger; } /** * Creates a product. * - * @return stdClass + * @param string $name Product name. + * @param string $description Product description. + * + * @return Product * * @throws RuntimeException If the request fails. * @throws PayPalApiException If the request fails. */ - public function create(string $name): stdClass { + public function create(string $name, string $description): Product { $data = array( 'name' => $name, ); + if($description) { + $data['description'] = $description; + } + $bearer = $this->bearer->bearer(); $url = trailingslashit( $this->host ) . 'v1/catalogs/products'; $args = array( @@ -99,6 +113,6 @@ class CatalogProducts { ); } - return $json; + return $this->product_factory->from_paypal_response($json); } } diff --git a/modules/ppcp-api-client/src/Entity/BillingCycle.php b/modules/ppcp-api-client/src/Entity/BillingCycle.php new file mode 100644 index 000000000..138b6b3ec --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/BillingCycle.php @@ -0,0 +1,97 @@ +frequency = $frequency; + $this->sequence = $sequence; + $this->tenure_type = $tenure_type; + $this->pricing_scheme = $pricing_scheme; + $this->total_cycles = $total_cycles; + } + + /** + * @return array + */ + public function frequency(): array { + return $this->frequency; + } + + /** + * @return int + */ + public function sequence(): int { + return $this->sequence; + } + + /** + * @return string + */ + public function tenure_type(): string { + return $this->tenure_type; + } + + /** + * @return array + */ + public function pricing_scheme(): array { + return $this->pricing_scheme; + } + + /** + * @return int + */ + public function total_cycles(): int { + return $this->total_cycles; + } + + public function to_array() { + return array( + 'frequency' => $this->frequency(), + 'sequence' => $this->sequence(), + 'tenure_type' => $this->tenure_type(), + 'pricing_scheme' => $this->pricing_scheme(), + 'total_cycles' => $this->total_cycles(), + ); + } +} diff --git a/modules/ppcp-api-client/src/Entity/PaymentPreferences.php b/modules/ppcp-api-client/src/Entity/PaymentPreferences.php new file mode 100644 index 000000000..41b6b335c --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/PaymentPreferences.php @@ -0,0 +1,82 @@ +setup_fee = $setup_fee; + $this->auto_bill_outstanding = $auto_bill_outstanding; + $this->setup_fee_failure_action = $setup_fee_failure_action; + $this->payment_failure_threshold = $payment_failure_threshold; + } + + /** + * @return array + */ + public function setup_fee(): array { + return $this->setup_fee; + } + + /** + * @return bool + */ + public function auto_bill_outstanding(): bool { + return $this->auto_bill_outstanding; + } + + /** + * @return string + */ + public function setup_fee_failure_action(): string { + return $this->setup_fee_failure_action; + } + + /** + * @return int + */ + public function payment_failure_threshold(): int { + return $this->payment_failure_threshold; + } + + public function to_array():array { + return array( + 'setup_fee' => $this->setup_fee(), + 'auto_bill_outstanding' => $this->auto_bill_outstanding(), + 'setup_fee_failure_action' => $this->setup_fee_failure_action(), + 'payment_failure_threshold' => $this->payment_failure_threshold(), + ); + } +} diff --git a/modules/ppcp-api-client/src/Entity/Plan.php b/modules/ppcp-api-client/src/Entity/Plan.php new file mode 100644 index 000000000..2a756cb14 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/Plan.php @@ -0,0 +1,112 @@ +id = $id; + $this->name = $name; + $this->product_id = $product_id; + $this->billing_cycles = $billing_cycles; + $this->payment_preferences = $payment_preferences; + $this->status = $status; + } + + /** + * @return string + */ + public function id(): string { + return $this->id; + } + + /** + * @return string + */ + public function name(): string { + return $this->name; + } + + /** + * @return string + */ + public function product_id(): string { + return $this->product_id; + } + + /** + * @return array + */ + public function billing_cycles(): array { + return $this->billing_cycles; + } + + /** + * @return PaymentPreferences + */ + public function payment_preferences(): PaymentPreferences { + return $this->payment_preferences; + } + + /** + * @return string + */ + public function status(): string { + return $this->status; + } + + public function to_array():array { + return array( + 'id' => $this->id(), + 'name' => $this->name(), + 'product_id' => $this->product_id(), + 'billing_cycles' => $this->billing_cycles(), + 'payment_preferences' => $this->payment_preferences(), + 'status' => $this->status(), + ); + } +} diff --git a/modules/ppcp-api-client/src/Entity/Product.php b/modules/ppcp-api-client/src/Entity/Product.php new file mode 100644 index 000000000..f1ea29f58 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/Product.php @@ -0,0 +1,85 @@ +id = $id; + $this->name = $name; + $this->description = $description; + } + + /** + * Returns the product ID. + * + * @return string + */ + public function id(): string { + return $this->id; + } + + /** + * Returns the product name. + * + * @return string + */ + public function name(): string { + return $this->name; + } + + /** + * Returns the product description. + * + * @return string + */ + public function description(): string { + return $this->description; + } + + /** + * Returns the object as array. + * + * @return array + */ + public function to_array() { + return array( + 'id' => $this->id(), + 'name' => $this->name(), + 'description' => $this->description(), + ); + } +} diff --git a/modules/ppcp-api-client/src/Factory/BillingCycleFactory.php b/modules/ppcp-api-client/src/Factory/BillingCycleFactory.php new file mode 100644 index 000000000..5750e30c0 --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/BillingCycleFactory.php @@ -0,0 +1,53 @@ + $product->get_meta('_subscription_period'), + 'interval_count' => $product->get_meta('_subscription_period_interval'), + ), + 1, + 'REGULAR', + array( + 'fixed_price' => array( + 'value' => $product->get_meta('_subscription_price'), + 'currency_code' => 'USD', + ), + ), + (int)$product->get_meta('_subscription_length') + ); + } + + public function from_paypal_response(stdClass $data): BillingCycle { + return new BillingCycle( + array( + 'interval_unit' => $data->frequency->interval_unit, + 'interval_count' => $data->frequency->interval_count, + ), + $data->sequence, + $data->tenure_type, + array( + 'fixed_price' => array( + 'value' => $data->pricing_scheme->fixed_price->value, + 'currency_code' => $data->pricing_scheme->fixed_price->currency_code, + ), + ), + $data->total_cycles + ); + } +} diff --git a/modules/ppcp-api-client/src/Factory/PaymentPreferencesFactory.php b/modules/ppcp-api-client/src/Factory/PaymentPreferencesFactory.php new file mode 100644 index 000000000..aef93cc58 --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/PaymentPreferencesFactory.php @@ -0,0 +1,38 @@ + $product->get_meta( '_subscription_sign_up_fee' ) ?: '0', + 'currency_code' => 'USD', + ) + ); + } + + public function from_paypal_response(stdClass $data) { + return new PaymentPreferences( + array( + 'value' => $data->setup_fee->value, + 'currency_code' => $data->setup_fee->currency_code, + ), + $data->auto_bill_outstanding, + $data->setup_fee_failure_action, + $data->payment_failure_threshold + ); + } +} diff --git a/modules/ppcp-api-client/src/Factory/PlanFactory.php b/modules/ppcp-api-client/src/Factory/PlanFactory.php new file mode 100644 index 000000000..7b129ecb4 --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/PlanFactory.php @@ -0,0 +1,66 @@ +billing_cycle_factory = $billing_cycle_factory; + $this->payment_preferences_factory = $payment_preferences_factory; + } + + public function from_paypal_response(stdClass $data): Plan { + if ( ! isset( $data->id ) ) { + throw new RuntimeException( + __( 'No id for given plan', 'woocommerce-paypal-payments' ) + ); + } + if ( ! isset( $data->name ) ) { + throw new RuntimeException( + __( 'No name for plan given', 'woocommerce-paypal-payments' ) + ); + } + if ( ! isset( $data->product_id ) ) { + throw new RuntimeException( + __( 'No product id for given plan', 'woocommerce-paypal-payments' ) + ); + } + if ( ! isset( $data->billing_cycles ) ) { + throw new RuntimeException( + __( 'No billing cycles for given plan', 'woocommerce-paypal-payments' ) + ); + } + + $billing_cycles = array(); + foreach ($data->billing_cycles as $billing_cycle) { + $billing_cycles[] = $this->billing_cycle_factory->from_paypal_response($billing_cycle); + } + + $payment_preferences = $this->payment_preferences_factory->from_paypal_response($data->payment_preferences) ?? array(); + + return new Plan( + $data->id, + $data->name, + $data->product_id, + $billing_cycles, + $payment_preferences, + $data->status ?? '' + ); + } +} diff --git a/modules/ppcp-api-client/src/Factory/ProductFactory.php b/modules/ppcp-api-client/src/Factory/ProductFactory.php new file mode 100644 index 000000000..c52798e5e --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/ProductFactory.php @@ -0,0 +1,44 @@ +id ) ) { + throw new RuntimeException( + __( 'No id for product given', 'woocommerce-paypal-payments' ) + ); + } + if ( ! isset( $data->name ) ) { + throw new RuntimeException( + __( 'No name for product given', 'woocommerce-paypal-payments' ) + ); + } + + return new Product( + $data->id, + $data->name, + $data->description ?? '' + ); + } +} diff --git a/modules/ppcp-subscription/services.php b/modules/ppcp-subscription/services.php index a24002869..927419ff9 100644 --- a/modules/ppcp-subscription/services.php +++ b/modules/ppcp-subscription/services.php @@ -43,4 +43,13 @@ return array( $endpoint = $container->get( 'api.endpoint.payment-token' ); return new PaymentTokenRepository( $factory, $endpoint ); }, + 'subscription.api-handler' => static function(ContainerInterface $container): SubscriptionsApiHandler { + return new SubscriptionsApiHandler( + $container->get('api.endpoint.catalog-products'), + $container->get('api.endpoint.billing-plans'), + $container->get('api.factory.billing-cycle'), + $container->get('api.factory.payment-preferences'), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + } ); diff --git a/modules/ppcp-subscription/src/SubscriptionModule.php b/modules/ppcp-subscription/src/SubscriptionModule.php index 1a7607de6..fc030552f 100644 --- a/modules/ppcp-subscription/src/SubscriptionModule.php +++ b/modules/ppcp-subscription/src/SubscriptionModule.php @@ -11,8 +11,6 @@ namespace WooCommerce\PayPalCommerce\Subscription; use WC_Product_Subscription; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts; -use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; @@ -162,104 +160,21 @@ class SubscriptionModule implements ModuleInterface { $product = wc_get_product( $product_id ); if ( $product->get_type() === 'subscription' ) { - $billing_plans_endpoint = $c->get( 'api.endpoint.billing-plans' ); - assert( $billing_plans_endpoint instanceof BillingPlans ); + $subscriptions_api_handler = $c->get('subscription.api-handler'); + assert($subscriptions_api_handler instanceof SubscriptionsApiHandler); if ( $product->meta_exists( 'ppcp_subscription_product' ) && $product->meta_exists( 'ppcp_subscription_plan' ) ) { - if ( $product->get_meta( '_subscription_price' ) === $product->get_meta( 'ppcp_subscription_plan' )->billing_cycles[0]->pricing_scheme->fixed_price->value ) { - return; - } - - $billing_cycles = array( - 'pricing_scheme' => array( - 'fixed_price' => array( - 'value' => $product->get_meta( '_subscription_price' ), - ), - ), - ); - - try { - $billing_plans_endpoint->update_pricing( - $product->get_meta( 'ppcp_subscription_plan' )->id, - $billing_cycles - ); - } catch ( RuntimeException $exception ) { - $error = $exception->getMessage(); - if ( is_a( $exception, PayPalApiException::class ) ) { - $error = $exception->get_details( $error ); - } - - $logger = $c->get( 'woocommerce.logger.woocommerce' ); - $logger->error( 'Could not update subscription product on PayPal. ' . $error ); - } - + $subscriptions_api_handler->update_product(); + $subscriptions_api_handler->update_plan(); return; } if ( ! $product->meta_exists( 'ppcp_subscription_product' ) ) { - $products_endpoint = $c->get( 'api.endpoint.catalog-products' ); - assert( $products_endpoint instanceof CatalogProducts ); - - try { - $subscription_product = $products_endpoint->create( $product->get_title() ); - $product->update_meta_data( 'ppcp_subscription_product', $subscription_product ); - $product->save(); - } catch ( RuntimeException $exception ) { - $error = $exception->getMessage(); - if ( is_a( $exception, PayPalApiException::class ) ) { - $error = $exception->get_details( $error ); - } - - $logger = $c->get( 'woocommerce.logger.woocommerce' ); - $logger->error( 'Could not create subscription product on PayPal. ' . $error ); - } + $subscriptions_api_handler->create_product($product); } if ( $product->get_meta( 'ppcp_subscription_product' ) && ! $product->meta_exists( 'ppcp_subscription_plan' ) ) { - $billing_cycles = array( - 'frequency' => array( - 'interval_unit' => $product->get_meta( '_subscription_period' ), - 'interval_count' => $product->get_meta( '_subscription_period_interval' ), - ), - 'tenure_type' => 'REGULAR', - 'sequence' => 1, - 'total_cycles' => $product->get_meta( '_subscription_length' ), - 'pricing_scheme' => array( - 'fixed_price' => array( - 'value' => $product->get_meta( '_subscription_price' ), - 'currency_code' => 'USD', - ), - ), - ); - - $payment_preferences = array( - 'auto_bill_outstanding' => true, - 'setup_fee' => array( - 'value' => $product->get_meta( '_subscription_sign_up_fee' ) ?: '0', - 'currency_code' => 'USD', - ), - 'setup_fee_failure_action' => 'CONTINUE', - 'payment_failure_threshold' => 3, - ); - - try { - $subscription_plan = $billing_plans_endpoint->create( - $product->get_meta( 'ppcp_subscription_product' )->id, - $billing_cycles, - $payment_preferences - ); - - $product->update_meta_data( 'ppcp_subscription_plan', $subscription_plan ); - $product->save(); - } catch ( RuntimeException $exception ) { - $error = $exception->getMessage(); - if ( is_a( $exception, PayPalApiException::class ) ) { - $error = $exception->get_details( $error ); - } - - $logger = $c->get( 'woocommerce.logger.woocommerce' ); - $logger->error( 'Could not create subscription plan on PayPal. ' . $error ); - } + $subscriptions_api_handler->create_plan($product); } } }, @@ -285,8 +200,8 @@ class SubscriptionModule implements ModuleInterface { function () use ( $subscription_product, $subscription_plan ) { echo ''; if ( $subscription_product && $subscription_plan ) { - echo '

Product ID: ' . esc_attr( $subscription_product->id ) . '

'; - echo '

Plan ID: ' . esc_attr( $subscription_plan->id ) . '

'; + echo '

Product ID: ' . esc_attr( $subscription_product['id'] ?? '' ) . '

'; + echo '

Plan ID: ' . esc_attr( $subscription_plan['id'] ?? '' ) . '

'; } }, $post_type, diff --git a/modules/ppcp-subscription/src/SubscriptionsApiHandler.php b/modules/ppcp-subscription/src/SubscriptionsApiHandler.php new file mode 100644 index 000000000..bb1e5fcf8 --- /dev/null +++ b/modules/ppcp-subscription/src/SubscriptionsApiHandler.php @@ -0,0 +1,104 @@ +products_endpoint = $products_endpoint; + $this->billing_plans_endpoint = $billing_plans_endpoint; + $this->billing_cycle_factory = $billing_cycle_factory; + $this->payment_preferences_factory = $payment_preferences_factory; + $this->logger = $logger; + } + + /** + * Creates a Catalog Product and adds it as WC product meta. + * + * @param WC_Product $product + * @return void + */ + public function create_product( WC_Product $product ) { + try { + $subscription_product = $this->products_endpoint->create( $product->get_title(), $product->get_description()); + $product->update_meta_data( 'ppcp_subscription_product', $subscription_product->to_array() ); + $product->save(); + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $this->logger->error( 'Could not create subscription product on PayPal. ' . $error ); + } + } + + public function create_plan( WC_Product $product ) { + try { + $subscription_plan = $this->billing_plans_endpoint->create( + $product->get_meta( 'ppcp_subscription_product' )['id'] ?? '', + array($this->billing_cycle_factory->from_wc_product($product)->to_array()), + $this->payment_preferences_factory->from_wc_product($product)->to_array() + ); + + $product->update_meta_data( 'ppcp_subscription_plan', $subscription_plan->to_array() ); + $product->save(); + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $this->logger->error( 'Could not create subscription plan on PayPal. ' . $error ); + } + } + + public function update_product() { + + } + + public function update_plan() { + + } +}