Skip to content

Commit

Permalink
Merge pull request #1223 from themeum/fix-flat-discount
Browse files Browse the repository at this point in the history
🎉 Optimized Checkout Process & Implemented Proportional Flat Discount
  • Loading branch information
shewa12 authored Oct 17, 2024
2 parents f7c981c + abcfea8 commit b3e3d3d
Show file tree
Hide file tree
Showing 10 changed files with 417 additions and 447 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ function RefundModal({ title, closeModal, actions, available_amount, order_id, o
<BasicModalWrapper onClose={() => closeModal({ action: 'CLOSE' })} title={title} actions={actions}>
<form
css={styles.form}
onSubmit={form.handleSubmit((values) => {
refundOrderMutation.mutate({ ...values, order_id });
onSubmit={form.handleSubmit(async (values) => {
await refundOrderMutation.mutateAsync({ ...values, order_id });
closeModal();
})}
>
Expand Down Expand Up @@ -108,7 +108,7 @@ function RefundModal({ title, closeModal, actions, available_amount, order_id, o
<Button size="small" variant="text" onClick={() => closeModal({ action: 'CLOSE' })}>
{__('Cancel', 'tutor')}
</Button>
<Button type="submit" size="small" variant="WP">
<Button type="submit" size="small" variant="primary" loading={refundOrderMutation.isPending}>
{__('Refund', 'tutor')} {!!amount && formatPrice(amount)}
</Button>
</div>
Expand Down
285 changes: 235 additions & 50 deletions ecommerce/CheckoutController.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ class CheckoutController {
*/
const PAY_NOW_ALERT_MSG_TRANSIENT_KEY = 'tutor_pay_now_alert_msg_';

/**
* Coupon model instance.
*
* @since 3.0.0
*
* @var CouponModel
*/
public $coupon_model;

/**
* Constructor.
*
Expand All @@ -81,6 +90,8 @@ class CheckoutController {
* @return void
*/
public function __construct( $register_hooks = true ) {
$this->coupon_model = new CouponModel();

if ( $register_hooks ) {
add_action( 'tutor_action_tutor_pay_now', array( $this, 'pay_now' ) );
add_action( 'template_redirect', array( $this, 'restrict_checkout_page' ) );
Expand Down Expand Up @@ -153,10 +164,216 @@ public function ajax_get_checkout_html() {
}

/**
* Pay now ajax handler
* Prepare items
*
* @since 3.0.0
*
* @param array $item_ids items.
* @param string $order_type order type.
* @param object|null $coupon coupon.
*
* @return array
*/
private function prepare_items( $item_ids, $order_type = OrderModel::TYPE_SINGLE_ORDER, $coupon = null ) {
$items = array();
$plan_info = null;

foreach ( $item_ids as $item_id ) {
$item_name = get_the_title( $item_id );
$course_price = tutor_utils()->get_raw_course_price( $item_id );
if ( OrderModel::TYPE_SINGLE_ORDER !== $order_type ) {
$plan_info = apply_filters( 'tutor_checkout_plan_info', null, $item_id );
if ( $plan_info ) {
$item_name = $plan_info->plan_name;
$course_price->regular_price = $plan_info->regular_price;
$course_price->sale_price = $plan_info->in_sale_price ? $plan_info->sale_price : 0;
}
}

$regular_price = $course_price->regular_price;
$sale_price = $course_price->sale_price;

$item = array(
'item_id' => (int) $item_id,
'item_name' => $item_name,
'regular_price' => $regular_price,
'sale_price' => $sale_price ? $sale_price : null,
'is_coupon_applied' => false,
'coupon_code' => null,
);

$is_coupon_applicable = false;
if ( Settings::is_coupon_usage_enabled() && is_object( $coupon ) ) {
$is_coupon_applicable = ! $sale_price && $this->coupon_model->is_coupon_applicable( $coupon, $item_id );
if ( $is_coupon_applicable ) {
$item['is_coupon_applied'] = $is_coupon_applicable;
$item['coupon_code'] = $coupon->coupon_code;
}
}

$items[] = $item;
}

return array( $items, $plan_info );
}

/**
* Calculate discount.
*
* @since 3.0.0
*
* @param array $items item array.
* @param string $discount_type discount type. like percentage or fixed.
* @param float $discount_value value of discount.
*
* @return array
*/
public function calculate_discount( $items, $discount_type, $discount_value ) {
// Filter products without a sale price.
$items_without_sale = array_filter(
$items,
function( $item ) {
return empty( $item['sale_price'] );
}
);

// Calculate the total regular price of products without a sale price.
$total_regular_price = array_sum( array_column( $items_without_sale, 'regular_price' ) );

$final = array();
foreach ( $items as $item ) {
// If product already has a sale price, no discount is applied.
if ( ! empty( $item['sale_price'] ) || ( isset( $item['is_coupon_applied'] ) && ! $item['is_coupon_applied'] ) ) {
$item['discount_amount'] = 0;
$final[] = $item;
} else {
if ( 'percentage' === $discount_type ) {
// Apply percentage discount.
$discount = $item['regular_price'] * ( $discount_value / 100 );
$discount_price = $item['regular_price'] - $discount;

$item['discount_amount'] = round( $discount, 2 );
$item['discount_price'] = $discount_price;

} elseif ( 'flat' === $discount_type && $total_regular_price > 0 ) {
// Apply a proportional fixed discount based on the regular price.
$proportion = $item['regular_price'] / $total_regular_price;
$discount = $discount_value * $proportion;
$discount_price = $item['regular_price'] - $discount;

$item['discount_amount'] = round( $discount, 2 );
$item['discount_price'] = round( $discount_price, 2 );
}
$final[] = $item;
}
}

return $final;
}

/**
* Prepare checkout item with applying coupon if required.
*
* @since 3.0.0
*
* Create pending order, prepare payment data & proceed
* to payment gateway
* @param int|array $item_ids Required, course ids or plan id.
* @param string $order_type order type.
* @param string $coupon_code coupon code.
*
* @return object
*/
public function prepare_checkout_items( $item_ids, $order_type = OrderModel::TYPE_SINGLE_ORDER, $coupon_code = null ) {
$item_ids = is_array( $item_ids ) ? $item_ids : array( $item_ids );
$response = array();

$coupon_type = empty( $coupon_code ) ? 'automatic' : 'manual';
$is_coupon_applied = false;
$coupon_title = '';

$total_price = 0;
$subtotal_price = 0;
$coupon_discount = 0;
$sale_discount = 0;

$coupon = null;
$is_coupon_applied = false;
$is_meet_min_requirement = false;
$selected_coupon = null;

if ( Settings::is_coupon_usage_enabled() ) {
$selected_coupon = $this->coupon_model->get_coupon_details_for_checkout( $coupon_code );
}

$is_valid = is_object( $selected_coupon ) && $this->coupon_model->is_coupon_valid( $selected_coupon );
if ( $is_valid ) {
$is_meet_min_requirement = $this->coupon_model->is_coupon_requirement_meet( $item_ids, $selected_coupon );
if ( $is_meet_min_requirement ) {
$coupon = $selected_coupon;
$is_coupon_applied = true;
}
}

list($items, $plan_info) = $this->prepare_items( $item_ids, $order_type, $coupon );

if ( $is_coupon_applied ) {
$items = $this->calculate_discount( $items, $coupon->discount_type, $coupon->discount_amount );
$coupon_title = $coupon->coupon_title;
}

// Keep calculated price for each item.
foreach ( $items as $item ) {
$discount_amount = isset( $item['discount_amount'] ) ? $item['discount_amount'] : 0;
$has_discount_amount = $discount_amount > 0;
$item['discount_price'] = $has_discount_amount ? max( 0, $item['discount_price'] ) : null;

$display_price = isset( $item['sale_price'] ) ? $item['sale_price'] : $item['regular_price'];
$display_price = $has_discount_amount ? $item['discount_price'] : $display_price;
$item['display_price'] = $display_price;

$sale_discount_amount = isset( $item['sale_price'] ) ? $item['regular_price'] - $item['sale_price'] : 0;
$item['sale_discount_amount'] = $sale_discount_amount;

$response['items'][] = (object) $item;

$subtotal_price += $item['regular_price'];
$coupon_discount += $discount_amount;
$sale_discount += $sale_discount_amount;
}

if ( $plan_info && $plan_info->enrollment_fee > 0 ) {
$subtotal_price += $plan_info->enrollment_fee;
}

$total_price = $subtotal_price - ( $coupon_discount + $sale_discount );
$tax_rate = Tax::get_user_tax_rate();
$tax_amount = Tax::calculate_tax( $total_price, $tax_rate );

$total_price_without_tax = $total_price;
if ( ! Tax::is_tax_included_in_price() ) {
$total_price += $tax_amount;
}

// Total price should not negative.
$total_price = max( 0, $total_price );

$response['coupon_type'] = $coupon_type;
$response['coupon_title'] = $coupon_title;
$response['is_coupon_applied'] = $is_coupon_applied;

$response['subtotal_price'] = $subtotal_price;
$response['coupon_discount'] = $coupon_discount;
$response['sale_discount'] = $sale_discount;
$response['tax_rate'] = $tax_rate;
$response['total_price_without_tax'] = $total_price_without_tax;
$response['tax_amount'] = $tax_amount;
$response['total_price'] = $total_price;

return (object) $response;
}

/**
* Pay now ajax handler
* Create pending order, prepare payment data & proceed to payment gateway
*
* @since 3.0.0
*
Expand All @@ -168,11 +385,8 @@ public function pay_now() {
$errors = array();
$order_data = null;

$coupon_model = new CouponModel();
$billing_model = new BillingModel();

$billing_model = new BillingModel();
$current_user_id = get_current_user_id();

$request = Input::sanitize_array( $_POST ); //phpcs:ignore --sanitized.

$billing_fillable_fields = array_intersect_key( $request, array_flip( $billing_model->get_fillable_fields() ) );
Expand Down Expand Up @@ -216,8 +430,6 @@ public function pay_now() {

$object_ids = array_filter( explode( ',', $request['object_ids'] ), 'is_numeric' );
$coupon_code = isset( $request['coupon_code'] ) ? $request['coupon_code'] : '';
$coupon_amount = null;
$sale_discount = 0;
$payment_method = $request['payment_method'];
$payment_type = $request['payment_type'];
$order_type = $request['order_type'];
Expand All @@ -226,6 +438,8 @@ public function pay_now() {
array_push( $errors, __( 'Invalid cart items', 'tutor' ) );
}

// TODO: add a check on selected gateway is configured or not.

$billing_info = $billing_model->get_info( $current_user_id );
if ( $billing_info ) {
$update_billing = $billing_model->update( $billing_fillable_fields, array( 'user_id' => $current_user_id ) );
Expand All @@ -242,51 +456,22 @@ public function pay_now() {
}
}

$items = array();

foreach ( $object_ids as $object_id ) {
$item_raw_price = tutor_utils()->get_raw_course_price( $object_id );
if ( OrderModel::TYPE_SINGLE_ORDER !== $order_type ) {
$item_raw_price = apply_filters( 'tutor_subscription_plan_price', $item_raw_price, $object_id );
}

if ( $item_raw_price->sale_price ) {
$items[] = array(
'item_id' => $object_id,
'regular_price' => tutor_get_locale_price( $item_raw_price->regular_price ),
'sale_price' => $item_raw_price->sale_price ? tutor_get_locale_price( $item_raw_price->sale_price ) : null,
'discount_price' => null,
'coupon_code' => null,
);
} else {
$coupon_price = $coupon_code
? $coupon_model->apply_coupon_discount( $object_id, $coupon_code, $order_type )
: $coupon_model->apply_automatic_coupon_discount( $object_id, $order_type );

if ( is_object( $coupon_price ) && $coupon_price->is_applied ) {
$coupon_amount = $coupon_price->deducted_price;
}

$discount_price = ! is_null( $coupon_price->items[0]->discount_price ) && $coupon_price->items[0]->discount_price >= 0 ? tutor_get_locale_price( $coupon_price->items[0]->discount_price ) : null;

$items[] = array(
'item_id' => $object_id,
'regular_price' => tutor_get_locale_price( $item_raw_price->regular_price ),
'sale_price' => null,
'discount_price' => $discount_price,
'coupon_code' => ! is_null( $discount_price ) ? ( $coupon_code ? $coupon_code : 'automatic' ) : null,
);
}

foreach ( $items as $item ) {
$sale_discount += $item['sale_price'] > 0 ? ( $item['regular_price'] - $item['sale_price'] ) : 0;
}
$checkout_data = $this->prepare_checkout_items( $object_ids, $order_type, $coupon_code );
$items = array();
foreach ( $checkout_data->items as $item ) {
$items[] = array(
'item_id' => $item->item_id,
'regular_price' => $item->regular_price,
'sale_price' => $item->sale_price,
'discount_price' => $item->discount_price,
'coupon_code' => $item->is_coupon_applied ? $item->coupon_code : null,
);
}

$args = array(
'payment_method' => $payment_method,
'coupon_amount' => $coupon_amount,
'discount_amount' => $sale_discount,
'coupon_amount' => $checkout_data->coupon_discount,
'discount_amount' => $checkout_data->sale_discount,
);

if ( empty( $errors ) ) {
Expand Down
Loading

0 comments on commit b3e3d3d

Please sign in to comment.