<?php

if (!defined('ABSPATH')) {
    die('You are not allowed to call this page directly.');
}

class MeprTransactionsHelper
{
    /**
     * Output the hidden checkout token field for a payment form.
     *
     * The token authorizes the payment request for this transaction — see
     * MeprCheckoutCtrl::verify_checkout_access().
     *
     * @param MeprTransaction|integer|string $txn The transaction object or its ID.
     *
     * @return void
     */
    public static function checkout_token_field($txn)
    {
        if (!($txn instanceof MeprTransaction)) {
            $txn = new MeprTransaction((int) $txn);
        }
        ?>
        <input type="hidden" name="mepr_checkout_token" value="<?php echo esc_attr($txn->checkout_token()); ?>" />
        <?php
    }

    /**
     * Determines if ReadyLaunch template should be used for invoice rendering.
     *
     * Checks multiple conditions to decide template usage:
     * 1. Actual product pages (not shortcodes)
     * 2. Thank you page (when thank you template is enabled)
     * 3. ReadyLaunch blocks (memberpress/checkout block)
     * 4. AJAX requests (inherits context from originating page)
     *
     * @return boolean True if ReadyLaunch template should be used, false otherwise.
     */
    private static function should_use_readylaunch_template()
    {
        // Check page type and context.
        $mepr_options           = MeprOptions::fetch();
        $current_post           = MeprUtils::get_current_post();
        $is_actual_product_page = (is_object($current_post) && property_exists($current_post, 'post_type') && $current_post->post_type === MeprProduct::$cpt);
        $is_thankyou_page       = MeprAppHelper::is_thankyou_page($current_post);
        $is_readylaunch_block    = (function_exists('has_block') && has_block('memberpress/checkout', $current_post));

        // Determine if ReadyLaunch template should be used based on page context.
        $use_readylaunch = (
            ($mepr_options->design_enable_checkout_template && ($is_actual_product_page || $is_readylaunch_block)) ||
            ($mepr_options->design_enable_thankyou_template && $is_thankyou_page)
        );

        // During AJAX requests, use the context passed from frontend
        // This applies to both regular invoice updates (coupons, address changes) and order bump toggles.
        if ($mepr_options->design_enable_checkout_template && wp_doing_ajax() && isset($_POST['mepr_is_product_page'])) {
            $use_readylaunch = filter_var(wp_unslash($_POST['mepr_is_product_page']), FILTER_VALIDATE_BOOLEAN);
        }

        return apply_filters('mepr_should_use_readylaunch_template_for_invoice', $use_readylaunch);
    }

    /**
     * Formats a transaction's currency display.
     * Especially for formatting a subscription's price
     *
     * @param MeprTransaction $txn           The transaction object.
     * @param boolean         $show_symbol   Whether to show the currency symbol.
     * @param boolean         $show_prorated Whether to show prorated amounts.
     *
     * @return string The formatted currency string.
     */
    public static function format_currency($txn, $show_symbol = true, $show_prorated = true)
    {
        $coupon_code = null;
        $coupon      = $txn->coupon();
        if ($coupon) {
            $coupon_code = $coupon->post_title;
        }

        $obj = $txn->subscription();
        if ($obj) {
            $price = $obj->price;
        } else {
            $obj               = $txn->product();
            $obj->expire_fixed = $txn->expires_at;

            // If the txn is expired -- don't check for pro-rations.
            if ($txn->is_expired()) {
                $obj->group_id = null;
            }

            $price = $txn->total;
        }

        $tax_info = [
            'rate' => $obj->tax_rate,
            'desc' => $obj->tax_desc,
        ];

        return MeprAppHelper::format_price_string($obj, $price, $show_symbol, $coupon_code, $show_prorated, $tax_info);
    }

    /**
     * Retrieves the payment methods dropdown for the new/edit transactions form.
     *
     * @param string $field_name The name of the field.
     * @param string $value      The value of the field.
     *
     * @return void The payment methods dropdown.
     */
    public static function payment_methods_dropdown($field_name, $value = 'manual')
    {
        $mepr_options = MeprOptions::fetch();

        $pms = array_keys($mepr_options->integrations);

        $value = isset($_POST[$field_name]) ? sanitize_text_field(wp_unslash($_POST[$field_name])) : $value;

        ?>
    <select name="<?php echo esc_attr($field_name); ?>" id="<?php echo esc_attr($field_name); ?>" class="mepr-multi-select mepr-payment-methods-select">
      <option value="manual" <?php selected($value, 'manual'); ?>><?php esc_html_e('Manual', 'memberpress'); ?>&nbsp;</option>
        <?php
        foreach ($pms as $pm_id) :
            $obj = $mepr_options->payment_method($pm_id);
            if ($obj instanceof MeprBaseRealGateway) :
                ?>
            <option value="<?php echo esc_attr($obj->id); ?>" <?php selected($value, $obj->id); ?>>
                <?php echo esc_html(sprintf('%1$s (%2$s)', $obj->label, $obj->name)); ?>
                &nbsp;
            </option>
                <?php
            endif;
        endforeach;
        ?>
    </select>
        <?php
    }

    /**
     * Retrieves email variables for transactions.
     *
     * @return array The email variables.
     */
    public static function get_email_vars()
    {
        return MeprHooks::apply_filters(
            'mepr_transaction_email_vars',
            [
                'user_id',
                'user_login',
                'username',
                'user_email',
                'user_first_name',
                'user_last_name',
                'user_full_name',
                'user_address',
                'user_registered',
                'usermeta:*',
                'membership_type',
                'product_name',
                'coupon_code',
                'trans_num',
                'trans_date',
                'trans_expires_at',
                'subscr_trial_end_date',
                'trans_gateway',
                'trans_status',
                'payment_amount',
                'blog_name',
                'payment_subtotal',
                'tax_rate',
                'tax_amount',
                'tax_desc',
                'business_name',
                'biz_name',
                'biz_address1',
                'biz_address2',
                'biz_city',
                'biz_state',
                'biz_postcode',
                'biz_country',
                'login_url',
                'account_url',
            ]
        );
    }

    /**
     * Retrieves email parameters for a transaction.
     *
     * @param MeprTransaction $txn The transaction object.
     *
     * @return array The email parameters.
     */
    public static function get_email_params($txn)
    {
        $mepr_options = MeprOptions::fetch();
        $usr          = $txn->user();
        $prd          = $txn->product();
        $pm           = $txn->payment_method();
        $cpn          = $txn->coupon();
        $created_at   = MeprAppHelper::format_date($txn->created_at, '');

        $unformatted_payment_total      = $txn->total;
        $unformatted_payment_subtotal   = $txn->amount;
        $unformatted_payment_tax_amount = $txn->tax_amount;

        if (!isset($txn->expires_at) || empty($txn->expires_at)) {
            $expires_at = __('Unknown', 'memberpress');
        } elseif ($txn->expires_at === MeprUtils::db_lifetime()) {
            $expires_at = __('Never', 'memberpress');
        } else {
            // Confirmation txn? Let's guess what the expires at will be then.
            if ($txn->status === MeprTransaction::$confirmed_str) {
                $sub = $txn->subscription();

                if ($sub->trial && $sub->trial_days) {
                    $expires_at_ts                  = strtotime($txn->created_at) + MeprUtils::days($sub->trial_days);
                    $unformatted_payment_total      = $sub->trial_total;
                    $unformatted_payment_subtotal   = $sub->trial_amount;
                    $unformatted_payment_tax_amount = $sub->trial_tax_amount;
                } else {
                    $expires_at_ts                  = $prd->get_expires_at(strtotime($txn->created_at));
                    $unformatted_payment_total      = $sub->total;
                    $unformatted_payment_subtotal   = $sub->price;
                    $unformatted_payment_tax_amount = $sub->tax_amount;
                }

                $expires_at = MeprAppHelper::format_date(gmdate('c', $expires_at_ts), '');
            } else {
                $expires_at = MeprAppHelper::format_date($txn->expires_at, '');
            }
        }

        if ($mepr_options->currency_symbol_after) {
            $payment_amount   = preg_replace('~\$~', '\\\$', sprintf(MeprUtils::format_currency_float($unformatted_payment_total) . '%s', stripslashes($mepr_options->currency_symbol)));
            $payment_subtotal = preg_replace('~\$~', '\\\$', sprintf(MeprUtils::format_currency_float($unformatted_payment_subtotal) . '%s', stripslashes($mepr_options->currency_symbol)));
            $tax_amount       = preg_replace('~\$~', '\\\$', sprintf(MeprUtils::format_currency_float($unformatted_payment_tax_amount) . '%s', stripslashes($mepr_options->currency_symbol)));
        } else {
            $payment_amount   = preg_replace('~\$~', '\\\$', sprintf('%s' . MeprUtils::format_currency_float($unformatted_payment_total), stripslashes($mepr_options->currency_symbol)));
            $payment_subtotal = preg_replace('~\$~', '\\\$', sprintf('%s' . MeprUtils::format_currency_float($unformatted_payment_subtotal), stripslashes($mepr_options->currency_symbol)));
            $tax_amount       = preg_replace('~\$~', '\\\$', sprintf('%s' . MeprUtils::format_currency_float($unformatted_payment_tax_amount), stripslashes($mepr_options->currency_symbol)));
        }

        // Coupon title.
        $cpn = ($cpn !== false) ? $cpn->post_title : '';

        // Handle cases where payment method might not exist.
        if ($pm && is_object($pm) && isset($pm->label) && isset($pm->name)) {
            $trans_gateway = sprintf('%1$s (%2$s)', $pm->label, $pm->name);
        } else {
            $trans_gateway = __('Unknown Payment Method', 'memberpress');
        }

        $params = [
            'user_id'          => $usr->ID,
            'user_login'       => $usr->user_login,
            'username'         => $usr->user_login,
            'user_email'       => $usr->user_email,
            'user_first_name'  => $usr->first_name,
            'user_last_name'   => $usr->last_name,
            'user_full_name'   => $usr->full_name(),
            'user_address'     => $usr->formatted_address(),
            'user_registered'  => $usr->user_registered,
            'membership_type'  => preg_replace('~\$~', '\\\$', $prd->post_title),
            'product_name'     => preg_replace('~\$~', '\\\$', $prd->post_title),
            'coupon_code'      => $cpn,
            'invoice_num'      => $txn->id,
            'trans_num'        => $txn->trans_num,
            'trans_date'       => $created_at,
            'trans_expires_at' => $expires_at,
            'trans_gateway'    => $trans_gateway,
            'trans_status'     => ucfirst($txn->status),
            'user_remote_addr' => sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? '')),
            'payment_amount'   => $payment_amount,
            'blog_name'        => MeprUtils::blogname(),
            'payment_subtotal' => $payment_subtotal,
            'tax_rate'         => $txn->tax_rate,
            'tax_amount'       => $tax_amount,
            'tax_desc'         => $txn->tax_desc,
            'business_name'    => $mepr_options->attr('biz_name'),
            'biz_name'         => $mepr_options->attr('biz_name'),
            'biz_address1'     => $mepr_options->attr('biz_address1'),
            'biz_address2'     => $mepr_options->attr('biz_address2'),
            'biz_city'         => $mepr_options->attr('biz_city'),
            'biz_state'        => $mepr_options->attr('biz_state'),
            'biz_postcode'     => $mepr_options->attr('biz_postcode'),
            'biz_country'      => $mepr_options->attr('biz_country'),
            'login_page'       => $mepr_options->login_page_url(),
            'account_url'      => $mepr_options->account_page_url(),
            'login_url'        => $mepr_options->login_page_url(),
        ];

        // When lifetime, include these subscription vars too.
        $sub = $txn->subscription();
        if ($sub) {
            $sub_params = MeprSubscriptionsHelper::get_email_params($sub);
        } else { // Get as much info out there as we can.
            $cc = (object)[
                'cc_last4'     => '****',
                'cc_exp_month' => '',
                'cc_exp_year'  => '',
            ];

            $sub_params = [
                'subscr_num'                 => $txn->trans_num,
                'subscr_date'                => $created_at,
                'subscr_gateway'             => $params['trans_gateway'],
                'subscr_next_billing_at'     => __('Never', 'memberpress'),
                'subscr_trial_end_date'      => '',
                'subscr_expires_at'          => $expires_at,
                'subscr_terms'               => $params['payment_amount'],
                'subscr_next_billing_amount' => $params['payment_amount'],
                'subscr_cc_num'              => MeprUtils::cc_num($cc->cc_last4),
                'subscr_cc_month_exp'        => $cc->cc_exp_month,
                'subscr_cc_year_exp'         => $cc->cc_exp_year,
                'subscr_renew_url'           => $mepr_options->login_page_url('redirect_to=' . urlencode($prd->url())),
                'subscr_update_url'          => $mepr_options->account_page_url(),
                'subscr_upgrade_url'         => $mepr_options->login_page_url('redirect_to=' . urlencode($prd->group_url())),
            ];
        }

        $params = array_merge($params, $sub_params);
        $ums    = MeprUtils::get_formatted_usermeta($usr->ID);

        if (!empty($ums)) {
            foreach ($ums as $umkey => $umval) {
                $params["usermeta:{$umkey}"] = $umval;
            }
        }

        // You know we're just going to lump the user record fields in here no problem.
        foreach ((array)$usr->rec as $ukey => $uval) {
            $params["usermeta:{$ukey}"] = $uval;
        }

        // This filter is DEPRECATED ... please use mepr-transaction-email-params instead.
        $params = MeprHooks::apply_filters('mepr_gateway_notification_params', $params, $txn);

        return MeprHooks::apply_filters('mepr_transaction_email_params', $params, $txn);
    }

    /**
     * Retrieves the invoice string for a transaction.
     *
     * @param MeprTransaction  $txn    The transaction object.
     * @param MeprSubscription $tmpsub Optional temporary subscription object.
     *
     * @return string The invoice string.
     */
    public static function get_invoice($txn, $tmpsub = '')
    {
        $mepr_options = MeprOptions::fetch();
        $prd          = $txn->product();

        if (!empty($tmpsub) && $tmpsub instanceof MeprSubscription) {
            $sub = $tmpsub;
        } else {
            $sub = $txn->subscription();
        }

        if ($sub instanceof MeprSubscription && (($sub->trial && $sub->trial_days > 0 && $sub->txn_count < 1) || $txn->txn_type === MeprTransaction::$subscription_confirmation_str)) {
            self::set_invoice_txn_vars_from_sub($txn, $sub);
        }

        $desc                         = self::get_payment_description($txn, $sub, $prd);
        $show_negative_tax_on_invoice = get_option('mepr_show_negative_tax_on_invoice');

        /*
         * Base line amount and an empty coupon row. When the transaction or
         * subscription carries a coupon, the coupons module recomputes both
         * on the invoice filter below.
         */
        $amount     = $show_negative_tax_on_invoice && $txn->tax_reversal_amount > 0 ? $txn->amount + $txn->tax_reversal_amount : $txn->amount;
        $cpn_id     = 0;
        $cpn_desc   = '';
        $cpn_amount = 0.00;

        /*
         * Data-at-rest read for the templates: the readylaunch views derive
         * the coupon code from this object for the price terms display. The
         * guarded accessor degrades to false in brand builds without the
         * coupons feature.
         */
        $coupon = $txn->coupon();

        if (!$coupon && $sub instanceof MeprSubscription) {
            $coupon = $sub->coupon();
        }

        $invoice = MeprHooks::apply_filters(
            'mepr_invoice',
            [
                'items'  => [
                    [
                        'description' => $prd->post_title . '&nbsp;&ndash;&nbsp;' . $desc,
                        'quantity'    => 1,
                        'amount'      => $amount,
                    ],
                ],
                'coupon' => [
                    'id'     => $cpn_id,
                    'desc'   => $cpn_desc,
                    'amount' => $cpn_amount,
                ],
                'tax'    => [
                    'percent' => $txn->tax_rate,
                    'type'    => $txn->tax_desc,
                    'amount'  => $show_negative_tax_on_invoice && $txn->tax_reversal_amount > 0 ? -1 * $txn->tax_reversal_amount : $txn->tax_amount,
                ],
            ],
            $txn,
            $sub
        );

        $show_quantity = MeprHooks::apply_filters('mepr_invoice_show_quantity', false, $txn);

        $quantities = [];
        foreach ($invoice['items'] as $item) {
            $quantities[] = $item['amount'];
        }

        $subtotal = (float)array_sum($quantities) - (float)$invoice['coupon']['amount'];
        $total    = $subtotal + $invoice['tax']['amount'];

        ob_start();

        if ($sub) {
            $prd = $sub->product();
            if (
                $prd->register_price_action === 'default' ||
                ($prd->register_price_action === 'custom' && MeprReadyLaunchCtrl::template_enabled('checkout') && $mepr_options->design_show_checkout_price_terms)
            ) {
                $sub_price_str = MeprSubscriptionsHelper::format_currency($sub);
            }

            if ($prd->register_price_action === 'custom' && !MeprReadyLaunchCtrl::template_enabled('checkout') && !empty($prd->register_price) && !$txn->coupon_id && !$txn->prorated) {
                $sub_price_str = stripslashes($prd->register_price);
            }
        }

        // Determine which invoice template to use.
        if (self::should_use_readylaunch_template()) {
            MeprView::render('/readylaunch/checkout/invoice', get_defined_vars());
        } else {
            MeprView::render('/checkout/invoice', get_defined_vars());
        }

        $invoice = ob_get_clean();
        // Wrap invoice in a div to maintain consistent DOM structure when replaced via AJAX.
        $invoice = '<div>' . $invoice . '</div>';
        return MeprHooks::apply_filters('mepr_invoice_html', $invoice, $txn);
    }

    /**
     * Displays a dropdown for transaction statuses.
     *
     * @param string $field   The field name.
     * @param string $value   The selected value.
     * @param string $id      Optional ID for the dropdown.
     * @param string $classes Optional CSS classes for the dropdown.
     *
     * @return void
     */
    public static function statuses_dropdown($field, $value, $id = '', $classes = '')
    {
        if (empty($id)) {
            $id = $field;
        }

        ?>
    <select name="<?php echo esc_attr($field); ?>" id="<?php echo esc_attr($id); ?>" class="<?php echo esc_attr($classes); ?>">
      <option value="<?php echo esc_attr(MeprTransaction::$complete_str); ?>" <?php echo selected($value, MeprTransaction::$complete_str); ?>><?php esc_html_e('Complete', 'memberpress'); ?></option>
      <option value="<?php echo esc_attr(MeprTransaction::$pending_str); ?>" <?php echo selected($value, MeprTransaction::$pending_str); ?>><?php esc_html_e('Pending', 'memberpress'); ?></option>
      <option value="<?php echo esc_attr(MeprTransaction::$failed_str); ?>" <?php echo selected($value, MeprTransaction::$failed_str); ?>><?php esc_html_e('Failed', 'memberpress'); ?></option>
      <option value="<?php echo esc_attr(MeprTransaction::$refunded_str); ?>" <?php echo selected($value, MeprTransaction::$refunded_str); ?>><?php esc_html_e('Refunded', 'memberpress'); ?></option>
    </select>
        <?php
    }

    /**
     * Displays a dropdown for transaction membership fields.
     *
     * @param string $field               The field name.
     * @param string $value               The selected value.
     * @param string $expires_at_field_id The ID for the expiration field.
     * @param string $id                  Optional ID for the dropdown.
     * @param string $classes             Optional CSS classes for the dropdown.
     *
     * @return void
     */
    public static function transaction_membership_field($field, $value = '', $expires_at_field_id = '', $id = '', $classes = '')
    {
        if (empty($id)) {
            $id = $field;
        }

        $products = MeprCptModel::all('MeprProduct', false, [
            'orderby' => 'title',
            'order'   => 'ASC',
        ]);

        ?>

    <select
      name="<?php echo esc_attr($field); ?>"
      id="<?php echo esc_attr($id); ?>"
      class="mepr-membership-dropdown <?php echo esc_attr($classes); ?>"
      data-expires_at_field_id="<?php echo esc_attr($expires_at_field_id); ?>"
      >
        <?php foreach ($products as $product) : ?>
        <option value="<?php echo esc_attr($product->ID); ?>" <?php selected($value, $product->ID); ?>><?php echo esc_html($product->post_title); ?></option>
        <?php endforeach; ?>
    </select>
        <?php
    }

    /**
     * Displays a field for transaction creation date.
     *
     * @param string $field   The field name.
     * @param string $value   The field value.
     * @param string $id      Optional ID for the field.
     * @param string $classes Optional CSS classes for the field.
     *
     * @return void
     */
    public static function transaction_created_at_field($field, $value = '', $id = '', $classes = '')
    {
        if (empty($id)) {
            $id = $field;
        }

        ?>
      <div id="<?php echo esc_attr($id); ?>" class="mepr_transaction_created_field">
        <input
          type="text"
          name="<?php echo esc_attr($field); ?>"
          value="<?php echo esc_attr(MeprAppHelper::format_date_utc($value, gmdate('Y-m-d H:i:s'), 'Y-m-d H:i:s')); ?>"
          class="regular-text mepr-date-picker mepr-created-at"
        />
        <a href="" class="mepr-today-button button"><?php esc_html_e('Now', 'memberpress'); ?></a>
      </div>
        <?php
    }

    /**
     * Displays a field for transaction expiration date.
     *
     * @param string $field            The field name.
     * @param string $membership_field The membership field ID.
     * @param string $created_at_field The creation date field ID.
     * @param string $value            The field value.
     * @param string $id               Optional ID for the field.
     * @param string $classes          Optional CSS classes for the field.
     *
     * @return void
     */
    public static function transaction_expires_at_field($field, $membership_field, $created_at_field, $value = '', $id = '', $classes = '')
    {
        if (empty($id)) {
            $id = $field;
        }

        ?>
      <div
        id="<?php echo esc_attr($id); ?>"
        data-membership_field_id="<?php echo esc_attr($membership_field); ?>"
        data-created_at_field_id="<?php echo esc_attr($created_at_field); ?>"
        class="mepr_transaction_expires_field">
        <input type="text" name="<?php echo esc_attr($field); ?>"
          data-initial_raw_value="<?php echo esc_attr($value); ?>"
          value="<?php echo esc_attr(MeprAppHelper::format_date_utc($value, '', 'Y-m-d H:i:s')); ?>"
          class="regular-text mepr-date-picker mepr-expires-at"
        />
        <a href="" class="mepr-default-expiration-button button"><?php esc_html_e('Default', 'memberpress'); ?></a>
        <a href="" class="mepr-lifetime-expiration-button button"><?php esc_html_e('Lifetime', 'memberpress'); ?></a>
      </div>
        <?php
    }

    /**
     * Determines if the business net price is being charged.
     *
     * @return boolean True if charging business net price, false otherwise.
     */
    public static function is_charging_business_net_price()
    {
        $charge_business_customer_net_price = (bool) get_option('mepr_charge_business_customer_net_price', false);
        $tax_calc_type                      = get_option('mepr_tax_calc_type');
        $vat_tax_businesses                 = (bool) get_option('mepr_vat_tax_businesses');

        if (
            $charge_business_customer_net_price === true &&
            $tax_calc_type === 'inclusive' &&
            $vat_tax_businesses === false
        ) {
            return true;
        }

        return false;
    }

    /**
     * Gets the payment description for a transaction.
     *
     * @param MeprTransaction  $txn The transaction object.
     * @param MeprSubscription $sub The subscription object.
     * @param MeprProduct      $prd The product object.
     *
     * @return string The payment description.
     */
    protected static function get_payment_description($txn, $sub, $prd)
    {
        if ($sub instanceof MeprSubscription) {
            if ($txn->subscription_payment_index() <= 1) {
                $desc = __('Initial Payment', 'memberpress');
            } else {
                $desc = __('Subscription Payment', 'memberpress');
            }
        } else {
            $desc = __('Payment', 'memberpress') . MeprProductsHelper::renewal_str($prd);
        }

        return $desc;
    }

    /**
     * Retrieves invoice order bumps for a transaction.
     *
     * @param MeprTransaction  $txn         The transaction object.
     * @param MeprSubscription $tmpsub      Optional temporary subscription object.
     * @param array            $order_bumps The order bumps.
     *
     * @return string The invoice order bumps.
     */
    public static function get_invoice_order_bumps($txn, $tmpsub = '', $order_bumps = [])
    {
        $mepr_options = MeprOptions::fetch();
        $prd          = $txn->product();

        if (!empty($tmpsub) && $tmpsub instanceof MeprSubscription) {
            $sub = $tmpsub;
        } else {
            $sub = $txn->subscription();
        }

        if ($sub instanceof MeprSubscription && (($sub->trial && $sub->trial_days > 0 && $sub->txn_count < 1) || $txn->txn_type === MeprTransaction::$subscription_confirmation_str)) {
            self::set_invoice_txn_vars_from_sub($txn, $sub);
        }

        $desc                         = self::get_payment_description($txn, $sub, $prd);
        $show_negative_tax_on_invoice = get_option('mepr_show_negative_tax_on_invoice');

        /*
         * Base line amount and an empty coupon row. When the transaction or
         * subscription carries a coupon, the coupons module recomputes both
         * on the invoice filter below.
         */
        $amount     = $show_negative_tax_on_invoice && $txn->tax_reversal_amount > 0 ? $txn->amount + $txn->tax_reversal_amount : $txn->amount;
        $cpn_id     = 0;
        $cpn_desc   = '';
        $cpn_amount = 0.00;

        /*
         * Data-at-rest read for the templates: the readylaunch views derive
         * the coupon code from this object for the price terms display. The
         * guarded accessor degrades to false in brand builds without the
         * coupons feature.
         */
        $coupon = $txn->coupon();

        if (!$coupon && $sub instanceof MeprSubscription) {
            $coupon = $sub->coupon();
        }

        $items = [
            [
                'description' => $prd->post_title . '&nbsp;&ndash;&nbsp;' . $desc,
                'quantity'    => 1,
                'amount'      => $amount,
            ],
        ];

        $tax_amount = 0.00;
        $tax_items  = [];

        if ($txn->tax_rate > 0 || $txn->tax_amount > 0) {
            $txn_tax_amount = $show_negative_tax_on_invoice && $txn->tax_reversal_amount > 0 ? -1 * $txn->tax_reversal_amount : $txn->tax_amount;
            $tax_amount    += $txn_tax_amount;

            $tax_items[] = [
                'percent'    => $txn->tax_rate,
                'type'       => $txn->tax_desc,
                'amount'     => $txn_tax_amount,
                'post_title' => $prd->post_title,
            ];
        }

        foreach ($order_bumps as $order_bump) {
            list($product, $transaction, $subscription) = $order_bump;

            if ($subscription instanceof MeprSubscription && (($subscription->trial && $subscription->trial_days > 0 && $subscription->txn_count < 1) || $transaction->txn_type === MeprTransaction::$subscription_confirmation_str)) {
                self::set_invoice_txn_vars_from_sub($transaction, $subscription);
            }

            $order_bump_amount = $show_negative_tax_on_invoice && $transaction->tax_reversal_amount > 0 ? $transaction->amount + $transaction->tax_reversal_amount : $transaction->amount;

            if ($transaction->tax_rate > 0 || $transaction->tax_amount > 0) {
                $transaction_tax_amount = $show_negative_tax_on_invoice && $transaction->tax_reversal_amount > 0 ? -1 * $transaction->tax_reversal_amount : $transaction->tax_amount;
                $tax_amount            += $transaction_tax_amount;

                $tax_items[] = [
                    'percent'    => $transaction->tax_rate,
                    'type'       => $transaction->tax_desc,
                    'amount'     => $transaction_tax_amount,
                    'post_title' => $product->post_title,
                ];
            }

            $price_string = '';
            if ($product->register_price_action !== 'hidden' && MeprHooks::apply_filters('mepr_checkout_show_terms', true, $product)) {
                if (!$transaction->is_one_time_payment() && $subscription instanceof MeprSubscription && $subscription->id > 0) {
                    $price_string = MeprAppHelper::format_price_string($subscription, $subscription->price);
                } else {
                    ob_start();
                    MeprProductsHelper::display_invoice($product);
                    $price_string = ob_get_clean();
                }
            }

            $order_bump_title = $product->post_title;
            $order_bump_desc  = self::get_payment_description($transaction, $subscription, $product);

            if ('' !== $order_bump_desc) {
                $order_bump_title .= '&nbsp;&ndash;&nbsp;' . $order_bump_desc;
            }

            if ($price_string !== '') {
                $order_bump_title .= MeprHooks::apply_filters('mepr_order_bump_price_string', ' <br /><small>' . $price_string . '</small>');
            }

            $items[] = [
                'description' => $order_bump_title,
                'quantity'    => 1,
                'amount'      => $order_bump_amount,
            ];
        }

        $items = MeprHooks::apply_filters(
            'mepr_invoice_items_order_bumps',
            $items,
            $txn,
            $order_bumps
        );

        $invoice = MeprHooks::apply_filters(
            'mepr_invoice_order_bumps',
            [
                'items'      => $items,
                'coupon'     => [
                    'id'     => $cpn_id,
                    'desc'   => $cpn_desc,
                    'amount' => $cpn_amount,
                ],
                'tax_items'  => $tax_items,
                'tax_amount' => $tax_amount,
            ],
            $txn,
            $order_bumps,
            $sub
        );

        $show_quantity = MeprHooks::apply_filters('mepr_invoice_show_quantity', false, $txn);

        $quantities = [];
        foreach ($invoice['items'] as $item) {
            $quantities[] = $item['amount'];
        }

        $subtotal = (float)array_sum($quantities) - (float)$invoice['coupon']['amount'];
        $total    = $subtotal + $tax_amount;

        ob_start();

        if ($sub) {
            $prd = $sub->product();
            if (
                $prd->register_price_action === 'default' ||
                ($prd->register_price_action === 'custom' && MeprReadyLaunchCtrl::template_enabled('checkout') && $mepr_options->design_show_checkout_price_terms)
            ) {
                $sub_price_str = MeprSubscriptionsHelper::format_currency($sub);
            }

            if ($prd->register_price_action === 'custom' && !MeprReadyLaunchCtrl::template_enabled('checkout') && !empty($prd->register_price) && !$txn->coupon_id && !$txn->prorated) {
                $sub_price_str = stripslashes($prd->register_price);
            }
        }

        // Determine which invoice template to use (with order bumps).
        if (self::should_use_readylaunch_template()) {
            MeprView::render('/readylaunch/checkout/invoice_order_bumps', get_defined_vars());
        } else {
            MeprView::render('/checkout/invoice_order_bumps', get_defined_vars());
        }

        $invoice = ob_get_clean();
        // Wrap invoice in a div to maintain consistent DOM structure when replaced via AJAX.
        $invoice = '<div>' . $invoice . '</div>';
        return MeprHooks::apply_filters('mepr_invoice_html', $invoice, $txn);
    }

    /**
     * Get the tooltip attributes for the given transaction rec
     *
     * Displays the gateway transaction ID in a tooltip for multi-item purchases.
     *
     * @param  stdClass $rec The transaction record.
     * @return string
     */
    public static function get_tooltip_attributes($rec)
    {
        if (strpos($rec->trans_num, 'mi_') === 0 && $rec->order_trans_num) {
            preg_match('/(.*) \((.+)\)/', $rec->gateway, $matches);
            $gateway = !empty($matches[1]) ? $matches[1] : (!empty($matches[2]) ? $matches[2] : '');

            if (!empty($gateway)) {
                $tooltip_text = sprintf(
                    // Translators: %1$s: gateway name, %2$s: transaction ID.
                    __('%1$s Transaction ID: %2$s', 'memberpress'),
                    $gateway,
                    $rec->order_trans_num
                );
            } else {
                $tooltip_text = sprintf(
                    // Translators: %s: transaction ID.
                    __('Transaction ID: %s', 'memberpress'),
                    $rec->order_trans_num
                );
            }

            return sprintf(' data-tooltip-text="%s" data-order-trans-num="%s"', $tooltip_text, $rec->order_trans_num);
        }

        return '';
    }

    /**
     * Set the transaction vars from the given subscription
     *
     * Used when there is a trial period or confirmation transaction to be displayed in the invoice.
     *
     * @param MeprTransaction  $txn The transaction object.
     * @param MeprSubscription $sub The subscription object.
     */
    public static function set_invoice_txn_vars_from_sub(MeprTransaction $txn, MeprSubscription $sub)
    {
        if ($sub->trial && $sub->trial_days > 0) {
            $txn->amount              = $sub->trial_total - $sub->trial_tax_amount;
            $txn->total               = $sub->trial_total;
            $txn->tax_amount          = $sub->trial_tax_amount;
            $txn->tax_reversal_amount = $sub->trial_tax_reversal_amount;
        } else {
            $txn->amount              = $sub->price;
            $txn->total               = $sub->total;
            $txn->tax_amount          = $sub->tax_amount;
            $txn->tax_reversal_amount = $sub->tax_reversal_amount;
        }

        $txn->tax_rate  = $sub->tax_rate;
        $txn->tax_desc  = $sub->tax_desc;
        $txn->tax_class = $sub->tax_class;
    }

    /**
     * Gets the admin action links for a transaction record.
     *
     * Generates an array of action links available for a transaction in the admin interface.
     * Links are generated based on transaction status and capabilities. Each link includes
     * attributes for rendering and a priority for display order.
     *
     * @param object $rec Transaction record object.
     * @type  int     $id        Transaction ID
     * @type  string  $status    Transaction status
     * @type  string  $trans_num Transaction number
     * @type  string  $href        Link URL
     * @type  string  $title       Link title attribute
     * @type  string  $text        Link display text
     * @type  string  $class       Link CSS class(es)
     * @type  array   $data        Link data attributes
     * @type  string  $id          Link ID attribute
     * @type  string  $wrapper     Wrapper span class (optional)
     * @type  string  $wrapper_id  Wrapper span ID (optional)
     * @type  integer $priority    Display priority (lower numbers display first)
     *
     * @return array Array of link definitions.
     *
     * @filter mepr_admin_txn_row_action_links Filters the array of action links
     */
    public static function get_admin_action_links($rec)
    {
        $links = [];
        $txn   = new MeprTransaction($rec->id);
        if (!$txn->id) {
            return $links;
        }
        // Edit link (always shown).
        $links['edit'] = [
            'href'     => admin_url('admin.php?page=memberpress-trans&action=edit&id=' . $rec->id),
            'title'    => __('Edit transaction', 'memberpress'),
            'text'     => __('Edit', 'memberpress'),
            'class'    => '',
            'data'     => [],
            'id'       => 'edit-txn-' . $rec->id,
            'priority' => 10,
        ];

        // Non-pending/failed transaction flow.
        if (!in_array($rec->status, [MeprTransaction::$pending_str, MeprTransaction::$failed_str], true)) {
            $links['send_receipt'] = [
                'href'     => '',
                'text'     => __('Send Receipt', 'memberpress'),
                'class'    => 'mepr_resend_txn_email',
                'data'     => ['value' => $rec->id],
                'id'       => 'send-receipt-' . $rec->id,
                'priority' => 20,
            ];

            $links['send_welcome'] = [
                'href'     => '',
                'text'     => __('Send Welcome', 'memberpress'),
                'class'    => 'mepr_send_welcome_email',
                'data'     => ['value' => $rec->id],
                'id'       => 'send-welcome-' . $rec->id,
                'priority' => 30,
            ];
        }

        // Refund capabilities.
        if ($txn->can('process-refunds')) {
            $links['refund'] = [
                'href'       => '',
                'title'      => __('Refund Transaction', 'memberpress'),
                'text'       => __('Refund', 'memberpress'),
                'class'      => 'mepr-refund-txn',
                'data'       => ['value' => $rec->id],
                'wrapper'    => 'mepr-refund-txn-action',
                'wrapper_id' => 'refund-wrapper-' . $rec->id,
                'id'         => 'refund-txn-' . $rec->id,
                'priority'   => 40,
            ];

            $sub = $txn->subscription();
            if ($sub && $sub->status === MeprSubscription::$active_str && $sub->can('cancel-subscriptions')) {
                $links['refund_and_cancel'] = [
                    'href'       => '',
                    'title'      => __('Refund Transaction and Cancel Subscription', 'memberpress'),
                    'text'       => __('Refund & Cancel', 'memberpress'),
                    'class'      => 'mepr-refund-txn-and-cancel-sub',
                    'data'       => ['value' => $rec->id],
                    'wrapper'    => 'mepr-refund-txn-and-cancel-sub-action',
                    'wrapper_id' => 'refund-cancel-wrapper-' . $rec->id,
                    'id'         => 'refund-cancel-txn-' . $rec->id,
                    'priority'   => 50,
                ];
            }
        }

        // Delete link (always shown).
        $links['delete'] = [
            'href'     => '',
            'title'    => __('Delete Transaction', 'memberpress'),
            'text'     => __('Delete', 'memberpress'),
            'class'    => 'remove-txn-row',
            'data'     => ['value' => $rec->id],
            'id'       => 'delete-txn-' . $rec->id,
            'priority' => 60,
        ];

        $links = (array) MeprHooks::apply_filters('mepr_admin_txn_row_action_links', $links, $rec, $txn);

        return $links;
    }

    /**
     * Gets the date filter fields for transactions.
     *
     * @param  boolean $keys_only Whether to return only field names (true) or full array with labels (false).
     * @return array Array of field names or field => label pairs.
     */
    public static function get_date_filter_fields($keys_only = false): array
    {
        $date_fields = MeprHooks::apply_filters('mepr_transactions_date_filter_fields', [
            'created_at'  => __('Created On', 'memberpress'),
            'expires_at'  => __('Expires On', 'memberpress'),
            'refunded_at' => __('Refunded On', 'memberpress'),
        ]);

        return $keys_only ? array_keys($date_fields) : $date_fields;
    }

    /**
     * Get the date range filter options.
     *
     * @param  boolean $keys_only Whether to return only field names (true) or full array with labels (false).
     * @return array Array of date range filter options.
     */
    public static function get_date_range_filter_options($keys_only = false): array
    {
        $date_range_filter_options = MeprHooks::apply_filters('mepr_transactions_date_range_filter_options', [
            'all'        => __('All Dates', 'memberpress'),
            'today'      => __('Today', 'memberpress'),
            'yesterday'  => __('Yesterday', 'memberpress'),
            'this_week'  => __('This Week', 'memberpress'),
            'last_week'  => __('Last Week', 'memberpress'),
            'this_month' => __('This Month', 'memberpress'),
            'last_month' => __('Last Month', 'memberpress'),
            'this_year'  => __('This Year', 'memberpress'),
            'last_year'  => __('Last Year', 'memberpress'),
            'custom'     => __('Custom', 'memberpress'),
        ]);

        return $keys_only ? array_keys($date_range_filter_options) : $date_range_filter_options;
    }
    /**
     * Displays a field for transaction refunded date.
     *
     * @param  string $field The field name.
     * @param  string $value The field value.
     * @param  string $id    The field ID.
     * @return void
     */
    public static function transaction_refunded_at_field($field, $value = '', $id = '')
    {
        if (empty($id)) {
            $id = $field;
        }

        ?>
        <div id="<?php echo esc_attr($id); ?>" class="mepr_transaction_refunded_at_field">
            <input
                type="text"
                name="<?php echo esc_attr($field); ?>"
                value="<?php echo esc_attr(MeprAppHelper::format_date_utc($value, '', 'Y-m-d H:i:s')); ?>"
          class="regular-text mepr-date-picker mepr-refunded-at"
        />
            <a href="" class="mepr-today-button button"><?php esc_html_e('Now', 'memberpress'); ?></a>
        </div>
        <?php
    }

    /**
     * Retrieves a transaction for the thank you page.
     *
     * @param  string  $trans_num      Optional transaction number.
     * @param  integer $transaction_id Optional transaction ID.
     * @return MeprTransaction|null The transaction object or null if not found.
     */
    public static function get_thankyou_transaction($trans_num = '', $transaction_id = 0)
    {
        // Get transaction from parameters or $_GET.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public thank-you page; params come from payment gateway redirects which cannot include a nonce.
        if (empty($trans_num) && isset($_GET['trans_num'])) {
            $trans_num = sanitize_text_field(wp_unslash($_GET['trans_num']));
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        if (empty($transaction_id) && isset($_GET['transaction_id'])) {
            $transaction_id = (int) $_GET['transaction_id'];
        }

        $txn = !empty($trans_num) ? MeprTransaction::get_one_by_trans_num($trans_num) : null;
        $txn = null === $txn && !empty($transaction_id) ? MeprTransaction::get_one($transaction_id) : $txn;

        return !empty($txn->id) ? new MeprTransaction($txn->id) : null;
    }

    /**
     * Gets the thank you page content.
     *
     * @param  string  $trans_num      Optional transaction number.
     * @param  integer $transaction_id Optional transaction ID.
     * @return string The thank you page HTML content.
     */
    public static function get_thankyou_content($trans_num = '', $transaction_id = 0)
    {
        $txn = self::get_thankyou_transaction($trans_num, $transaction_id);

        if ($txn instanceof MeprTransaction && !empty($txn->id)) {
            $mepr_options      = MeprOptions::fetch();
            $hide_invoice      = $mepr_options->design_thankyou_hide_invoice;
            $invoice_message   = do_shortcode($mepr_options->design_thankyou_invoice_message);
            $has_welcome_image = $mepr_options->design_show_thankyou_welcome_image;
            $welcome_image     = esc_url(wp_get_attachment_url($mepr_options->design_thankyou_welcome_img));
            $order             = $txn->order();

            if ($order instanceof MeprOrder) {
                $order_bump_transactions = MeprTransaction::get_all_by_order_id_and_gateway($order->id, $txn->gateway, $txn->id);
                $transactions            = array_merge([$txn], $order_bump_transactions);
                $processed_sub_ids       = [];
                $order_bumps             = [];
                $amount                  = 0.00;

                foreach ($transactions as $index => $transaction) {
                    if ($transaction->is_one_time_payment()) {
                        $amount += (float) $transaction->total;

                        if ($index > 0) {
                            $order_bumps[] = [$transaction->product(), $transaction, null];
                        }
                    } else {
                        $subscription = $transaction->subscription();

                        if ($subscription instanceof MeprSubscription) {
                            // Subs can have both a payment txn and confirmation txn, make sure we don't process a sub twice.
                            if (in_array((int) $subscription->id, $processed_sub_ids, true)) {
                                continue;
                            }

                            $processed_sub_ids[] = (int) $subscription->id;

                            if (($subscription->trial && $subscription->trial_days > 0 && $subscription->txn_count < 1) || $transaction->txn_type === MeprTransaction::$subscription_confirmation_str) {
                                self::set_invoice_txn_vars_from_sub($transaction, $subscription);
                            }

                            $amount += (float) $transaction->total;

                            if ($index > 0) {
                                $order_bumps[] = [$transaction->product(), $transaction, $subscription];
                            }
                        }
                    }
                }

                $amount       = MeprAppHelper::format_currency($amount);
                $trans_num    = $order->trans_num;
                $invoice_html = self::get_invoice_order_bumps($txn, '', $order_bumps);
            } else {
                $sub = $txn->subscription();

                if ($sub instanceof MeprSubscription && (($sub->trial && $sub->trial_days > 0 && $sub->txn_count < 1) || $txn->txn_type === MeprTransaction::$subscription_confirmation_str)) {
                    self::set_invoice_txn_vars_from_sub($txn, $sub);
                }

                $amount = strtok(MeprAppHelper::format_price_string($txn, $txn->amount), ' ');

                $trans_num    = $txn->trans_num;
                $invoice_html = self::get_invoice($txn);
            }

            return MeprView::get_string('/readylaunch/thankyou', get_defined_vars());
        } else {
            return '<p>' . esc_html__('Transaction not found', 'memberpress') . '</p>';
        }
    }
}
