max( 0, (int) $v ), $weights ); $total = array_sum( $weights ); $amount = max( 0, min( $amount, $total ) ); $out = array_fill_keys( array_keys( $weights ), 0 ); if ( ! $total || ! $amount ) { return $out; } $fractions = array(); foreach ( $weights as $key => $weight ) { $exact = $amount * ( $weight / $total ); $out[ $key ] = (int) floor( $exact ); $fractions[ $key ] = $exact - $out[ $key ]; } arsort( $fractions, SORT_NUMERIC ); $left = $amount - array_sum( $out ); foreach ( $fractions as $key => $fraction ) { if ( $left <= 0 ) { break; } if ( $out[ $key ] < $weights[ $key ] ) { ++$out[ $key ]; --$left; } } return $out; } public static function eligible( array $line, array $rule ): bool { $ids = array( (int) $line['product_id'], (int) ( $line['variation_id'] ?? 0 ) ); $cats = $line['categories'] ?? array(); if ( ! empty( $line['gift'] ) || ( ! empty( $rule['exclude_sale'] ) && ! empty( $line['sale'] ) ) ) { return false; } foreach ( array( 'products' => $ids, 'categories' => $cats ) as $field => $values ) { if ( ! empty( $rule[ 'exclude_' . $field ] ) && array_intersect( $values, $rule[ 'exclude_' . $field ] ) ) { return false; } if ( ! empty( $rule[ $field ] ) && ! array_intersect( $values, $rule[ $field ] ) ) { return false; } } return (bool) apply_filters( 'digidiscount_line_eligible', true, $line, $rule ); } public static function candidate( array $lines, array $rule, array $context ): array { $empty = array( 'discounts' => array(), 'shipping' => false, 'gifts' => array() ); if ( empty( $rule['enabled'] ) || ( ! empty( $rule['currency'] ) && $rule['currency'] !== $context['currency'] ) ) { return $empty; } $now = $context['now'] ?? time(); if ( ( ! empty( $rule['start'] ) && $now < $rule['start'] ) || ( ! empty( $rule['end'] ) && $now >= $rule['end'] ) ) { return $empty; } if ( empty( $rule['allow_coupons'] ) && ! empty( $context['coupons'] ) ) { return $empty; } $eligible = array_filter( $lines, static fn( $line ) => self::eligible( $line, $rule ) ); $scope = 'cart' === ( $rule['threshold_scope'] ?? 'eligible' ) ? array_filter( $lines, static fn( $line ) => empty( $line['gift'] ) ) : $eligible; $total = array_sum( array_column( $scope, 'total' ) ); $quantity = array_sum( array_column( $scope, 'qty' ) ); if ( ! $eligible || $total < (int) ( $rule['min_total'] ?? 0 ) || $quantity < ( $rule['min_qty'] ?? 0 ) || ( ! empty( $rule['max_qty'] ) && $quantity > $rule['max_qty'] ) ) { return $empty; } if ( ! apply_filters( 'digidiscount_rule_matches', true, $rule, $lines, $context ) ) { return $empty; } $weights = array_column( $eligible, 'total', 'key' ); if ( in_array( $rule['type'], array( 'product', 'category', 'cart', 'threshold' ), true ) ) { $discounts = self::basic( $eligible, $rule ); $result = array( 'discounts' => $discounts, 'shipping' => false, 'gifts' => array() ); } else { $result = apply_filters( 'digidiscount_rule_result', $empty, $rule, $eligible, $lines, $context ); } foreach ( $result['discounts'] as $key => $discount ) { $result['discounts'][ $key ] = min( max( 0, (int) $discount ), $weights[ $key ] ?? 0 ); } if ( ! empty( $rule['max_discount'] ) && array_sum( $result['discounts'] ) > $rule['max_discount'] ) { $result['discounts'] = self::allocate( (int) $rule['max_discount'], $result['discounts'] ); } return $result; } public static function basic( array $lines, array $rule ): array { $weights = array_column( $lines, 'total', 'key' ); $amount = max( 0, (float) $rule['amount'] ); $discounts = array(); if ( 'fixed' === $rule['kind'] ) { return self::allocate( (int) round( $amount ), $weights ); } foreach ( $lines as $line ) { $discounts[ $line['key'] ] = 'percent' === $rule['kind'] ? (int) round( $line['total'] * min( 100, $amount ) / 100 ) : (int) min( $line['total'], round( $amount * $line['qty'] ) ); } return $discounts; } public static function calculate( array $lines, array $rules, array $context, string $mode = 'best' ): array { $result = array( 'discounts' => array(), 'rules' => array(), 'shipping' => false, 'gifts' => array() ); usort( $rules, static fn( $a, $b ) => ( (int) $a['priority'] <=> (int) $b['priority'] ) ?: strcmp( $a['id'], $b['id'] ) ); $mode = apply_filters( 'digidiscount_processing_mode', in_array( $mode, array( 'priority', 'best', 'stack' ), true ) ? $mode : 'best', $mode ); $working = $lines; $groups = array(); foreach ( $rules as $rule ) { if ( 'stack' === $mode && ! empty( $rule['group'] ) && isset( $groups[ $rule['group'] ] ) ) { continue; } $candidate = self::candidate( $working, $rule, $context ); $sum = array_sum( $candidate['discounts'] ); if ( ! $sum && ! $candidate['shipping'] && ! $candidate['gifts'] ) { continue; } $entry = array( 'id' => $rule['id'], 'name' => $rule['name'], 'amount' => $sum ); if ( 'stack' === $mode ) { if ( ! empty( $result['rules'] ) && empty( $rule['stackable'] ) ) { continue; } foreach ( $candidate['discounts'] as $key => $discount ) { $result['discounts'][ $key ] = ( $result['discounts'][ $key ] ?? 0 ) + $discount; $working[ $key ]['total'] -= $discount; } $result['rules'][] = $entry; $result['shipping'] = $result['shipping'] || $candidate['shipping']; $result['gifts'] = array_merge( $result['gifts'], $candidate['gifts'] ); $groups[ $rule['group'] ?? '' ] = true; if ( empty( $rule['stackable'] ) ) { break; } } elseif ( empty( $result['rules'] ) || $sum > array_sum( $result['discounts'] ) ) { $result = array_merge( $candidate, array( 'rules' => array( $entry ) ) ); if ( 'priority' === $mode ) { break; } } } return $result; } }