WebhookHandlers
4 months ago
APIClass.php
3 years ago
Helpers.php
1 month ago
PaymentHelpers.php
3 months ago
Stripe.php
2 months ago
WebhookHelpers.php
8 months ago
index.php
3 years ago
PaymentHelpers.php
387 lines
| 1 | <?php |
| 2 | |
| 3 | namespace ProfilePress\Core\Membership\PaymentMethods\Stripe; |
| 4 | |
| 5 | use ProfilePress\Core\Classes\ExtensionManager; |
| 6 | use ProfilePress\Core\Membership\CheckoutFields; |
| 7 | use ProfilePress\Core\Membership\Models\Customer\CustomerEntity; |
| 8 | use ProfilePress\Core\Membership\Models\Plan\PlanFactory; |
| 9 | use ProfilePress\Core\Membership\Models\Subscription\SubscriptionBillingFrequency; |
| 10 | use ProfilePress\Core\Membership\Models\Subscription\SubscriptionEntity; |
| 11 | use ProfilePress\Core\Membership\Models\Subscription\SubscriptionTrialPeriod; |
| 12 | use ProfilePress\Core\Membership\Services\Calculator; |
| 13 | use ProfilePressVendor\Carbon\CarbonImmutable; |
| 14 | |
| 15 | class PaymentHelpers |
| 16 | { |
| 17 | public static function add_coupon_to_bucket($stripe_coupon_id) |
| 18 | { |
| 19 | $data = \get_option('ppress_stripe_coupon_bucket', []); |
| 20 | $data[$stripe_coupon_id] = time() + (24 * HOUR_IN_SECONDS); |
| 21 | \update_option('ppress_stripe_coupon_bucket', $data); |
| 22 | } |
| 23 | |
| 24 | public static function empty_coupon_bucket() |
| 25 | { |
| 26 | $data = \get_option('ppress_stripe_coupon_bucket', []); |
| 27 | |
| 28 | if ( ! empty($data)) { |
| 29 | |
| 30 | foreach ($data as $coupon_id => $time) { |
| 31 | if (time() >= $time) { |
| 32 | self::delete_coupon($coupon_id); |
| 33 | unset($data[$coupon_id]); |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | \update_option('ppress_stripe_coupon_bucket', $data); |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | public static function delete_coupon($stripe_coupon_id) |
| 42 | { |
| 43 | try { |
| 44 | APIClass::stripeClient()->coupons->delete($stripe_coupon_id); |
| 45 | } catch (\Exception $e) { |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * @return string |
| 51 | */ |
| 52 | public static function get_signup_fee_label() |
| 53 | { |
| 54 | return apply_filters('ppress_stripe_signup_fee_label', __('Setup Fee', 'wp-user-avatar')); |
| 55 | } |
| 56 | |
| 57 | public static function stripe_amount_to_ppress_amount($amount, $currency = '') |
| 58 | { |
| 59 | if ( ! self::is_zero_decimal_currency($currency)) { |
| 60 | $amount = ppress_cent_to_decimal($amount); |
| 61 | } |
| 62 | |
| 63 | return $amount; |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * @param string $freeTrial |
| 68 | * |
| 69 | * @return int |
| 70 | */ |
| 71 | public static function free_trial_days_count($freeTrial) |
| 72 | { |
| 73 | switch ($freeTrial) { |
| 74 | case SubscriptionTrialPeriod::THREE_DAYS: |
| 75 | return 3; |
| 76 | case SubscriptionTrialPeriod::FIVE_DAYS: |
| 77 | return 5; |
| 78 | case SubscriptionTrialPeriod::ONE_WEEK: |
| 79 | return 7; |
| 80 | case SubscriptionTrialPeriod::TWO_WEEKS: |
| 81 | return 14; |
| 82 | case SubscriptionTrialPeriod::THREE_WEEKS: |
| 83 | return 21; |
| 84 | case SubscriptionTrialPeriod::ONE_MONTH: |
| 85 | return CarbonImmutable::now('UTC')->diffInDays( |
| 86 | CarbonImmutable::now('UTC')->addMonth() |
| 87 | ); |
| 88 | } |
| 89 | |
| 90 | return 0; |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * @param SubscriptionEntity $subscription |
| 95 | * |
| 96 | * @return mixed |
| 97 | * |
| 98 | * @throws \Exception |
| 99 | */ |
| 100 | public static function get_product_price($subscription, $statement_descriptor) |
| 101 | { |
| 102 | switch ($subscription->billing_frequency) { |
| 103 | |
| 104 | case SubscriptionBillingFrequency::DAILY : |
| 105 | $frequency = 1; |
| 106 | $period = 'day'; |
| 107 | break; |
| 108 | case SubscriptionBillingFrequency::MONTHLY : |
| 109 | $frequency = 1; |
| 110 | $period = 'month'; |
| 111 | break; |
| 112 | case SubscriptionBillingFrequency::WEEKLY : |
| 113 | $frequency = 1; |
| 114 | $period = 'week'; |
| 115 | break; |
| 116 | case SubscriptionBillingFrequency::QUARTERLY : |
| 117 | $frequency = 3; |
| 118 | $period = 'month'; |
| 119 | break; |
| 120 | case SubscriptionBillingFrequency::EVERY_6_MONTHS : |
| 121 | $frequency = 6; |
| 122 | $period = 'month'; |
| 123 | break; |
| 124 | case SubscriptionBillingFrequency::YEARLY : |
| 125 | $frequency = 1; |
| 126 | $period = 'year'; |
| 127 | break; |
| 128 | default : |
| 129 | $frequency = 1; |
| 130 | $period = $subscription->billing_frequency; |
| 131 | break; |
| 132 | } |
| 133 | |
| 134 | $frequency = apply_filters('ppress_stripe_billing_frequency_interval', $frequency, $subscription->billing_frequency, $subscription); |
| 135 | $period = apply_filters('ppress_stripe_billing_period_interval_count', $period, $subscription->billing_frequency, $subscription); |
| 136 | |
| 137 | $stripe_product_id = 'ppress_prod_' . ppress_md5($subscription->plan_id); |
| 138 | |
| 139 | $plan = PlanFactory::fromId($subscription->plan_id); |
| 140 | |
| 141 | $is_recurring = $plan->is_auto_renew(); |
| 142 | |
| 143 | try { |
| 144 | |
| 145 | APIClass::stripeClient()->products->retrieve($stripe_product_id); |
| 146 | |
| 147 | } catch (\Exception $e) { |
| 148 | |
| 149 | $create_product_args = [ |
| 150 | 'id' => $stripe_product_id, |
| 151 | 'name' => $plan->name, |
| 152 | 'description' => strip_tags(sanitize_textarea_field($plan->description)), |
| 153 | 'metadata' => [ |
| 154 | 'plan_id' => $plan->id, |
| 155 | 'caller' => __CLASS__ . '|' . __METHOD__ . '|' . __LINE__ . '|' . PPRESS_VERSION_NUMBER, |
| 156 | ] |
| 157 | ]; |
| 158 | |
| 159 | if ( ! empty($statement_descriptor)) { |
| 160 | $create_product_args['statement_descriptor'] = $statement_descriptor; |
| 161 | } |
| 162 | |
| 163 | $create_product_args = array_filter(apply_filters('ppress_stripe_create_product_args', $create_product_args, $subscription)); |
| 164 | |
| 165 | try { |
| 166 | APIClass::stripeClient()->products->create($create_product_args)->toArray(); |
| 167 | } catch (\Exception $e) { |
| 168 | |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | $price_args = [ |
| 173 | 'currency' => ppress_get_currency(), |
| 174 | 'product' => $stripe_product_id, |
| 175 | 'unit_amount' => self::process_amount($subscription->get_recurring_amount()), |
| 176 | ]; |
| 177 | |
| 178 | if ( ! $plan->is_auto_renew()) { |
| 179 | $price_args['unit_amount'] = self::process_amount($subscription->initial_amount); |
| 180 | } |
| 181 | |
| 182 | if ($is_recurring === true) { |
| 183 | |
| 184 | $price_args['recurring'] = array( |
| 185 | 'interval' => $period, |
| 186 | 'interval_count' => $frequency |
| 187 | ); |
| 188 | } |
| 189 | |
| 190 | $price_args = apply_filters('ppress_stripe_create_price_args', $price_args, $subscription, $plan); |
| 191 | |
| 192 | $stripe_price_id = ppress_md5(wp_json_encode($price_args)); |
| 193 | |
| 194 | $price_args['metadata'] = [ |
| 195 | 'ppress_price_id' => $stripe_price_id, |
| 196 | 'caller' => __CLASS__ . '|' . __METHOD__ . '|' . __LINE__ . '|' . PPRESS_VERSION_NUMBER, |
| 197 | ]; |
| 198 | |
| 199 | $price_search_args = [ |
| 200 | 'product' => $stripe_product_id, |
| 201 | 'type' => $is_recurring ? 'recurring' : 'one_time', |
| 202 | ]; |
| 203 | |
| 204 | if ($is_recurring) { |
| 205 | $price_search_args['recurring'] = array('interval' => $period); |
| 206 | } |
| 207 | |
| 208 | $price_search_args = apply_filters('ppress_stripe_price_search_args', $price_search_args, $subscription, $plan); |
| 209 | |
| 210 | $stripe_prices = APIClass::stripeClient()->prices->all($price_search_args)->toArray(); |
| 211 | |
| 212 | if ( ! empty($stripe_prices['data']) && is_array($stripe_prices['data'])) { |
| 213 | |
| 214 | foreach ($stripe_prices['data'] as $price) { |
| 215 | if (isset($price['metadata']['ppress_price_id']) && $price['metadata']['ppress_price_id'] == $stripe_price_id) { |
| 216 | return [ |
| 217 | 'stripe_product_id' => $stripe_product_id, |
| 218 | 'stripe_price_id' => $price['id'] |
| 219 | ]; |
| 220 | } |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | try { |
| 225 | $created_price = APIClass::stripeClient()->prices->create($price_args)->toArray(); |
| 226 | } catch (\Exception $e) { |
| 227 | throw new \Exception($e->getMessage(), $e->getCode()); |
| 228 | } |
| 229 | |
| 230 | return [ |
| 231 | 'stripe_product_id' => $stripe_product_id, |
| 232 | 'stripe_price_id' => $created_price['id'] |
| 233 | ]; |
| 234 | } |
| 235 | |
| 236 | /** |
| 237 | * @param CustomerEntity $customer |
| 238 | * |
| 239 | * @return int |
| 240 | * |
| 241 | * @throws \Exception |
| 242 | */ |
| 243 | public static function get_stripe_customer_id($customer) |
| 244 | { |
| 245 | try { |
| 246 | |
| 247 | $search_result = APIClass::stripeClient()->customers->search([ |
| 248 | 'query' => sprintf('email:\'%s\' AND metadata[\'ppress_customer_id\']:\'%s\'', $customer->get_email(), $customer->id) |
| 249 | ])->toArray(); |
| 250 | |
| 251 | if ( ! empty($search_result['data']) && isset($search_result['data'][0]['id'])) { |
| 252 | return $search_result['data'][0]['id']; |
| 253 | } |
| 254 | |
| 255 | } catch (\Exception $e) { |
| 256 | |
| 257 | $stripe_customer_id = $customer->get_meta('stripe_customer_id'); |
| 258 | |
| 259 | if ( ! empty($stripe_customer_id)) return $stripe_customer_id; |
| 260 | } |
| 261 | |
| 262 | $pp_customer_billing = $customer->get_billing_details(); |
| 263 | |
| 264 | $create_customer_args = [ |
| 265 | 'email' => $customer->get_email(), |
| 266 | 'name' => $customer->get_name() |
| 267 | ]; |
| 268 | |
| 269 | if ( ! empty($pp_customer_billing)) { |
| 270 | |
| 271 | $create_customer_args['address'] = []; |
| 272 | |
| 273 | if ( ! empty($pp_customer_billing[CheckoutFields::BILLING_CITY])) { |
| 274 | $create_customer_args['address']['city'] = $pp_customer_billing[CheckoutFields::BILLING_CITY]; |
| 275 | } |
| 276 | |
| 277 | if ( ! empty($pp_customer_billing[CheckoutFields::BILLING_COUNTRY])) { |
| 278 | $create_customer_args['address']['country'] = $pp_customer_billing[CheckoutFields::BILLING_COUNTRY]; |
| 279 | } |
| 280 | |
| 281 | if ( ! empty($pp_customer_billing[CheckoutFields::BILLING_ADDRESS])) { |
| 282 | $create_customer_args['address']['line1'] = $pp_customer_billing[CheckoutFields::BILLING_ADDRESS]; |
| 283 | } |
| 284 | |
| 285 | if ( ! empty($pp_customer_billing[CheckoutFields::BILLING_POST_CODE])) { |
| 286 | $create_customer_args['address']['postal_code'] = $pp_customer_billing[CheckoutFields::BILLING_POST_CODE]; |
| 287 | } |
| 288 | |
| 289 | if ( ! empty($pp_customer_billing[CheckoutFields::BILLING_STATE])) { |
| 290 | $create_customer_args['address']['state'] = $pp_customer_billing[CheckoutFields::BILLING_STATE]; |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | $create_customer_args['metadata'] = apply_filters('ppress_stripe_customer_metadata', [ |
| 295 | 'ppress_customer_id' => $customer->id, |
| 296 | 'caller' => __CLASS__ . '|' . __METHOD__ . '|' . __LINE__ . '|' . PPRESS_VERSION_NUMBER, |
| 297 | ], $customer, $create_customer_args); |
| 298 | |
| 299 | $create_customer_args = apply_filters('ppress_create_customer_args', $create_customer_args, $customer); |
| 300 | |
| 301 | $created_customer = APIClass::stripeClient()->customers->create($create_customer_args)->toArray(); |
| 302 | |
| 303 | $stripe_customer_id = ppress_var($created_customer, 'id'); |
| 304 | |
| 305 | $customer->update_meta('stripe_customer_id', $stripe_customer_id); |
| 306 | |
| 307 | return $stripe_customer_id; |
| 308 | } |
| 309 | |
| 310 | public static function is_zero_decimal_currency($currency = '') |
| 311 | { |
| 312 | if (empty($currency)) { |
| 313 | $currency = ppress_get_currency(); |
| 314 | } |
| 315 | |
| 316 | $currency = strtolower($currency); |
| 317 | |
| 318 | $currencies = [ |
| 319 | 'bif', |
| 320 | 'clp', |
| 321 | 'djf', |
| 322 | 'gnf', |
| 323 | 'jpy', |
| 324 | 'kmf', |
| 325 | 'krw', |
| 326 | 'mga', |
| 327 | 'pyg', |
| 328 | 'rwf', |
| 329 | 'ugx', |
| 330 | 'vnd', |
| 331 | 'vuv', |
| 332 | 'xaf', |
| 333 | 'xof', |
| 334 | 'xpf', |
| 335 | ]; |
| 336 | |
| 337 | return in_array($currency, $currencies, true); |
| 338 | } |
| 339 | |
| 340 | public static function process_amount($price, $currency = '') |
| 341 | { |
| 342 | if ( ! self::is_zero_decimal_currency($currency)) { |
| 343 | $price = Calculator::init($price)->toScale(2)->multipliedBy(100)->val(); |
| 344 | } |
| 345 | |
| 346 | return Calculator::init($price)->toScale(0)->val(); |
| 347 | } |
| 348 | |
| 349 | public static function application_fee_percent() |
| 350 | { |
| 351 | return 2; |
| 352 | } |
| 353 | |
| 354 | public static function application_fee_amount($order_total) |
| 355 | { |
| 356 | return self::process_amount( |
| 357 | Calculator::init($order_total)->multipliedBy('0.02')->val() |
| 358 | ); |
| 359 | } |
| 360 | |
| 361 | public static function has_application_fee() |
| 362 | { |
| 363 | if (ExtensionManager::is_premium()) return false; |
| 364 | |
| 365 | $account_country = ppress_business_country('US'); |
| 366 | |
| 367 | /** |
| 368 | * Do not add a fee if account country does not support application fees. |
| 369 | * @see https://stripe.com/docs/connect/direct-charges#collecting-fees |
| 370 | * @see https://groups.google.com/a/lists.stripe.com/g/api-discuss/c/-Ezjn3roCiI/m/MYUpA4kUAQAJ |
| 371 | */ |
| 372 | $disallowed_list = [ |
| 373 | 'br' |
| 374 | /** @see https://stripe.com/docs/connect/direct-charges#collecting-fees */, |
| 375 | 'in', |
| 376 | // Error: Stripe doesn't currently support application fees for platforms in US with connected accounts in IN|MY|MX |
| 377 | 'mx', |
| 378 | 'my' |
| 379 | ]; |
| 380 | |
| 381 | if (in_array(strtolower($account_country), $disallowed_list, true)) { |
| 382 | return false; |
| 383 | } |
| 384 | |
| 385 | return true; |
| 386 | } |
| 387 | } |