/** * Include the PayPal payment meta data required to process automatic recurring payments so that store managers can * manually set up automatic recurring payments for a customer via the Edit Subscription screen. * * @param array $payment_meta associative array of meta data required for automatic payments * @param WC_Subscription $subscription An instance of a subscription object * @return array * @since 2.0 */ public static function add_payment_meta_details($payment_meta, $subscription) { if (WCS_PayPal::are_reference_transactions_enabled()) { $payment_meta['paypal'] = array('post_meta' => array('_paypal_subscription_id' => array('value' => get_post_meta($subscription->id, '_paypal_subscription_id', true), 'label' => 'PayPal Billing Agreement ID'))); } return $payment_meta; }
/** * Allow items on PayPal Standard Subscriptions to be switch when the PayPal account supports Reference Transactions * * Because PayPal Standard does not support recurring amount or date changes, items can not be switched when the subscription is using a * profile ID for PayPal Standard. However, PayPal Reference Transactions do allow these to be updated and because switching uses the checkout * process, we can migrate a subscription from PayPal Standard to Reference Transactions when the customer switches, so we will allow that. * * @since 2.0 */ public static function can_item_be_switched($item_can_be_switch, $item, $subscription) { if (false === $item_can_be_switch && 'paypal' === $subscription->payment_method && WCS_PayPal::are_reference_transactions_enabled()) { $is_billing_agreement = wcs_is_paypal_profile_a(wcs_get_paypal_id($subscription->id), 'billing_agreement'); if ('line_item' == $item['type'] && wcs_is_product_switchable_type($item['product_id'])) { $is_product_switchable = true; } else { $is_product_switchable = false; } if ($subscription->has_status('active') && 0 !== $subscription->get_date('last_payment')) { $is_subscription_switchable = true; } else { $is_subscription_switchable = false; } // If the only reason the subscription isn't switchable is because the PayPal profile ID is not a billing agreement, allow it to be switched if (false === $is_billing_agreement && $is_product_switchable && $is_subscription_switchable) { $item_can_be_switch = true; } } return $item_can_be_switch; }
/** * Add additional feature support at the subscription level instead of just the gateway level because some subscriptions may have been * setup with PayPal Standard while others may have been setup with Billing Agreements to use with Reference Transactions. * * @since 2.0 */ public static function add_feature_support_for_subscription($is_supported, $feature, $subscription) { if ('paypal' === $subscription->payment_method && WCS_PayPal::are_credentials_set()) { $paypal_profile_id = wcs_get_paypal_id($subscription->id); $is_billing_agreement = wcs_is_paypal_profile_a($paypal_profile_id, 'billing_agreement'); if ('gateway_scheduled_payments' === $feature && $is_billing_agreement) { $is_supported = false; } elseif (in_array($feature, self::$standard_supported_features)) { if (wcs_is_paypal_profile_a($paypal_profile_id, 'out_of_date_id')) { $is_supported = false; } else { $is_supported = true; } } elseif (in_array($feature, self::$reference_transaction_supported_features)) { if ($is_billing_agreement) { $is_supported = true; } else { $is_supported = false; } } } return $is_supported; }
/** * Checks a set of args and derives an Order ID with backward compatibility for WC < 1.7 where 'custom' was the Order ID. * * @since 2.0 */ public static function get_order_id_and_key($args, $order_type = 'shop_order') { $order_id = $order_key = ''; if (isset($args['subscr_id'])) { // PayPal Standard IPN message $subscription_id = $args['subscr_id']; } elseif (isset($args['recurring_payment_id'])) { // PayPal Express Checkout IPN, most likely 'recurring_payment_suspended_due_to_max_failed_payment', for a PayPal Standard Subscription $subscription_id = $args['recurring_payment_id']; } else { $subscription_id = ''; } // First try and get the order ID by the subscription ID if (!empty($subscription_id)) { $posts = get_posts(array('numberposts' => 1, 'orderby' => 'ID', 'order' => 'ASC', 'meta_key' => '_paypal_subscription_id', 'meta_value' => $subscription_id, 'post_type' => $order_type, 'post_status' => 'any', 'suppress_filters' => true)); if (!empty($posts)) { $order_id = $posts[0]->ID; $order_key = get_post_meta($order_id, '_order_key', true); } } // Couldn't find the order ID by subscr_id, so it's either not set on the order yet or the $args doesn't have a subscr_id, either way, let's get it from the args if (empty($order_id) && isset($args['custom'])) { // WC < 1.6.5 if (is_numeric($args['custom']) && 'shop_order' == $order_type) { $order_id = $args['custom']; $order_key = $args['invoice']; } else { $order_details = json_decode($args['custom']); if (is_object($order_details)) { // WC 2.3.11+ converted the custom value to JSON, if we have an object, we've got valid JSON if ('shop_order' == $order_type) { $order_id = $order_details->order_id; $order_key = $order_details->order_key; } elseif (isset($order_details->subscription_id)) { // Subscription created with Subscriptions 2.0+ $order_id = $order_details->subscription_id; $order_key = $order_details->subscription_key; } else { // Subscription created with Subscriptions < 2.0 $subscriptions = wcs_get_subscriptions_for_order($order_details->order_id, array('order_type' => array('parent'))); if (!empty($subscriptions)) { $subscription = array_pop($subscriptions); $order_id = $subscription->id; $order_key = $subscription->order_key; } } } elseif (preg_match('/^a:2:{/', $args['custom']) && !preg_match('/[CO]:\\+?[0-9]+:"/', $args['custom']) && ($order_details = maybe_unserialize($args['custom']))) { // WC 2.0 - WC 2.3.11, only allow serialized data in the expected format, do not allow objects or anything nasty to sneak in if ('shop_order' == $order_type) { $order_id = $order_details[0]; $order_key = $order_details[1]; } else { // Subscription, but we didn't have the subscription data in old, serialized value, so we need to pull it based on the order $subscriptions = wcs_get_subscriptions_for_order($order_details[0], array('order_type' => array('parent'))); if (!empty($subscriptions)) { $subscription = array_pop($subscriptions); $order_id = $subscription->id; $order_key = $subscription->order_key; } } } else { // WC 1.6.5 - WC 2.0 or invalid data $order_id = str_replace(WCS_PayPal::get_option('invoice_prefix'), '', $args['invoice']); $order_key = $args['custom']; } } } return array('order_id' => (int) $order_id, 'order_key' => $order_key); }
/** * Do not allow subscriptions to be switched using PayPal Standard as the payment method * * @since 2.0.16 */ public static function get_available_payment_gateways($available_gateways) { if (WC_Subscriptions_Switcher::cart_contains_switches() || isset($_GET['order_id']) && wcs_order_contains_switch($_GET['order_id'])) { foreach ($available_gateways as $gateway_id => $gateway) { if ('paypal' == $gateway_id && false == WCS_PayPal::are_reference_transactions_enabled()) { unset($available_gateways[$gateway_id]); } } } return $available_gateways; }
/** * Get PayPal Args for passing to PP * * Based on the HTML Variables documented here: https://developer.paypal.com/webapps/developer/docs/classic/paypal-payments-standard/integration-guide/Appx_websitestandard_htmlvariables/#id08A6HI00JQU * * @param WC_Order $order * @return array */ public static function get_paypal_args($paypal_args, $order) { $is_payment_change = WC_Subscriptions_Change_Payment_Gateway::$is_request_to_change_payment; $order_contains_failed_renewal = false; // Payment method changes act on the subscription not the original order if ($is_payment_change) { $subscriptions = array(wcs_get_subscription($order->id)); $subscription = array_pop($subscriptions); $order = $subscription->order; // We need the subscription's total remove_filter('woocommerce_order_amount_total', 'WC_Subscriptions_Change_Payment_Gateway::maybe_zero_total', 11, 2); } else { // Otherwise the order is the $order if ($cart_item = wcs_cart_contains_failed_renewal_order_payment() || false !== WC_Subscriptions_Renewal_Order::get_failed_order_replaced_by($order->id)) { $subscriptions = wcs_get_subscriptions_for_renewal_order($order); $order_contains_failed_renewal = true; } else { $subscriptions = wcs_get_subscriptions_for_order($order); } // Only one subscription allowed per order with PayPal $subscription = array_pop($subscriptions); } if ($order_contains_failed_renewal || !empty($subscription) && $subscription->get_total() > 0 && 'yes' !== get_option(WC_Subscriptions_Admin::$option_prefix . '_turn_off_automatic_payments', 'no')) { // It's a subscription $paypal_args['cmd'] = '_xclick-subscriptions'; // Store the subscription ID in the args sent to PayPal so we can access them later $paypal_args['custom'] = wcs_json_encode(array('order_id' => $order->id, 'order_key' => $order->order_key, 'subscription_id' => $subscription->id, 'subscription_key' => $subscription->order_key)); foreach ($subscription->get_items() as $item) { if ($item['qty'] > 1) { $item_names[] = $item['qty'] . ' x ' . wcs_get_paypal_item_name($item['name']); } elseif ($item['qty'] > 0) { $item_names[] = wcs_get_paypal_item_name($item['name']); } } // translators: 1$: subscription ID, 2$: order ID, 3$: names of items, comma separated $paypal_args['item_name'] = wcs_get_paypal_item_name(sprintf(_x('Subscription %1$s (Order %2$s) - %3$s', 'item name sent to paypal', 'woocommerce-subscriptions'), $subscription->get_order_number(), $order->get_order_number(), implode(', ', $item_names))); $unconverted_periods = array('billing_period' => $subscription->billing_period, 'trial_period' => $subscription->trial_period); $converted_periods = array(); // Convert period strings into PayPay's format foreach ($unconverted_periods as $key => $period) { switch (strtolower($period)) { case 'day': $converted_periods[$key] = 'D'; break; case 'week': $converted_periods[$key] = 'W'; break; case 'year': $converted_periods[$key] = 'Y'; break; case 'month': default: $converted_periods[$key] = 'M'; break; } } $price_per_period = $subscription->get_total(); $subscription_interval = $subscription->billing_interval; $start_timestamp = $subscription->get_time('start'); $trial_end_timestamp = $subscription->get_time('trial_end'); $next_payment_timestamp = $subscription->get_time('next_payment'); $is_synced_subscription = WC_Subscriptions_Synchroniser::subscription_contains_synced_product($subscription->id); if ($is_synced_subscription) { $length_from_timestamp = $next_payment_timestamp; } elseif ($trial_end_timestamp > 0) { $length_from_timestamp = $trial_end_timestamp; } else { $length_from_timestamp = $start_timestamp; } $subscription_length = wcs_estimate_periods_between($length_from_timestamp, $subscription->get_time('end'), $subscription->billing_period); $subscription_installments = $subscription_length / $subscription_interval; $initial_payment = $is_payment_change ? 0 : $order->get_total(); if ($order_contains_failed_renewal || $is_payment_change) { if ($is_payment_change) { // Add a nonce to the order ID to avoid "This invoice has already been paid" error when changing payment method to PayPal when it was previously PayPal $suffix = '-wcscpm-' . wp_create_nonce(); } else { // Failed renewal order, append a descriptor and renewal order's ID $suffix = '-wcsfrp-' . $order->id; } // Change the 'invoice' and the 'custom' values to be for the original order (if there is one) if (false === $subscription->order) { // No original order so we need to use the subscriptions values instead $order_number = ltrim($subscription->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions')) . '-subscription'; $order_id_key = array('order_id' => $subscription->id, 'order_key' => $subscription->order_key); } else { $order_number = ltrim($subscription->order->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions')); $order_id_key = array('order_id' => $subscription->order->id, 'order_key' => $subscription->order->order_key); } $order_details = false !== $subscription->order ? $subscription->order : $subscription; // Set the invoice details to the original order's invoice but also append a special string and this renewal orders ID so that we can match it up as a failed renewal order payment later $paypal_args['invoice'] = WCS_PayPal::get_option('invoice_prefix') . $order_number . $suffix; $paypal_args['custom'] = wcs_json_encode(array_merge($order_id_key, array('subscription_id' => $subscription->id, 'subscription_key' => $subscription->order_key))); } if ($order_contains_failed_renewal) { $subscription_trial_length = 0; $subscription_installments = max($subscription_installments - $subscription->get_completed_payment_count(), 0); // If we're changing the payment date or switching subs, we need to set the trial period to the next payment date & installments to be the number of installments left } elseif ($is_payment_change || $is_synced_subscription) { $next_payment_timestamp = $subscription->get_time('next_payment'); // When the subscription is on hold if (false != $next_payment_timestamp && !empty($next_payment_timestamp)) { $trial_until = wcs_calculate_paypal_trial_periods_until($next_payment_timestamp); $subscription_trial_length = $trial_until['first_trial_length']; $converted_periods['trial_period'] = $trial_until['first_trial_period']; $second_trial_length = $trial_until['second_trial_length']; $second_trial_period = $trial_until['second_trial_period']; } else { $subscription_trial_length = 0; } // If this is a payment change, we need to account for completed payments on the number of installments owing if ($is_payment_change && $subscription_length > 0) { $subscription_installments = max($subscription_installments - $subscription->get_completed_payment_count(), 0); } } else { $subscription_trial_length = wcs_estimate_periods_between($start_timestamp, $trial_end_timestamp, $subscription->trial_period); } if ($subscription_trial_length > 0) { // Specify a free trial period $paypal_args['a1'] = $initial_payment > 0 ? $initial_payment : 0; // Trial period length $paypal_args['p1'] = $subscription_trial_length; // Trial period $paypal_args['t1'] = $converted_periods['trial_period']; // We need to use a second trial period before we have more than 90 days until the next payment if (isset($second_trial_length) && $second_trial_length > 0) { $paypal_args['a2'] = 0.01; // Alas, although it's undocumented, PayPal appears to require a non-zero value in order to allow a second trial period $paypal_args['p2'] = $second_trial_length; $paypal_args['t2'] = $second_trial_period; } } elseif ($initial_payment != $price_per_period) { // No trial period, but initial amount includes a sign-up fee and/or other items, so charge it as a separate period if (1 == $subscription_installments) { $param_number = 3; } else { $param_number = 1; } $paypal_args['a' . $param_number] = $initial_payment; // Sign Up interval $paypal_args['p' . $param_number] = $subscription_interval; // Sign Up unit of duration $paypal_args['t' . $param_number] = $converted_periods['billing_period']; } // We have a recurring payment if (!isset($param_number) || 1 == $param_number) { // Subscription price $paypal_args['a3'] = $price_per_period; // Subscription duration $paypal_args['p3'] = $subscription_interval; // Subscription period $paypal_args['t3'] = $converted_periods['billing_period']; } // Recurring payments if (1 == $subscription_installments || $initial_payment != $price_per_period && 0 == $subscription_trial_length && 2 == $subscription_installments) { // Non-recurring payments $paypal_args['src'] = 0; } else { $paypal_args['src'] = 1; if ($subscription_installments > 0) { // An initial period is being used to charge a sign-up fee if ($initial_payment != $price_per_period && 0 == $subscription_trial_length) { $subscription_installments--; } $paypal_args['srt'] = $subscription_installments; } } // Don't reattempt failed payments, instead let Subscriptions handle the failed payment $paypal_args['sra'] = 0; // Force return URL so that order description & instructions display $paypal_args['rm'] = 2; // Reattach the filter we removed earlier if ($is_payment_change) { add_filter('woocommerce_order_amount_total', 'WC_Subscriptions_Change_Payment_Gateway::maybe_zero_total', 11, 2); } } return $paypal_args; }
/** * Instantiate our custom PayPal class * * @since 2.0 */ public static function init_paypal() { require_once 'paypal/class-wcs-paypal.php'; WCS_PayPal::init(); }
/** * Return the default WC PayPal gateway's settings. * * @since 2.0 */ protected static function get_options() { self::$paypal_settings = get_option('woocommerce_paypal_settings'); return self::$paypal_settings; }
/** * Remove the invalid credentials error flag whenever a new set of API credentials are saved. * * @since 2.0 */ protected static function maybe_update_credentials_error_flag() { // Check if the API credentials are being saved - we can't do this on the 'woocommerce_update_options_payment_gateways_paypal' hook because it is triggered after 'admin_notices' if (!empty($_REQUEST['_wpnonce']) && wp_verify_nonce($_REQUEST['_wpnonce'], 'woocommerce-settings') && isset($_POST['woocommerce_paypal_api_username']) || isset($_POST['woocommerce_paypal_api_password']) || isset($_POST['woocommerce_paypal_api_signature'])) { $credentials_updated = false; if (isset($_POST['woocommerce_paypal_api_username']) && WCS_PayPal::get_option('api_username') != $_POST['woocommerce_paypal_api_username']) { $credentials_updated = true; } elseif (isset($_POST['woocommerce_paypal_api_password']) && WCS_PayPal::get_option('api_password') != $_POST['woocommerce_paypal_api_password']) { $credentials_updated = true; } elseif (isset($_POST['woocommerce_paypal_api_signature']) && WCS_PayPal::get_option('api_signature') != $_POST['woocommerce_paypal_api_signature']) { $credentials_updated = true; } if ($credentials_updated) { delete_option('wcs_paypal_credentials_error'); } } do_action('wcs_paypal_admin_update_credentials'); }
/** * Performs an Express Checkout NVP API operation as passed in $api_method. * * Although the PayPal Standard API provides no facility for cancelling a subscription, the PayPal * Express Checkout NVP API can be used. * * @since 1.1 */ public static function change_subscription_status($profile_id, $new_status, $order = null) { _deprecated_function(__METHOD__, '2.0', 'WCS_PayPal::get_api()->manage_recurring_payments_profile_status()'); return WCS_PayPal::get_api()->manage_recurring_payments_profile_status($profile_id, $new_status, $order); }
/** * Supposed to return the main gatewya plugin class, but we don't have one of those * * @see \WCS_SV_API_Base::get_plugin() * @return object * @since 2.0 */ protected function get_plugin() { return WCS_PayPal::instance(); }
/** * If changing a subscriptions payment method from and to PayPal, the cancelled subscription hook was removed in * @see self::maybe_remove_cancelled_subscription_hook() so we want to add it again for other subscriptions. * * @since 2.0 */ public static function maybe_reattach_subscription_cancelled_callback($subscription, $new_payment_method, $old_payment_method) { if ('paypal' == $new_payment_method && 'paypal' == $old_payment_method && !WCS_PayPal::are_reference_transactions_enabled()) { add_action('woocommerce_subscription_cancelled_paypal', 'WCS_PayPal_Status_Manager::cancel_subscription'); } }
/** * Set up the payment details for a DoExpressCheckoutPayment or DoReferenceTransaction request * * @since 2.0.9 * @param WC_Order $order order object * @param string $type the type of transaction for the payment * @param bool $use_deprecated_params whether to use deprecated PayPal NVP parameters (required for DoReferenceTransaction API calls) */ protected function add_payment_details_parameters(WC_Order $order, $type, $use_deprecated_params = false) { $calculated_total = 0; $order_subtotal = 0; $item_count = 0; $order_items = array(); // add line items foreach ($order->get_items() as $item) { $product = new WC_Product($item['product_id']); $order_items[] = array('NAME' => wcs_get_paypal_item_name($product->get_title()), 'DESC' => $this->get_item_description($item, $product), 'AMT' => $this->round($order->get_item_subtotal($item)), 'QTY' => !empty($item['qty']) ? absint($item['qty']) : 1, 'ITEMURL' => $product->get_permalink()); $order_subtotal += $item['line_total']; } // add fees foreach ($order->get_fees() as $fee) { $order_items[] = array('NAME' => wcs_get_paypal_item_name($fee['name']), 'AMT' => $this->round($fee['line_total']), 'QTY' => 1); $order_subtotal += $fee['line_total']; } // add discounts if ($order->get_total_discount() > 0) { $order_items[] = array('NAME' => __('Total Discount', 'woocommerce-subscriptions'), 'QTY' => 1, 'AMT' => -$this->round($order->get_total_discount())); } if ($this->skip_line_items($order)) { $total_amount = $this->round($order->get_total()); // calculate the total as PayPal would $calculated_total += $this->round($order_subtotal + $order->get_cart_tax()) + $this->round($order->get_total_shipping() + $order->get_shipping_tax()); // offset the discrepency between the WooCommerce cart total and PayPal's calculated total by adjusting the order subtotal if ($total_amount !== $calculated_total) { $order_subtotal = $order_subtotal - ($calculated_total - $total_amount); } $item_names = array(); foreach ($order_items as $item) { $item_names[] = sprintf('%1$s x %2$s', $item['NAME'], $item['QTY']); } // add a single item for the entire order $this->add_line_item_parameters(array('NAME' => sprintf(__('%s - Order', 'woocommerce-subscriptions'), get_option('blogname')), 'DESC' => wcs_get_paypal_item_name(implode(', ', $item_names)), 'AMT' => $this->round($order_subtotal + $order->get_cart_tax()), 'QTY' => 1), 0, $use_deprecated_params); // add order-level parameters // - Do not sent the TAXAMT due to rounding errors if ($use_deprecated_params) { $this->add_parameters(array('AMT' => $total_amount, 'CURRENCYCODE' => $order->get_order_currency(), 'ITEMAMT' => $this->round($order_subtotal + $order->get_cart_tax()), 'SHIPPINGAMT' => $this->round($order->get_total_shipping() + $order->get_shipping_tax()), 'INVNUM' => WCS_PayPal::get_option('invoice_prefix') . wcs_str_to_ascii(ltrim($order->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions'))), 'PAYMENTACTION' => $type, 'PAYMENTREQUESTID' => $order->id, 'CUSTOM' => json_encode(array('order_id' => $order->id, 'order_key' => $order->order_key)))); } else { $this->add_payment_parameters(array('AMT' => $total_amount, 'CURRENCYCODE' => $order->get_order_currency(), 'ITEMAMT' => $this->round($order_subtotal + $order->get_cart_tax()), 'SHIPPINGAMT' => $this->round($order->get_total_shipping() + $order->get_shipping_tax()), 'INVNUM' => WCS_PayPal::get_option('invoice_prefix') . wcs_str_to_ascii(ltrim($order->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions'))), 'PAYMENTACTION' => $type, 'PAYMENTREQUESTID' => $order->id, 'CUSTOM' => json_encode(array('order_id' => $order->id, 'order_key' => $order->order_key)))); } } else { // add individual order items foreach ($order_items as $item) { $this->add_line_item_parameters($item, $item_count++, $use_deprecated_params); $calculated_total += $this->round($item['AMT'] * $item['QTY']); } // add shipping and tax to calculated total $calculated_total += $this->round($order->get_total_shipping()) + $this->round($order->get_total_tax()); $total_amount = $this->round($order->get_total()); // add order-level parameters if ($use_deprecated_params) { $this->add_parameters(array('AMT' => $total_amount, 'CURRENCYCODE' => $order->get_order_currency(), 'ITEMAMT' => $this->round($order_subtotal), 'SHIPPINGAMT' => $this->round($order->get_total_shipping()), 'TAXAMT' => $this->round($order->get_total_tax()), 'INVNUM' => WCS_PayPal::get_option('invoice_prefix') . wcs_str_to_ascii(ltrim($order->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions'))), 'PAYMENTACTION' => $type, 'PAYMENTREQUESTID' => $order->id, 'CUSTOM' => json_encode(array('order_id' => $order->id, 'order_key' => $order->order_key)))); } else { $this->add_payment_parameters(array('AMT' => $total_amount, 'CURRENCYCODE' => $order->get_order_currency(), 'ITEMAMT' => $this->round($order_subtotal), 'SHIPPINGAMT' => $this->round($order->get_total_shipping()), 'TAXAMT' => $this->round($order->get_total_tax()), 'INVNUM' => WCS_PayPal::get_option('invoice_prefix') . wcs_str_to_ascii(ltrim($order->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions'))), 'PAYMENTACTION' => $type, 'PAYMENTREQUESTID' => $order->id, 'CUSTOM' => json_encode(array('order_id' => $order->id, 'order_key' => $order->order_key)))); } // offset the discrepency between the WooCommerce cart total and PayPal's calculated total by adjusting the cost of the first item if ($total_amount !== $calculated_total) { $this->parameters['L_PAYMENTREQUEST_0_AMT0'] = $this->parameters['L_PAYMENTREQUEST_0_AMT0'] - ($calculated_total - $total_amount); } } }