Commit b39bc233 authored by Patrick Schmalstig's avatar Patrick Schmalstig
Browse files

Fixed MANTIS-6065 (Several eCommerce issues with points products)

parent 88443c50
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -476,7 +476,8 @@ class Module_admin_ecommerce_reports
        $status = 'Completed';
        $reason = '';
        $pending_reason = '';
        $txn_id = 'manual-' . substr(uniqid('', true), 0, 10);
        require_code('crypt');
        $txn_id = 'man-' . str_replace('-', '', get_secure_v1_guid()); // Must keep under 40 characters
        $parent_txn_id = '';

        $item_name = $details['item_name'];
+41 −14
Original line number Diff line number Diff line
@@ -368,7 +368,8 @@ class Module_purchase
                            $type_code = 'topic_pin';
                            break;
                    }
                    $txn_id = 'manual-' . substr(uniqid('', true), 0, 10);
                    require_code('crypt');
                    $txn_id = 'man-' . str_replace('-', '', get_secure_v1_guid()); // Must keep under 40 characters
                    $GLOBALS['SITE_DB']->query_insert('ecom_transactions', [
                        'id' => $txn_id,
                        't_type_code' => $type_code,
@@ -870,9 +871,12 @@ class Module_purchase
            if (!$can_purchase) {
                $_discounted_price = do_lang('NA');
                $written_price = do_lang_tempcode('NA_EM');
            } elseif ($full_price === 0.00/*free without any need for discount*/) {
            } elseif (($full_price === 0.00) && ($points_for_discount === null)/*free without any need for discount*/) {
                $_discounted_price = do_lang('NA');
                $written_price = do_lang_tempcode('ECOMMERCE_PRODUCT_PRICING_FOR_FREE');
            } elseif (($full_price === 0.00) && ($points_for_discount !== null)/*Points only*/) {
                $_discounted_price = do_lang('NA');
                $written_price = do_lang_tempcode('ECOMMERCE_PRODUCT_PRICING_FOR_FREE_WITH_POINTS', $_discounted_price, $_full_price, [escape_html(integer_format($points_for_discount))]);
            } elseif ($discounted_price === 0.00/*discounted via points to zero*/) {
                $_discounted_price = currency_convert_wrap(0.00, $currency);
                $written_price = do_lang_tempcode('ECOMMERCE_PRODUCT_PRICING_FOR_FREE_WITH_POINTS', $_discounted_price, $_full_price, [escape_html(integer_format($points_for_discount))]);
@@ -1262,7 +1266,12 @@ class Module_purchase
            }
        }

        $icon = 'buttons/proceed';

        if ($price == 0.00) { // Free/point-based product
            $button = '';
            $next_purchase_step = get_next_purchase_step($product_object, $type_code, 'pay');

            if ($confirmation_box === null) {
                if ($points_for_discount !== null) {
                    if (!addon_installed('points')) {
@@ -1273,30 +1282,34 @@ class Module_purchase

                    require_css('points');
                    $icon = 'menu/social/points';

                    $button = make_transaction_button($type_code, $item_name, $purchase_id, $price, [], 0.00, [], 0.00, 0.00, $currency, $points_for_discount, 'points');
                    $finish_url = null; // Button handles the transaction
                } else {
                    $confirmation_box = do_lang_tempcode('BUYING_FOR_FREE_CONFIRMATION', escape_html($item_name));

                    $icon = 'buttons/proceed';
                    $finish_url = build_url(['page' => '_SELF', 'type' => $next_purchase_step, 'type_code' => $type_code, 'points' => 1, 'purchase_id' => $purchase_id], '_SELF', ['include_message' => null], true);
                }
            }

            // No form
            $result = do_template('ECOM_PURCHASE_STAGE_TRANSACT', [
                '_GUID' => '76ecddb8d2429ed02412943e47dff5d9',
            $result = do_template('ECOM_PURCHASE_STAGE_PAY', [
                'TITLE' => $this->title,
                'TRANSACTION_BUTTON' => $button,
                'CURRENCY' => $currency,
                'ITEM_NAME' => $item_name,
                'TYPE_CODE' => $type_code,
                'PURCHASE_ID' => $purchase_id,
                'FIELDS' => null,
                'HIDDEN' => '',
                'LOGOS' => '',
                'PAYMENT_PROCESSOR_LINKS' => '',
                'LENGTH' => ($length === null) ? '' : strval($length),
                'LENGTH_UNITS' => $length_units,
                'PRICE' => float_to_raw_string($price),
                'TEXT' => $text,
                'CONFIRMATION_BOX' => $confirmation_box,
                'LOGOS' => '',
                'PAYMENT_PROCESSOR_LINKS' => '',
            ]);

            $next_purchase_step = get_next_purchase_step($product_object, $type_code, 'pay');
            $finish_url = build_url(['page' => '_SELF', 'type' => $next_purchase_step, 'points' => 1, 'purchase_id' => $purchase_id, 'type_code' => $type_code], '_SELF', ['include_message' => null], true);
            $submit_name = do_lang_tempcode('MAKE_PAYMENT');
        } elseif (perform_local_payment()) { // Handle the transaction internally
            if ($confirmation_box === null) {
@@ -1481,6 +1494,7 @@ class Module_purchase
            $item_name = $details['item_name'];

            $purchase_id = get_param_string('purchase_id');
            $trans_expecting_id = get_param_string('trans_expecting_id');

            list($discounted_price, $discounted_tax_code, $points_for_discount) = get_discounted_price($details);
            if (($discounted_price === null) && ($details['price'] !== 0.00)) {
@@ -1489,21 +1503,34 @@ class Module_purchase

            if ($points_for_discount !== null) {
                // Paying with points
                $errormsg = new Tempcode();
                if (!addon_installed__messaged('points', $errormsg)) {
                    return $errormsg;
                }

                $message = do_lang_tempcode('POINTS_PURCHASE', escape_html(integer_format($points_for_discount)));
                $memo = post_param_string('memo', do_lang('POINTS'));
                $currency = 'points';

                // handle_confirmed_transaction by nature is executed when a transaction is already complete, so we need to verify points balance here
                require_code('points');
                if (points_balance(get_member()) < $points_for_discount) {
                    require_lang('points');
                    warn_exit(do_lang_tempcode('LACKING_POINTS', escape_html(integer_format($points_for_discount - points_balance(get_member())))));
                }
            } else {
                // Completely free
                $message = do_lang_tempcode('FREE_PURCHASE');
                $memo = post_param_string('memo', do_lang('FREE'));
                $currency = get_option('currency');
            }
            $price = 0.00;

            $currency = get_option('currency');

            $status = 'Completed';
            $reason = '';
            $pending_reason = '';
            $txn_id = 'manual-' . substr(uniqid('', true), 0, 10);
            require_code('crypt');
            $txn_id = 'tx-' . str_replace('-', '', get_secure_v1_guid()); // Must keep under 40 characters
            $parent_txn_id = '';
            $is_subscription = ($details['type'] == PRODUCT_SUBSCRIPTION);
            if ($is_subscription) {
@@ -1512,7 +1539,7 @@ class Module_purchase
                $period = '';
            }

            handle_confirmed_transaction(null, $txn_id, $type_code, $item_name, $purchase_id, $is_subscription, $status, $reason, $price, 0.00, 0.00, 0.00, $currency, true, $parent_txn_id, $pending_reason, $memo, $period, get_member(), 'manual', false, true);
            handle_confirmed_transaction($trans_expecting_id, $txn_id, $type_code, $item_name, $purchase_id, $is_subscription, $status, $reason, $price, 0.00, 0.00, 0.00, $currency, true, $parent_txn_id, $pending_reason, $memo, $period, get_member(), 'manual', false, true);

            global $ECOMMERCE_SPECIAL_SUCCESS_MESSAGE;
            if ($ECOMMERCE_SPECIAL_SUCCESS_MESSAGE !== null) {
+39 −18
Original line number Diff line number Diff line
@@ -253,30 +253,51 @@ function get_secure_random_number() : int
 */
function get_secure_v1_guid() : string
{
    // Generate random sequence index
    static $clock_sequence = 0;
    $clock_sequence = get_secure_random_number();
    // Initialize clock sequence
    $_clock_sequence = get_value_newer_than('guidv1_clock_sequence', time() - 1, true);

    // Get timestamp
    $ts = intval((microtime(true) * 10000000) + mt_rand(0, 9) + 0x01b21dd213814000);
    // Generate a random clock sequence if we do not need to worry about replication
    if ($_clock_sequence === null) {
        $clock_sequence = random_int(0, 0x3FFF);
    } else {
        $clock_sequence = intval($_clock_sequence) & 0x3FFF;
    }

    // Increment sequence for next use
    set_value('guidv1_clock_sequence', strval($clock_sequence + 1), true);

    // Set the variant (RFC 4122)
    $clock_sequence |= 0x8000;

    // Get timestamp (with 100-nanosecond precision)
    $time = intval((microtime(true) * 10000000) + mt_rand(0, 9) + 0x01b21dd213814000);

    // Convert time to 60-bit value (as UUID uses only 60 bits for timestamp)
    $time_hex = str_pad(dechex($time), 15, '0', STR_PAD_LEFT);

    // Format time into UUID parts
    $time_low = substr($time_hex, 7, 8);
    $time_mid = substr($time_hex, 3, 4);
    $time_high_and_version = substr($time_hex, 0, 3);

    // Set version to 1 (time-based UUID)
    $time_high_and_version = dechex(hexdec($time_high_and_version) | 0x1000);

    // Use the first 12 characters of the MD5 of the site salt as the node
    $node = substr(md5(get_site_salt()), 0, 12);
    $node[0] = dechex(hexdec($node[0]) & 1);

    // Piece together time and clock components
    $time_low = $ts & 0xFFFFFFFF;
    $time_mid = ($ts >> 32) & 0xFFFF;
    $time_hi_and_version = ($ts >> 48) & 0x0FFF;
    $time_hi_and_version |= (1 << 12);
    $clock_seq_hi_and_reserved = ($clock_sequence & 0x3F00) >> 8;
    $clock_seq_hi_and_reserved |= 0x80;
    $clock_seq_low = $clock_sequence & 0xFF;

    // Increment sequence for next use
    $clock_sequence++;

    return sprintf('%08x-%04x-%04x-%02x%02x-%s', $time_low, $time_mid, $time_hi_and_version, $clock_seq_hi_and_reserved, $clock_seq_low, $node);
    // Assemble the GUID
    $guid = sprintf(
        '%s-%s-%s-%04x-%s',
        $time_low,
        $time_mid,
        $time_high_and_version,
        $clock_sequence,
        $node
    );

    return $guid;
}

/**
+19 −9
Original line number Diff line number Diff line
@@ -568,20 +568,24 @@ function build_transaction_linker(string $txn_id, bool $awaiting_payment, ?array
 * @param  REAL $shipping_tax Transaction shipping tax in money
 * @param  ID_TEXT $currency The currency to use
 * @param  integer $price_points Transaction price in points
 * @param  ?ID_TEXT $payment_gateway The payment gateway the payment will go via (null: autodetect)
 * @return Tempcode The button
 * @param  ?ID_TEXT $payment_gateway The payment gateway the payment will go via (null: autodetect) (false: none, usually used for manual transactions)
 * @return ?Tempcode The button (null: $payment_gateway was false, so payment is handled manually)
 */
function make_transaction_button(string $type_code, string $item_name, string $purchase_id, float $price, array $tax_derivation, float $tax, array $tax_tracking, float $shipping_cost, float $shipping_tax, string $currency, int $price_points = 0, ?string $payment_gateway = null) : object
function make_transaction_button(string $type_code, string $item_name, string $purchase_id, float $price, array $tax_derivation, float $tax, array $tax_tracking, float $shipping_cost, float $shipping_tax, string $currency, int $price_points = 0, ?string $payment_gateway = null) : ?object
{
    if ($payment_gateway === null) {
        $payment_gateway = get_option('payment_gateway');
    }
    } elseif ($payment_gateway !== false) {
        require_code('hooks/systems/payment_gateway/' . filter_naughty_harsh($payment_gateway));
        $payment_gateway_object = object_factory('Hook_payment_gateway_' . filter_naughty_harsh($payment_gateway));
        $trans_expecting_id = $payment_gateway_object->generate_trans_id();
    } else {
        require_code('crypt');
        $trans_expecting_id = get_secure_random_string();
    }

    $invoicing_breakdown = generate_invoicing_breakdown($type_code, $item_name, $purchase_id, $price, $tax, $shipping_cost, $shipping_tax);

    $trans_expecting_id = $payment_gateway_object->generate_trans_id();
    $GLOBALS['SITE_DB']->query_insert('ecom_trans_expecting', [
        'id' => $trans_expecting_id,
        'e_type_code' => $type_code,
@@ -605,6 +609,10 @@ function make_transaction_button(string $type_code, string $item_name, string $p
    ]);
    store_shipping_address($trans_expecting_id);

    if ($payment_gateway === false) {
        return null;
    }

    return $payment_gateway_object->make_transaction_button($trans_expecting_id, $type_code, $item_name, $purchase_id, $price, $tax, $shipping_cost, $currency);
}

@@ -780,7 +788,8 @@ function perform_local_payment() : bool
    $payment_gateway = get_option('payment_gateway');
    require_code('hooks/systems/payment_gateway/' . filter_naughty_harsh($payment_gateway));
    $payment_gateway_object = object_factory('Hook_payment_gateway_' . filter_naughty_harsh($payment_gateway));
    return (get_option('use_local_payment') == '1') && (method_exists($payment_gateway_object, 'do_local_transaction'));
    $payment_gateway_config = $payment_gateway_object->get_config();
    return ((($payment_gateway_config['local_only'] === true) || (get_option('use_local_payment') == '1')) && (method_exists($payment_gateway_object, 'do_local_transaction')));
}

/**
@@ -1507,7 +1516,8 @@ function handle_pdt_ipn_transaction_script(bool $silent_fail = false, bool $send
function handle_confirmed_transaction(?string $trans_expecting_id, ?string $txn_id = null, ?string $type_code = null, ?string $item_name = null, ?string $purchase_id = null, bool $is_subscription = false, string $status = 'Completed', string $reason = '', ?float $price = null, ?float $tax = null, ?float $shipping = null, ?float $transaction_fee = null, ?string $currency = null, bool $check_amounts = true, string $parent_txn_id = '', string $pending_reason = '', string $memo = '', string $period = '', ?int $member_id_paying = null, string $payment_gateway = '', bool $silent_fail = false, bool $send_notifications = true) : ?array
{
    if ($txn_id === null) {
        $txn_id = uniqid('trans', true);
        require_code('crypt');
        $txn_id = 'tx-' . str_replace('-', '', get_secure_v1_guid()); // Must keep under 40 characters
        $t_status = null;
    } else {
        // Grab the t_status of the $txn_id, mainly used for determining when the transaction was already processed.
@@ -1730,7 +1740,7 @@ function handle_confirmed_transaction(?string $trans_expecting_id, ?string $txn_
    // Charge points if required
    if ((addon_installed('points')) && ($t_status === null) && ($status == 'Completed') && ($check_amounts) && ($expected_price_points !== null) && ($expected_price_points != 0)) {
        require_code('points2');
        points_debit_member($member_id_paying, do_lang(($expected_amount == 0.00) ? 'FREE_ECOMMERCE_PRODUCT' : 'DISCOUNTED_ECOMMERCE_PRODUCT', $item_name), $expected_price_points, 0, 0, false, 0, 'ecommerce', 'purchase', strval($txn_id));
        points_debit_member($member_id_paying, do_lang(($expected_amount == 0.00) ? 'FREE_ECOMMERCE_PRODUCT' : 'DISCOUNTED_ECOMMERCE_PRODUCT', $item_name), $expected_price_points, 0, 0, false, 0, 'ecommerce', 'purchase', strval($txn_id), null, true/*should force so if the member tries to cheat by spending points, at least they'll end up in the negative*/);
    }
    $price_points = (($expected_price_points !== null) ? $expected_price_points : 0);

+2 −2
Original line number Diff line number Diff line
@@ -260,10 +260,10 @@ class Hook_ecommerce_banners
                break;

            case 'BANNER_UPGRADE_HITS':
                return [null, null, []];
                return [null, null, null, []];

            case 'BANNER_UPGRADE_IMPORTANCE':
                return [null, null, []];
                return [null, null, null, []];
        }

        ecommerce_attach_memo_field_if_needed($fields);
Loading