_x( 'Partially Paid', 'Order status', 'advanced-partial-payment-or-deposit-for-woocommerce' ),
'public' => true,
'show_in_admin_status_list' => true,
'show_in_admin_all_list' => true,
'exclude_from_search' => false,
/* translators: %s: number of orders */
'label_count' => _n_noop(
'Partially Paid (%s)',
'Partially Paid (%s)',
'advanced-partial-payment-or-deposit-for-woocommerce'
),
)
);
}
/**
* Add custom statuses to WooCommerce.
*/
public function add_order_statuses( $statuses ) {
$new_statuses = array();
foreach ( $statuses as $key => $label ) {
$new_statuses[ $key ] = $label;
if ( 'wc-on-hold' === $key ) {
$new_statuses['wc-partially-paid'] = _x( 'Partially Paid', 'Order status', 'advanced-partial-payment-or-deposit-for-woocommerce' );
}
}
return $new_statuses;
}
/**
* Check if deposit is enabled for a product.
*/
public function is_deposit_enabled( $product_id ) {
$enabled = true;
// Global check
if ( apd_get_option( 'enable_deposit', 'yes' ) !== 'yes' ) {
$enabled = false;
} else {
// Product-level override
$product_enabled = get_post_meta( $product_id, '_apd_enable_deposit', true );
if ( $product_enabled === 'yes' ) {
$enabled = true;
} elseif ( $product_enabled === 'no' ) {
$enabled = false;
} else {
// Category-level check
$product = wc_get_product( $product_id );
if ( $product ) {
$categories = $product->get_category_ids();
foreach ( $categories as $cat_id ) {
$cat_enabled = get_term_meta( $cat_id, '_apd_enable_deposit', true );
if ( $cat_enabled === 'yes' ) {
$enabled = true;
break;
}
if ( $cat_enabled === 'no' ) {
$enabled = false;
break;
}
}
}
}
}
/**
* Filter whether deposit is enabled for a product after core rules are applied.
*/
return (bool) apply_filters( 'apd_deposit_enabled', $enabled, $product_id );
}
/**
* Get deposit type for a product.
*
* @return string 'fixed' or 'percentage'
*/
public function get_deposit_type( $product_id ) {
$type = '';
// Product-level
$product_type = get_post_meta( $product_id, '_apd_deposit_type', true );
if ( ! empty( $product_type ) && $product_type !== 'global' ) {
$type = $product_type;
} else {
// Category-level
$product = wc_get_product( $product_id );
if ( $product ) {
$categories = $product->get_category_ids();
foreach ( $categories as $cat_id ) {
$cat_type = get_term_meta( $cat_id, '_apd_deposit_type', true );
if ( ! empty( $cat_type ) && $cat_type !== 'global' ) {
$type = $cat_type;
break;
}
}
}
}
// Global
if ( '' === $type ) {
$type = apd_get_option( 'deposit_type', 'percentage' );
}
return (string) apply_filters( 'apd_deposit_type', $type, $product_id );
}
/**
* Get raw deposit value for a product.
*/
public function get_deposit_value( $product_id ) {
$value = null;
// Product-level
$product_value = get_post_meta( $product_id, '_apd_deposit_value', true );
if ( $product_value !== '' && $product_value !== null ) {
$product_type = get_post_meta( $product_id, '_apd_deposit_type', true );
if ( ! empty( $product_type ) && $product_type !== 'global' ) {
$value = floatval( $product_value );
}
}
if ( null === $value ) {
// Category-level
$product = wc_get_product( $product_id );
if ( $product ) {
$categories = $product->get_category_ids();
foreach ( $categories as $cat_id ) {
$cat_value = get_term_meta( $cat_id, '_apd_deposit_value', true );
$cat_type = get_term_meta( $cat_id, '_apd_deposit_type', true );
if ( $cat_value !== '' && ! empty( $cat_type ) && $cat_type !== 'global' ) {
$value = floatval( $cat_value );
break;
}
}
}
}
if ( null === $value ) {
// Global
$value = floatval( apd_get_option( 'deposit_value', 50 ) );
}
return floatval( apply_filters( 'apd_deposit_value', $value, $product_id, $this->get_deposit_type( $product_id ) ) );
}
/**
* Calculate deposit amount for a product.
*/
public function get_deposit_amount( $product_id, $price = null ) {
if ( ! $this->is_deposit_enabled( $product_id ) ) {
return 0;
}
if ( is_null( $price ) ) {
$product = wc_get_product( $product_id );
if ( ! $product ) {
return 0;
}
$price = floatval( $product->get_price() );
}
$type = $this->get_deposit_type( $product_id );
$value = $this->get_deposit_value( $product_id );
$deposit = 0;
if ( 'fixed' === $type ) {
$deposit = min( $value, $price );
} elseif ( 'percentage' === $type ) {
$value = min( $value, 100 );
$deposit = ( $price * $value ) / 100;
} elseif ( 'min_max' === $type ) {
$bounds = $this->get_min_max_bounds( $product_id, $price );
$deposit = $bounds['min'];
}
/**
* Filter the calculated deposit amount.
*/
$deposit = apply_filters( 'apd_deposit_amount', $deposit, $product_id, $price, $type, $value );
return round( $deposit, wc_get_price_decimals() );
}
/**
* Get min/max deposit bounds for a product.
*
* @param int $product_id Product ID.
* @param float|null $price Product price.
* @return array
*/
public function get_min_max_bounds( $product_id, $price = null ) {
if ( is_null( $price ) ) {
$product = wc_get_product( $product_id );
$price = $product ? floatval( $product->get_price() ) : 0;
}
$min = floatval( apd_get_option( 'min_deposit_amount', 0 ) );
$max = floatval( apd_get_option( 'max_deposit_amount', 0 ) );
$product_min = get_post_meta( $product_id, '_apd_min_deposit', true );
$product_max = get_post_meta( $product_id, '_apd_max_deposit', true );
if ( '' !== $product_min && false !== $product_min ) {
$min = floatval( $product_min );
}
if ( '' !== $product_max && false !== $product_max ) {
$max = floatval( $product_max );
}
if ( $min <= 0 ) {
$min = round( floatval( $price ) * 0.10, wc_get_price_decimals() );
}
if ( $max <= 0 || $max > $price ) {
$max = floatval( $price );
}
if ( $min > $max ) {
$min = $max;
}
return array(
'min' => round( max( 0, floatval( $min ) ), wc_get_price_decimals() ),
'max' => round( max( 0, floatval( $max ) ), wc_get_price_decimals() ),
);
}
/**
* Sanitize a custom min/max deposit amount against allowed bounds.
*
* @param int $product_id Product ID.
* @param float $custom_deposit Submitted/custom deposit.
* @param float|null $price Product price.
* @return float
*/
public function sanitize_custom_deposit( $product_id, $custom_deposit, $price = null ) {
$bounds = $this->get_min_max_bounds( $product_id, $price );
$amount = floatval( $custom_deposit );
if ( $amount < $bounds['min'] ) {
$amount = $bounds['min'];
}
if ( $amount > $bounds['max'] ) {
$amount = $bounds['max'];
}
return round( $amount, wc_get_price_decimals() );
}
/**
* Get due balance amount.
*/
public function get_due_balance( $product_id, $price = null ) {
if ( is_null( $price ) ) {
$product = wc_get_product( $product_id );
if ( ! $product ) {
return 0;
}
$price = floatval( $product->get_price() );
}
$deposit = $this->get_deposit_amount( $product_id, $price );
return round( $price - $deposit, wc_get_price_decimals() );
}
/**
* Calculate total deposit for the entire cart.
*/
public function calculate_cart_deposit() {
$summary = $this->get_cart_payment_summary();
return $summary['deposit_amount'];
}
/**
* Check if cart has any deposit items.
*/
public function cart_has_deposit() {
if ( ! WC()->cart ) {
return false;
}
foreach ( WC()->cart->get_cart() as $cart_item ) {
if ( $this->is_deposit_enabled( $cart_item['product_id'] ) ) {
$pay_deposit = isset( $cart_item['apd_pay_deposit'] ) ? $cart_item['apd_pay_deposit'] : 'yes';
if ( $pay_deposit === 'yes' ) {
return true;
}
}
}
return false;
}
/**
* Whether a cart line is being paid as a deposit.
*
* @param array $cart_item Cart item.
* @return bool
*/
public function is_cart_item_deposit( $cart_item ) {
$product_id = intval( $cart_item['product_id'] ?? 0 );
if ( ! $product_id || ! $this->is_deposit_enabled( $product_id ) ) {
return false;
}
$pay_deposit = isset( $cart_item['apd_pay_deposit'] ) ? $cart_item['apd_pay_deposit'] : 'yes';
return 'yes' === $pay_deposit;
}
/**
* Deposit payable for a whole cart line.
*
* Single source of truth for the per-line figure so the cart, the Store API and
* the totals summary can never disagree. Addons override it through the
* apd_cart_item_deposit filter — payment plans use that to charge the first
* installment of the plan the customer actually picked.
*
* @param array $cart_item Cart item.
* @return float
*/
public function get_cart_item_deposit( $cart_item ) {
$line_total = floatval( $cart_item['line_total'] ?? 0 );
if ( ! $this->is_cart_item_deposit( $cart_item ) ) {
return $line_total;
}
$product_id = intval( $cart_item['product_id'] ?? 0 );
$quantity = max( 1, intval( $cart_item['quantity'] ?? 1 ) );
$unit_price = $line_total / $quantity;
$type = $this->get_deposit_type( $product_id );
if ( 'min_max' === $type && isset( $cart_item['apd_custom_deposit'] ) ) {
$deposit = $this->sanitize_custom_deposit( $product_id, floatval( $cart_item['apd_custom_deposit'] ), $unit_price ) * $quantity;
} else {
$deposit = $this->get_deposit_amount( $product_id, $unit_price ) * $quantity;
}
/**
* Filter the deposit payable for a single cart line.
*
* @param float $deposit Deposit for the whole line.
* @param array $cart_item Cart item.
* @param float $line_total Line total.
* @param string $type Resolved deposit type for the product.
*/
$deposit = floatval( apply_filters( 'apd_cart_item_deposit', $deposit, $cart_item, $line_total, $type ) );
return round( min( max( 0, $deposit ), $line_total ), wc_get_price_decimals() );
}
/**
* Check whether a cart fee is the plugin's own balance adjustment.
*
* Older releases added the adjustment with WC_Cart::add_fee(), which derives the
* fee id from the translated label rather than from 'apd-balance-due'. The id check
* therefore never matched and the adjustment was counted as a real cart fee, which
* collapsed the cart total and re-derived a far too small deposit.
*
* @param object $fee Fee object.
* @return bool
*/
public static function is_balance_adjustment_fee( $fee ) {
if ( ! is_object( $fee ) || ! isset( $fee->id ) ) {
return false;
}
if ( 'apd-balance-due' === $fee->id ) {
return true;
}
$settings = get_option( 'apd_settings', array() );
$balance_label = $settings['due_balance_label'] ?? __( 'Due Balance', 'advanced-partial-payment-or-deposit-for-woocommerce' );
$legacy_label = sprintf(
'%1$s (%2$s)',
$balance_label,
__( 'Pay Later', 'advanced-partial-payment-or-deposit-for-woocommerce' )
);
return sanitize_title( $legacy_label ) === $fee->id;
}
/**
* Get a normalized payment summary for the current cart.
*
* @return array
*/
public function get_cart_payment_summary() {
if ( ! WC()->cart ) {
return array(
'has_deposit' => false,
'cart_subtotal' => 0,
'deposit_subtotal' => 0,
'shipping_total' => 0,
'tax_total' => 0,
'fee_total' => 0,
'deposit_only_fee_total' => 0,
'full_total' => 0,
'deposit_amount' => 0,
'balance_due' => 0,
'deposit_ratio' => 0,
);
}
$cart_subtotal = 0;
$deposit_subtotal = 0;
$has_deposit = false;
foreach ( WC()->cart->get_cart() as $cart_item ) {
$line_total = floatval( $cart_item['line_total'] ?? 0 );
$quantity = max( 1, intval( $cart_item['quantity'] ?? 1 ) );
$cart_subtotal += $line_total;
if ( $this->is_cart_item_deposit( $cart_item ) ) {
$line_deposit = $this->get_cart_item_deposit( $cart_item );
// Only a line that genuinely defers part of its price is a deposit line.
// A zero deposit (e.g. deposit type "payment plan" with no usable plan)
// or a deposit equal to the line total must be charged in full instead
// of producing a zero-value order.
if ( $line_deposit > 0 && $line_deposit < $line_total ) {
$deposit_subtotal += $line_deposit;
$has_deposit = true;
continue;
}
}
$deposit_subtotal += $line_total;
}
$shipping_total = floatval( WC()->cart->get_shipping_total() );
$tax_total = floatval( WC()->cart->get_total_tax() );
$fee_total = 0;
$deposit_only_fee_total = 0;
foreach ( WC()->cart->get_fees() as $fee ) {
$current_fee_total = isset( $fee->total ) ? floatval( $fee->total ) : ( isset( $fee->amount ) ? floatval( $fee->amount ) : 0 );
// Skip our own balance adjustment fee to avoid double-counting
if ( self::is_balance_adjustment_fee( $fee ) ) {
continue;
}
if ( class_exists( 'APD_Deposit_Fees' ) && APD_Deposit_Fees::is_partial_payment_fee( $fee ) ) {
$deposit_only_fee_total += abs( $current_fee_total );
}
$fee_total += $current_fee_total;
}
$full_total = $cart_subtotal + $shipping_total + $tax_total + $fee_total;
$deposit_ratio = $cart_subtotal > 0 ? min( 1, $deposit_subtotal / $cart_subtotal ) : 0;
$deposit_total = $deposit_subtotal;
if ( $cart_subtotal > 0 ) {
$deposit_total += ( $shipping_total * $deposit_ratio );
$deposit_total += ( $tax_total * $deposit_ratio );
$deposit_total += ( ( $fee_total - $deposit_only_fee_total ) * $deposit_ratio );
}
$deposit_total += $deposit_only_fee_total;
$deposit_total = round( min( $deposit_total, $full_total ), wc_get_price_decimals() );
$balance_due = round( max( 0, $full_total - $deposit_total ), wc_get_price_decimals() );
$summary = array(
'has_deposit' => $has_deposit,
'cart_subtotal' => round( $cart_subtotal, wc_get_price_decimals() ),
'deposit_subtotal' => round( $deposit_subtotal, wc_get_price_decimals() ),
'shipping_total' => round( $shipping_total, wc_get_price_decimals() ),
'tax_total' => round( $tax_total, wc_get_price_decimals() ),
'fee_total' => round( $fee_total, wc_get_price_decimals() ),
'deposit_only_fee_total' => round( $deposit_only_fee_total, wc_get_price_decimals() ),
'full_total' => round( $full_total, wc_get_price_decimals() ),
'deposit_amount' => $deposit_total,
'balance_due' => $balance_due,
'deposit_ratio' => $deposit_ratio,
);
return apply_filters( 'apd_cart_payment_summary', $summary, WC()->cart );
}
/**
* Check if deposit-only mode is enabled.
*
* @param int $product_id Optional product ID.
* @return bool
*/
public function is_force_deposit_enabled( $product_id = 0 ) {
if ( $product_id && ! $this->is_deposit_enabled( $product_id ) ) {
return false;
}
if ( $product_id ) {
$product_force = get_post_meta( $product_id, '_apd_force_deposit', true );
if ( 'yes' === $product_force ) {
return true;
}
if ( 'no' === $product_force ) {
return false;
}
$product = wc_get_product( $product_id );
if ( $product ) {
foreach ( $product->get_category_ids() as $cat_id ) {
$category_force = get_term_meta( $cat_id, '_apd_force_deposit', true );
if ( 'yes' === $category_force ) {
return true;
}
if ( 'no' === $category_force ) {
return false;
}
}
}
}
return apd_get_option( 'force_deposit', 'no' ) === 'yes';
}
/**
* Check if full payment is allowed.
*
* @param int $product_id Optional product ID.
* @return bool
*/
public function is_full_payment_allowed( $product_id = 0 ) {
if ( $this->is_force_deposit_enabled( $product_id ) ) {
return false;
}
return apd_get_option( 'allow_full_payment', 'yes' ) === 'yes';
}
/**
* The payment option preselected for a product: 'deposit' or 'full'.
*
* Falls back to 'deposit' whenever full payment is not offered for the product.
*
* @param int $product_id Optional product ID.
* @return string
*/
public function get_default_payment_type( $product_id = 0 ) {
$default = 'full' === apd_get_option( 'default_payment_type', 'deposit' ) && $this->is_full_payment_allowed( $product_id )
? 'full'
: 'deposit';
/**
* Filter the preselected payment option for a product.
*
* @param string $default 'deposit' or 'full'.
* @param int $product_id Product ID.
*/
$default = apply_filters( 'apd_default_payment_type', $default, $product_id );
return 'full' === $default && $this->is_full_payment_allowed( $product_id ) ? 'full' : 'deposit';
}
}