items = $this->read_storage(); } public function all(): array { return $this->items; } public function add(int $download_id, array $options = []): void { // Quantity capped at 1 for now $price_id = isset($options['price_id']) ? (int) $options['price_id'] : null; $price = $this->get_item_price($download_id, $price_id); $this->items[(string)$download_id] = [ 'download_id' => $download_id, 'qty' => 1, 'price_id' => $price_id, 'price' => $price, ]; $this->write_storage(); do_action('ToDownload_cart_item_added', $download_id, $this->items); } public function remove(int $download_id): void { unset($this->items[(string)$download_id]); $this->write_storage(); do_action('ToDownload_cart_item_removed', $download_id, $this->items); } public function clear(): void { $this->items = []; $this->write_storage(); do_action('ToDownload_cart_cleared'); } public function subtotal(): float { $sum = 0.0; foreach ($this->items as $it) { $price = isset($it['price']) ? (float) $it['price'] : 0.0; $sum += max(0.0, $price); } return (float) apply_filters('ToDownload_cart_subtotal', $sum, $this->items); } public function tax(): float { // v1: global tax with optional seller-level override (single-seller checkout) $settings = get_option('ToDownload_settings', []); $enabled = is_array($settings) && !empty($settings['enable_tax']); $rate = is_array($settings) ? (float) ($settings['tax_percent'] ?? 0) : 0.0; $rate = max(0.0, min(100.0, $rate)); $seller_user_id = $this->get_single_seller_user_id_from_cart(); if ($seller_user_id > 0) { $seller_enabled = Sellers::seller_tax_enabled($seller_user_id); $seller_rate = Sellers::seller_tax_percent($seller_user_id); if ($seller_enabled !== null) { $enabled = $seller_enabled; } if ($seller_rate !== null) { $rate = $seller_rate; } } $tax = 0.0; if ($enabled && $rate > 0) { $tax = $this->subtotal() * ($rate / 100.0); $tax = (float) round($tax, 2); } return (float) apply_filters('ToDownload_cart_tax', $tax, $this->items); } private function get_single_seller_user_id_from_cart(): int { $seller_user_id = 0; foreach ($this->items as $it) { $download_id = (int) ($it['download_id'] ?? 0); if ($download_id <= 0) { continue; } $author_id = (int) get_post_field('post_author', $download_id); if ($author_id <= 0) { continue; } if ($seller_user_id === 0) { $seller_user_id = $author_id; continue; } if ($seller_user_id !== $author_id) { // Multi-seller carts are blocked elsewhere in v1; treat as no override. return 0; } } return $seller_user_id; } public function discount(): float { $discount = 0.0; return (float) apply_filters('ToDownload_cart_discount', $discount, $this->items); } public function total(): float { $total = max(0.0, $this->subtotal() + $this->tax() - $this->discount()); return (float) apply_filters('ToDownload_cart_total', $total, $this->items); } private function read_storage(): array { $user_id = is_user_logged_in() ? (int) get_current_user_id() : 0; if ($user_id > 0) { $raw = get_user_meta($user_id, self::USER_META, true); if (is_array($raw) && !empty($raw)) { return $raw; } } if (!empty($_COOKIE[self::COOKIE])) { $json = sanitize_text_field(wp_unslash((string) $_COOKIE[self::COOKIE])); $data = json_decode($json, true); if (is_array($data)) { // If the user is logged in but meta is empty/unset, hydrate it from cookie. if ($user_id > 0) { update_user_meta($user_id, self::USER_META, $data); } return $data; } } return []; } private function write_storage(): void { $payload = wp_json_encode($this->items); // Cookie for guests and as a fallback $cookie_args = [ 'expires' => time() + DAY_IN_SECONDS * 7, 'path' => COOKIEPATH ?: '/', 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ]; // Prefer a host-scoped cookie to avoid cross-tenant bleed and misconfigured COOKIE_DOMAIN. $host = (string) (wp_parse_url(home_url('/'), PHP_URL_HOST) ?? ''); $host = strtolower(trim($host)); if ($host !== '') { $cookie_args['domain'] = $host; } setcookie(self::COOKIE, $payload, $cookie_args); $_COOKIE[self::COOKIE] = $payload; if (is_user_logged_in()) { update_user_meta(get_current_user_id(), self::USER_META, $this->items); } } }