post_type !== self::POST_TYPE) { return; } if ($new_status === $old_status) { return; } if (wp_is_post_revision((int) $post->ID)) { return; } // Keep hosted canonical status in sync with publish state. $force = ($new_status === 'publish') ? 'active' : 'inactive'; self::sync_discount_to_hosted((int) $post->ID, $post, null, $force); } public static function on_trash_post(int $post_id): void { $post_id = (int) $post_id; if ($post_id <= 0) { return; } $post = get_post($post_id); if (!($post instanceof \WP_Post) || $post->post_type !== self::POST_TYPE) { return; } self::sync_discount_to_hosted((int) $post->ID, $post, null, 'inactive'); } public static function on_before_delete_post(int $post_id): void { $post_id = (int) $post_id; if ($post_id <= 0) { return; } $post = get_post($post_id); if (!($post instanceof \WP_Post) || $post->post_type !== self::POST_TYPE) { return; } self::sync_discount_to_hosted((int) $post->ID, $post, null, 'inactive'); } public static function enqueue_admin_styles(): void { $screen = get_current_screen(); if ($screen && $screen->post_type === self::POST_TYPE) { $css_ver = ToDownload_VERSION; $css_path = ToDownload_PLUGIN_DIR . 'assets/admin-datepicker.css'; if (is_readable($css_path)) { $mtime = filemtime($css_path); if ($mtime) { $css_ver = (string) $mtime; } } wp_enqueue_style( 'todownload-admin-datepicker', ToDownload_PLUGIN_URL . 'assets/admin-datepicker.css', ['wp-jquery-ui-dialog'], $css_ver ); $js_ver = ToDownload_VERSION; $js_path = ToDownload_PLUGIN_DIR . 'assets/admin-discounts-metabox.js'; if (is_readable($js_path)) { $mtime = filemtime($js_path); if ($mtime) { $js_ver = (string) $mtime; } } wp_enqueue_script( 'todownload-admin-discounts-metabox', ToDownload_PLUGIN_URL . 'assets/admin-discounts-metabox.js', [], $js_ver, true ); } } public static function register_post_type(): void { register_post_type(self::POST_TYPE, [ 'labels' => [ 'name' => __('Discount Codes', '2download-connector'), 'singular_name' => __('Discount', '2download-connector'), 'menu_name' => __('Discount Code', '2download-connector'), 'add_new' => __('Add Discount', '2download-connector'), 'add_new_item' => __('Add Discount', '2download-connector'), 'edit_item' => __('Edit Discount', '2download-connector'), 'new_item' => __('New Discount', '2download-connector'), 'view_item' => __('View Discount', '2download-connector'), 'search_items' => __('Search Discounts', '2download-connector'), 'not_found' => __('No discounts found.', '2download-connector'), 'not_found_in_trash' => __('No discounts found in Trash.', '2download-connector'), 'all_items' => __('Discount Codes', '2download-connector'), ], 'public' => false, 'show_ui' => true, 'show_in_menu' => 'ToDownload_seller', 'supports' => ['title', 'author'], 'capability_type' => 'todownload_discount', 'map_meta_cap' => true, 'show_in_rest' => true, 'menu_position' => 58, ]); } public static function register_metaboxes(): void { add_meta_box( 'ToDownload_discount_settings', __('Discount Settings', '2download-connector'), [self::class, 'render_metabox'], self::POST_TYPE, 'normal', 'default' ); } private static function meta_int(int $post_id, string $key): int { return (int) get_post_meta($post_id, $key, true); } private static function meta_str(int $post_id, string $key): string { return (string) get_post_meta($post_id, $key, true); } public static function render_metabox($post): void { wp_nonce_field('ToDownload_discount_save', 'ToDownload_discount_nonce'); $code = self::meta_str($post->ID, '_ToDownload_discount_code'); $apply_mode = self::meta_str($post->ID, '_ToDownload_discount_apply_mode') ?: 'code'; $type = self::meta_str($post->ID, '_ToDownload_discount_type') ?: 'percent'; $amount = self::meta_str($post->ID, '_ToDownload_discount_amount'); $min_subtotal = self::meta_str($post->ID, '_ToDownload_discount_min_subtotal'); $min_quantity = self::meta_int($post->ID, '_ToDownload_discount_min_quantity'); $max_discount = self::meta_str($post->ID, '_ToDownload_discount_max_discount'); $cart_rule = self::meta_str($post->ID, '_ToDownload_discount_cart_rule'); $rule_quantity_basis = self::meta_str($post->ID, '_ToDownload_discount_rule_quantity_basis') ?: 'eligible'; $tier_rules_text = self::meta_str($post->ID, '_ToDownload_discount_tier_rules_text'); $cheapest_item_quantity = self::meta_int($post->ID, '_ToDownload_discount_cheapest_item_quantity'); $cheapest_item_percent_off = self::meta_str($post->ID, '_ToDownload_discount_cheapest_item_percent_off'); $bundle_set_quantity = self::meta_int($post->ID, '_ToDownload_discount_bundle_set_quantity'); $bogo_buy_quantity = self::meta_int($post->ID, '_ToDownload_discount_bogo_buy_quantity'); $bogo_get_quantity = self::meta_int($post->ID, '_ToDownload_discount_bogo_get_quantity'); $bogo_get_percent_off = self::meta_str($post->ID, '_ToDownload_discount_bogo_get_percent_off'); $product_ids = self::meta_str($post->ID, '_ToDownload_discount_product_ids'); $selected_ids = self::parse_ids_csv($product_ids); $excluded_product_ids = self::meta_str($post->ID, '_ToDownload_discount_excluded_product_ids'); $selected_excluded_ids = self::parse_ids_csv($excluded_product_ids); $required_prior_product_ids = self::meta_str($post->ID, '_ToDownload_discount_required_prior_product_ids'); $selected_required_prior_ids = self::parse_ids_csv($required_prior_product_ids); $allowed_customer_emails = self::meta_str($post->ID, '_ToDownload_discount_allowed_customer_emails'); $usage_limit = self::meta_int($post->ID, '_ToDownload_discount_usage_limit'); $usage_limit_per_customer = self::meta_int($post->ID, '_ToDownload_discount_usage_limit_per_customer'); $usage_count = self::meta_int($post->ID, '_ToDownload_discount_usage_count'); $expires = self::meta_str($post->ID, '_ToDownload_discount_expires'); $returning_only = self::meta_int($post->ID, '_ToDownload_discount_returning_customers_only') === 1; $first_time_only = self::meta_int($post->ID, '_ToDownload_discount_first_time_customers_only') === 1; $eligibility = $returning_only ? 'returning' : ($first_time_only ? 'first_time' : 'any'); ?>

































:

$id > 0))); $product_ids = implode(',', $ids); } else { $product_ids = sanitize_text_field((string) $product_ids_raw); } $usage_limit = isset($post_data['ToDownload_discount_usage_limit']) ? absint($post_data['ToDownload_discount_usage_limit']) : 0; $usage_limit_per_customer = isset($post_data['ToDownload_discount_usage_limit_per_customer']) ? absint($post_data['ToDownload_discount_usage_limit_per_customer']) : 0; $eligibility = sanitize_text_field((string) ($post_data['ToDownload_discount_customer_eligibility'] ?? 'any')); $expires = sanitize_text_field((string) ($post_data['ToDownload_discount_expires'] ?? '')); $excluded_product_ids_raw = $post_data['ToDownload_discount_excluded_product_ids'] ?? ''; $excluded_product_ids = ''; if (is_array($excluded_product_ids_raw)) { $ids = array_values(array_filter(array_map('intval', $excluded_product_ids_raw))); $ids = array_values(array_unique(array_filter($ids, fn($id) => $id > 0))); $excluded_product_ids = implode(',', $ids); } else { $excluded_product_ids = sanitize_text_field((string) $excluded_product_ids_raw); } $required_prior_product_ids_raw = $post_data['ToDownload_discount_required_prior_product_ids'] ?? ''; $required_prior_product_ids = ''; if (is_array($required_prior_product_ids_raw)) { $ids = array_values(array_filter(array_map('intval', $required_prior_product_ids_raw))); $ids = array_values(array_unique(array_filter($ids, fn($id) => $id > 0))); $required_prior_product_ids = implode(',', $ids); } else { $required_prior_product_ids = sanitize_text_field((string) $required_prior_product_ids_raw); } $allowed_customer_emails = isset($post_data['ToDownload_discount_allowed_customer_emails']) ? sanitize_textarea_field((string) $post_data['ToDownload_discount_allowed_customer_emails']) : ''; update_post_meta($post_id, '_ToDownload_discount_code', $code); update_post_meta($post_id, '_ToDownload_discount_apply_mode', in_array($apply_mode, ['code', 'auto'], true) ? $apply_mode : 'code'); update_post_meta($post_id, '_ToDownload_discount_type', in_array($type, ['percent', 'flat', 'fixed_price', 'bogo'], true) ? $type : 'percent'); update_post_meta($post_id, '_ToDownload_discount_amount', $amount); update_post_meta($post_id, '_ToDownload_discount_min_subtotal', $min_subtotal); update_post_meta($post_id, '_ToDownload_discount_min_quantity', $min_quantity); update_post_meta($post_id, '_ToDownload_discount_max_discount', $max_discount); update_post_meta($post_id, '_ToDownload_discount_bogo_buy_quantity', $bogo_buy_quantity); update_post_meta($post_id, '_ToDownload_discount_bogo_get_quantity', $bogo_get_quantity); update_post_meta($post_id, '_ToDownload_discount_bogo_get_percent_off', $bogo_get_percent_off); update_post_meta($post_id, '_ToDownload_discount_cart_rule', in_array($cart_rule, ['tiered', 'cheapest', 'bundle'], true) ? $cart_rule : ''); update_post_meta($post_id, '_ToDownload_discount_rule_quantity_basis', in_array($rule_quantity_basis, ['eligible', 'total'], true) ? $rule_quantity_basis : 'eligible'); update_post_meta($post_id, '_ToDownload_discount_tier_rules_text', $tier_rules_text); update_post_meta($post_id, '_ToDownload_discount_cheapest_item_quantity', $cheapest_item_quantity); update_post_meta($post_id, '_ToDownload_discount_cheapest_item_percent_off', $cheapest_item_percent_off); update_post_meta($post_id, '_ToDownload_discount_bundle_set_quantity', $bundle_set_quantity); update_post_meta($post_id, '_ToDownload_discount_product_ids', $product_ids); update_post_meta($post_id, '_ToDownload_discount_excluded_product_ids', $excluded_product_ids); update_post_meta($post_id, '_ToDownload_discount_required_prior_product_ids', $required_prior_product_ids); update_post_meta($post_id, '_ToDownload_discount_allowed_customer_emails', $allowed_customer_emails); update_post_meta($post_id, '_ToDownload_discount_usage_limit', $usage_limit); update_post_meta($post_id, '_ToDownload_discount_usage_limit_per_customer', $usage_limit_per_customer); update_post_meta($post_id, '_ToDownload_discount_returning_customers_only', $eligibility === 'returning' ? 1 : 0); update_post_meta($post_id, '_ToDownload_discount_first_time_customers_only', $eligibility === 'first_time' ? 1 : 0); update_post_meta($post_id, '_ToDownload_discount_expires', $expires); // Sync to hosted platform so 2dl.app is the canonical discount system. self::sync_discount_to_hosted((int) $post_id, $post, [ 'code' => $code, 'apply_mode' => $apply_mode, 'type' => $type, 'amount' => $amount, 'min_subtotal' => $min_subtotal, 'min_quantity' => (string) $min_quantity, 'max_discount' => $max_discount, 'bogo_buy_quantity' => (string) $bogo_buy_quantity, 'bogo_get_quantity' => (string) $bogo_get_quantity, 'bogo_get_percent_off' => $bogo_get_percent_off, 'cart_rule' => $cart_rule, 'rule_quantity_basis' => $rule_quantity_basis, 'tier_rules_text' => $tier_rules_text, 'cheapest_item_quantity' => $cheapest_item_quantity, 'cheapest_item_percent_off' => $cheapest_item_percent_off, 'bundle_set_quantity' => $bundle_set_quantity, 'usage_limit' => $usage_limit, 'usage_limit_per_customer' => $usage_limit_per_customer, 'customer_eligibility' => $eligibility, 'expires' => $expires, ], null); } private static function parse_emails_text(string $raw): array { $raw = trim($raw); if ($raw === '') { return []; } $raw = str_replace("\r", "\n", $raw); $parts = preg_split('/\n|,/', $raw); if (!is_array($parts)) { return []; } $out = []; $seen = []; foreach ($parts as $p) { $e = strtolower(trim((string) $p)); if ($e === '' || !is_email($e)) { continue; } if (isset($seen[$e])) { continue; } $seen[$e] = true; $out[] = $e; if (count($out) >= 500) { break; } } return $out; } /** * @param int[] $wp_download_ids * @return array Map of wp_download_id => hosted product publicId */ private static function hosted_public_id_map_for_wp_download_ids(string $hosted_base, string $store_public_id, string $api_key, string $request_id, array $wp_download_ids): array { $wp_download_ids = array_values(array_unique(array_filter(array_map('intval', $wp_download_ids), fn($id) => $id > 0))); if (empty($wp_download_ids)) { return []; } $hosted_products = []; $products_res = HostedClient::get_json( $hosted_base, '/api/v1/stores/' . rawurlencode($store_public_id) . '/products', $api_key, $request_id ); if (!empty($products_res['ok']) && isset($products_res['data']) && is_array($products_res['data'])) { $hosted_products = is_array($products_res['data']['products'] ?? null) ? (array) $products_res['data']['products'] : []; } $external_to_public = []; foreach ($hosted_products as $p) { if (!is_array($p)) { continue; } $ext = isset($p['externalId']) ? (string) $p['externalId'] : ''; $pub = isset($p['publicId']) ? (string) $p['publicId'] : ''; $ext = trim($ext); $pub = trim($pub); if ($ext !== '' && $pub !== '') { $external_to_public[$ext] = $pub; } } $out = []; foreach ($wp_download_ids as $wp_download_id) { $wp_download_id = (int) $wp_download_id; $external_id = 'wp_download_' . (string) $wp_download_id; if (isset($external_to_public[$external_id])) { $out[$wp_download_id] = (string) $external_to_public[$external_id]; continue; } $title = get_the_title($wp_download_id); $name = is_string($title) && trim($title) !== '' ? trim($title) : ('WP Download #' . (string) $wp_download_id); $price_raw = (string) get_post_meta($wp_download_id, '_ToDownload_price', true); $price = (float) $price_raw; $amount_minor = (int) round(max(0.0, $price) * 100); if ($amount_minor <= 0) { continue; } $create_res = HostedClient::post_json( $hosted_base, '/api/v1/stores/' . rawurlencode($store_public_id) . '/products', [ 'external_id' => $external_id, 'name' => $name, 'currency' => 'usd', 'amount_minor' => $amount_minor, 'status' => 'active', ], $api_key, $request_id ); if (!empty($create_res['ok']) && isset($create_res['product']) && is_array($create_res['product'])) { $created = (array) $create_res['product']; $pub = isset($created['public_id']) ? trim((string) $created['public_id']) : ''; if ($pub !== '') { $out[$wp_download_id] = $pub; $external_to_public[$external_id] = $pub; } } } return $out; } /** * @param array{code:string,apply_mode?:string,type:string,amount:string,min_subtotal?:string,min_quantity?:string,max_discount?:string,bogo_buy_quantity?:string,bogo_get_quantity?:string,bogo_get_percent_off?:string,cart_rule?:string,rule_quantity_basis?:string,tier_rules_text?:string,cheapest_item_quantity?:int,cheapest_item_percent_off?:string,bundle_set_quantity?:int,usage_limit:int,usage_limit_per_customer?:int,customer_eligibility?:string,expires:string}|null $data */ private static function sync_discount_to_hosted(int $post_id, \WP_Post $post, ?array $data, ?string $force_status): void { $code = ''; if (is_array($data) && isset($data['code'])) { $code = strtoupper(trim((string) $data['code'])); } if ($code === '') { $code = (string) get_post_meta($post_id, '_ToDownload_discount_code', true); $code = strtoupper(trim($code)); } if ($code === '') { return; } $seller_user_id = (int) ($post->post_author ?? 0); if ($seller_user_id <= 0) { return; } $hosted = Sellers::hosted_credentials($seller_user_id); if (empty($hosted['connected']) || empty($hosted['store_public_id']) || empty($hosted['api_key'])) { return; } $hosted_base = (string) ($hosted['hosted_base'] ?? 'https://2dl.app'); $hosted_base = $hosted_base !== '' ? rtrim($hosted_base, '/') : 'https://2dl.app'; $store_public_id = trim((string) ($hosted['store_public_id'] ?? '')); $api_key = trim((string) ($hosted['api_key'] ?? '')); if ($store_public_id === '' || $api_key === '') { return; } $apply_mode = is_array($data) ? (string) ($data['apply_mode'] ?? '') : ''; $type = is_array($data) ? (string) ($data['type'] ?? '') : ''; $amount = is_array($data) ? (string) ($data['amount'] ?? '') : ''; $min_subtotal = is_array($data) ? (string) ($data['min_subtotal'] ?? '') : ''; $min_quantity = is_array($data) ? (string) ($data['min_quantity'] ?? '') : ''; $max_discount = is_array($data) ? (string) ($data['max_discount'] ?? '') : ''; $bogo_buy_quantity = is_array($data) ? (string) ($data['bogo_buy_quantity'] ?? '') : ''; $bogo_get_quantity = is_array($data) ? (string) ($data['bogo_get_quantity'] ?? '') : ''; $bogo_get_percent_off = is_array($data) ? (string) ($data['bogo_get_percent_off'] ?? '') : ''; $cart_rule = is_array($data) ? (string) ($data['cart_rule'] ?? '') : ''; $rule_quantity_basis = is_array($data) ? (string) ($data['rule_quantity_basis'] ?? '') : ''; $tier_rules_text = is_array($data) ? (string) ($data['tier_rules_text'] ?? '') : ''; $cheapest_item_quantity = is_array($data) ? (int) ($data['cheapest_item_quantity'] ?? 0) : 0; $cheapest_item_percent_off = is_array($data) ? (string) ($data['cheapest_item_percent_off'] ?? '') : ''; $bundle_set_quantity = is_array($data) ? (int) ($data['bundle_set_quantity'] ?? 0) : 0; $usage_limit = is_array($data) ? (int) ($data['usage_limit'] ?? 0) : 0; $usage_limit_per_customer = is_array($data) ? (int) ($data['usage_limit_per_customer'] ?? 0) : 0; $customer_eligibility = is_array($data) ? (string) ($data['customer_eligibility'] ?? '') : ''; $expires = is_array($data) ? trim((string) ($data['expires'] ?? '')) : ''; if ($type === '' || $amount === '') { $apply_mode = (string) get_post_meta($post_id, '_ToDownload_discount_apply_mode', true); $type = (string) get_post_meta($post_id, '_ToDownload_discount_type', true); $amount = (string) get_post_meta($post_id, '_ToDownload_discount_amount', true); $min_subtotal = (string) get_post_meta($post_id, '_ToDownload_discount_min_subtotal', true); $min_quantity = (string) get_post_meta($post_id, '_ToDownload_discount_min_quantity', true); $max_discount = (string) get_post_meta($post_id, '_ToDownload_discount_max_discount', true); $bogo_buy_quantity = (string) get_post_meta($post_id, '_ToDownload_discount_bogo_buy_quantity', true); $bogo_get_quantity = (string) get_post_meta($post_id, '_ToDownload_discount_bogo_get_quantity', true); $bogo_get_percent_off = (string) get_post_meta($post_id, '_ToDownload_discount_bogo_get_percent_off', true); $cart_rule = (string) get_post_meta($post_id, '_ToDownload_discount_cart_rule', true); $rule_quantity_basis = (string) get_post_meta($post_id, '_ToDownload_discount_rule_quantity_basis', true); $tier_rules_text = (string) get_post_meta($post_id, '_ToDownload_discount_tier_rules_text', true); $cheapest_item_quantity = (int) get_post_meta($post_id, '_ToDownload_discount_cheapest_item_quantity', true); $cheapest_item_percent_off = (string) get_post_meta($post_id, '_ToDownload_discount_cheapest_item_percent_off', true); $bundle_set_quantity = (int) get_post_meta($post_id, '_ToDownload_discount_bundle_set_quantity', true); $usage_limit = (int) get_post_meta($post_id, '_ToDownload_discount_usage_limit', true); $usage_limit_per_customer = (int) get_post_meta($post_id, '_ToDownload_discount_usage_limit_per_customer', true); $customer_eligibility = (string) get_post_meta($post_id, '_ToDownload_discount_returning_customers_only', true) === '1' ? 'returning' : (((string) get_post_meta($post_id, '_ToDownload_discount_first_time_customers_only', true) === '1') ? 'first_time' : 'any'); $expires = (string) get_post_meta($post_id, '_ToDownload_discount_expires', true); } if (!in_array($apply_mode, ['code', 'auto'], true)) { $apply_mode = 'code'; } if (!in_array($type, ['percent', 'flat', 'fixed_price', 'bogo'], true)) { $type = 'percent'; } $amount_f = (float) $amount; $min_subtotal_f = (float) $min_subtotal; $min_quantity_i = (int) $min_quantity; $max_discount_f = (float) $max_discount; $bogo_buy_i = (int) $bogo_buy_quantity; $bogo_get_i = (int) $bogo_get_quantity; $bogo_pct_f = (float) $bogo_get_percent_off; $request_id = function_exists('wp_generate_uuid4') ? (string) wp_generate_uuid4() : (string) uniqid('2dl_', true); $payload = [ 'code' => $code, 'status' => ($force_status === 'active' || $force_status === 'inactive') ? $force_status : (($post->post_status === 'publish') ? 'active' : 'inactive'), 'apply_mode' => $apply_mode, ]; // Product targeting: sync WP download selections to hosted product_public_ids. // We mirror WP downloads as hosted Products keyed by external_id = wp_download_{POST_ID}. $include_ids = self::parse_ids_csv((string) get_post_meta($post_id, '_ToDownload_discount_product_ids', true)); $exclude_ids = self::parse_ids_csv((string) get_post_meta($post_id, '_ToDownload_discount_excluded_product_ids', true)); $required_prior_ids = self::parse_ids_csv((string) get_post_meta($post_id, '_ToDownload_discount_required_prior_product_ids', true)); $all = array_values(array_unique(array_merge($include_ids, $exclude_ids, $required_prior_ids))); $map = !empty($all) ? self::hosted_public_id_map_for_wp_download_ids($hosted_base, $store_public_id, $api_key, $request_id, $all) : []; $payload['product_public_ids'] = array_values(array_unique(array_values(array_filter(array_map(fn($id) => $map[(int) $id] ?? '', $include_ids))))); $payload['excluded_product_public_ids'] = array_values(array_unique(array_values(array_filter(array_map(fn($id) => $map[(int) $id] ?? '', $exclude_ids))))); $payload['required_prior_product_public_ids'] = array_values(array_unique(array_values(array_filter(array_map(fn($id) => $map[(int) $id] ?? '', $required_prior_ids))))); if ($type === 'bogo') { $payload['type'] = 'bogo'; $payload['bogo_buy_quantity'] = max(1, $bogo_buy_i); $payload['bogo_get_quantity'] = max(1, $bogo_get_i); $payload['bogo_get_percent_off_bps'] = (int) round(max(0.0, min(100.0, $bogo_pct_f > 0 ? $bogo_pct_f : 100.0)) * 100); } else if ($type === 'flat') { $payload['type'] = 'amount'; $payload['amount_off_minor'] = max(0, (int) round(max(0.0, $amount_f) * 100)); } else if ($type === 'fixed_price') { $payload['type'] = 'fixed_price'; $payload['fixed_price_minor'] = max(0, (int) round(max(0.0, $amount_f) * 100)); } else { $payload['type'] = 'percent'; $percent = max(0.0, min(100.0, $amount_f)); $payload['percent_off_bps'] = (int) round($percent * 100); } if ($min_subtotal !== '') { $payload['min_subtotal_minor'] = max(0, (int) round(max(0.0, $min_subtotal_f) * 100)); } if ($min_quantity_i > 0) { $payload['min_quantity'] = $min_quantity_i; } if ($max_discount !== '') { $payload['max_discount_minor'] = max(0, (int) round(max(0.0, $max_discount_f) * 100)); } if ($usage_limit > 0) { $payload['usage_limit_total'] = $usage_limit; } if ($usage_limit_per_customer > 0) { $payload['usage_limit_per_customer'] = $usage_limit_per_customer; } if ($customer_eligibility === 'returning') { $payload['returning_customers_only'] = true; $payload['first_time_customers_only'] = false; } else if ($customer_eligibility === 'first_time') { $payload['returning_customers_only'] = false; $payload['first_time_customers_only'] = true; } if ($expires !== '') { try { $dt = new \DateTimeImmutable($expires . ' 23:59:59', new \DateTimeZone('UTC')); $payload['ends_at'] = $dt->format('c'); } catch (\Exception $e) { // ignore invalid date } } // Cart rules (tiered / cheapest / bundle). These are optional and cannot be combined with BOGO. $cart_rule = trim((string) $cart_rule); if (!in_array($cart_rule, ['tiered', 'cheapest', 'bundle'], true)) { $cart_rule = ''; } $rule_quantity_basis = trim((string) $rule_quantity_basis); if (!in_array($rule_quantity_basis, ['eligible', 'total'], true)) { $rule_quantity_basis = 'eligible'; } if ($cart_rule !== '' && ($payload['type'] ?? '') !== 'bogo') { if ($cart_rule === 'tiered' && in_array((string) ($payload['type'] ?? ''), ['percent', 'amount'], true)) { $tiers = self::parse_tier_rules_text($tier_rules_text, (string) ($payload['type'] ?? '')); if (!empty($tiers)) { $payload['cart_rule'] = 'tiered'; $payload['rule_quantity_basis'] = $rule_quantity_basis; $payload['tier_rules'] = $tiers; } } else if ($cart_rule === 'cheapest' && (string) ($payload['type'] ?? '') === 'percent') { $qty = max(0, (int) $cheapest_item_quantity); $pct = (float) $cheapest_item_percent_off; $bps = (int) round(max(0.0, min(100.0, $pct)) * 100); if ($qty > 0 && $bps > 0) { $payload['cart_rule'] = 'cheapest'; $payload['rule_quantity_basis'] = $rule_quantity_basis; $payload['cheapest_item_quantity'] = $qty; $payload['cheapest_item_percent_off_bps'] = $bps; } } else if ($cart_rule === 'bundle' && (string) ($payload['type'] ?? '') === 'fixed_price') { $set_qty = max(0, (int) $bundle_set_quantity); if ($set_qty > 0) { $payload['cart_rule'] = 'bundle'; $payload['rule_quantity_basis'] = $rule_quantity_basis; $payload['bundle_set_quantity'] = $set_qty; } } } $emails_raw = (string) get_post_meta($post_id, '_ToDownload_discount_allowed_customer_emails', true); $emails = self::parse_emails_text($emails_raw); if (!empty($emails)) { $payload['allowed_customer_emails'] = $emails; } $res = HostedClient::post_json( $hosted_base, '/api/v1/stores/' . rawurlencode($store_public_id) . '/discounts', $payload, $api_key, $request_id ); if (!empty($res['ok']) && isset($res['data']) && is_array($res['data'])) { $d = (array) ($res['data']['discount'] ?? []); $public_id = isset($d['publicId']) ? sanitize_text_field((string) $d['publicId']) : ''; if ($public_id !== '') { update_post_meta($post_id, '_ToDownload_hosted_discount_public_id', $public_id); } delete_post_meta($post_id, '_ToDownload_hosted_discount_sync_error'); return; } $msg = isset($res['message']) ? (string) $res['message'] : ''; if ($msg === '' && isset($res['error']) && is_array($res['error'])) { $msg = isset($res['error']['message']) ? (string) $res['error']['message'] : ''; } if ($msg !== '') { update_post_meta($post_id, '_ToDownload_hosted_discount_sync_error', sanitize_text_field($msg)); } } /** * @return array */ private static function parse_tier_rules_text(string $raw, string $hosted_type): array { $raw = trim((string) $raw); if ($raw === '') { return []; } $hosted_type = trim((string) $hosted_type); if (!in_array($hosted_type, ['percent', 'amount'], true)) { return []; } $raw = str_replace("\r", "\n", $raw); $lines = explode("\n", $raw); $by_qty = []; foreach ($lines as $line) { $line = trim((string) $line); if ($line === '') { continue; } // Format: "min_qty: value" (e.g. "2: 10") if (!preg_match('/^\s*(\d+)\s*:\s*([0-9]+(?:\.[0-9]+)?)\s*$/', $line, $m)) { continue; } $min_qty = max(0, (int) $m[1]); $val = (float) $m[2]; if ($min_qty <= 0) { continue; } if ($hosted_type === 'percent') { $bps = (int) round(max(0.0, min(100.0, $val)) * 100); if ($bps <= 0) { continue; } $by_qty[$min_qty] = ['min_quantity' => $min_qty, 'percent_off_bps' => $bps]; } else { $minor = (int) round(max(0.0, $val) * 100); if ($minor <= 0) { continue; } $by_qty[$min_qty] = ['min_quantity' => $min_qty, 'amount_off_minor' => $minor]; } } if (empty($by_qty)) { return []; } ksort($by_qty); return array_values($by_qty); } public static function apply_discount_code(string $code, array $cart_items, float $subtotal, int $seller_user_id = 0, ?string $buyer_email = null): array { $code = strtoupper(trim($code)); if ($code === '') { return ['ok' => true, 'discount' => 0.0, 'code' => '']; } $seller_user_id = (int) $seller_user_id; if ($seller_user_id <= 0) { foreach ($cart_items as $it) { $download_id = (int) ($it['download_id'] ?? 0); if ($download_id > 0) { $seller_user_id = (int) get_post_field('post_author', $download_id); break; } } } $discount_post_id = self::find_discount_post_id($code, $seller_user_id); if (!$discount_post_id) { return ['ok' => false, 'message' => __('Invalid discount code.', '2download-connector')]; } $expires = (string) get_post_meta($discount_post_id, '_ToDownload_discount_expires', true); if ($expires !== '') { $ts = strtotime($expires . ' 23:59:59'); if ($ts && time() > $ts) { return ['ok' => false, 'message' => __('This discount code has expired.', '2download-connector')]; } } $usage_limit = (int) get_post_meta($discount_post_id, '_ToDownload_discount_usage_limit', true); $usage_count = (int) get_post_meta($discount_post_id, '_ToDownload_discount_usage_count', true); if ($usage_limit > 0 && $usage_count >= $usage_limit) { return ['ok' => false, 'message' => __('This discount code has reached its usage limit.', '2download-connector')]; } $buyer_email = sanitize_email((string) $buyer_email); $usage_limit_per_customer = (int) get_post_meta($discount_post_id, '_ToDownload_discount_usage_limit_per_customer', true); $returning_only = (string) get_post_meta($discount_post_id, '_ToDownload_discount_returning_customers_only', true) === '1'; $first_time_only = (string) get_post_meta($discount_post_id, '_ToDownload_discount_first_time_customers_only', true) === '1'; if (($usage_limit_per_customer > 0 || $returning_only || $first_time_only) && ($buyer_email === '' || !is_email($buyer_email))) { return ['ok' => false, 'message' => __('Please enter a valid email to use this discount code.', '2download-connector')]; } if ($buyer_email !== '' && is_email($buyer_email)) { $prior_complete_for_seller = 0; $prior_complete_with_code = 0; $orders = Orders::find_orders_by_email($buyer_email); foreach ($orders as $o) { if (!is_array($o)) { continue; } if ((string) ($o['status'] ?? '') !== 'complete') { continue; } if ((int) ($o['seller_user_id'] ?? 0) !== (int) $seller_user_id) { continue; } $prior_complete_for_seller++; $notes = (string) ($o['notes'] ?? ''); if ($notes !== '' && strpos($notes, 'ToDownload_discount_code: ' . $code) !== false) { $prior_complete_with_code++; } } if ($returning_only && $prior_complete_for_seller <= 0) { return ['ok' => false, 'message' => __('This discount code is only for returning customers.', '2download-connector')]; } if ($first_time_only && $prior_complete_for_seller > 0) { return ['ok' => false, 'message' => __('This discount code is only for first-time customers.', '2download-connector')]; } if ($usage_limit_per_customer > 0 && $prior_complete_with_code >= $usage_limit_per_customer) { return ['ok' => false, 'message' => __('This discount code has reached its per-customer limit.', '2download-connector')]; } } $product_ids_raw = (string) get_post_meta($discount_post_id, '_ToDownload_discount_product_ids', true); $allowed_ids = self::parse_ids_csv($product_ids_raw); if (!empty($allowed_ids)) { $cart_ids = array_map(fn($it) => (int) ($it['download_id'] ?? 0), $cart_items); $matches = array_intersect($allowed_ids, $cart_ids); if (empty($matches)) { return ['ok' => false, 'message' => __('This discount code does not apply to items in your cart.', '2download-connector')]; } } $type = (string) get_post_meta($discount_post_id, '_ToDownload_discount_type', true); $amount = (float) get_post_meta($discount_post_id, '_ToDownload_discount_amount', true); $min_quantity = (int) get_post_meta($discount_post_id, '_ToDownload_discount_min_quantity', true); $max_discount = (string) get_post_meta($discount_post_id, '_ToDownload_discount_max_discount', true); $cart_rule = (string) get_post_meta($discount_post_id, '_ToDownload_discount_cart_rule', true); $rule_quantity_basis = (string) get_post_meta($discount_post_id, '_ToDownload_discount_rule_quantity_basis', true); $tier_rules_text = (string) get_post_meta($discount_post_id, '_ToDownload_discount_tier_rules_text', true); $cheapest_item_quantity = (int) get_post_meta($discount_post_id, '_ToDownload_discount_cheapest_item_quantity', true); $cheapest_item_percent_off = (float) get_post_meta($discount_post_id, '_ToDownload_discount_cheapest_item_percent_off', true); $bundle_set_quantity = (int) get_post_meta($discount_post_id, '_ToDownload_discount_bundle_set_quantity', true); $excluded_ids_raw = (string) get_post_meta($discount_post_id, '_ToDownload_discount_excluded_product_ids', true); $excluded_ids = self::parse_ids_csv($excluded_ids_raw); $total_unit_amounts_minor = []; $eligible_unit_amounts_minor = []; foreach ($cart_items as $it) { $download_id = (int) ($it['download_id'] ?? 0); if ($download_id <= 0) { continue; } $price = isset($it['price']) ? (float) $it['price'] : 0.0; $minor = (int) round(max(0.0, $price) * 100); if ($minor <= 0) { continue; } $total_unit_amounts_minor[] = $minor; if (!empty($excluded_ids) && in_array($download_id, $excluded_ids, true)) { continue; } if (!empty($allowed_ids) && !in_array($download_id, $allowed_ids, true)) { continue; } $eligible_unit_amounts_minor[] = $minor; } $eligible_subtotal_minor = array_sum($eligible_unit_amounts_minor); $total_count = count($total_unit_amounts_minor); $eligible_count = count($eligible_unit_amounts_minor); $basis = in_array($rule_quantity_basis, ['eligible', 'total'], true) ? $rule_quantity_basis : 'eligible'; $basis_count = $basis === 'total' ? $total_count : $eligible_count; if ($min_quantity > 0 && $eligible_count < $min_quantity) { return ['ok' => false, 'message' => __('Your cart does not meet the minimum quantity for this discount code.', '2download-connector')]; } $discount = 0.0; $cart_rule = in_array($cart_rule, ['tiered', 'cheapest', 'bundle'], true) ? $cart_rule : ''; if ($type === 'bogo') { $buy_qty = (int) get_post_meta($discount_post_id, '_ToDownload_discount_bogo_buy_quantity', true); $get_qty = (int) get_post_meta($discount_post_id, '_ToDownload_discount_bogo_get_quantity', true); $pct = (float) get_post_meta($discount_post_id, '_ToDownload_discount_bogo_get_percent_off', true); $buy_qty = max(1, $buy_qty); $get_qty = max(1, $get_qty); $set_size = $buy_qty + $get_qty; $sets = (int) floor(count($eligible_unit_amounts_minor) / $set_size); $discount_units = $sets * $get_qty; if ($discount_units <= 0) { return ['ok' => false, 'message' => __('This discount code does not apply to items in your cart.', '2download-connector')]; } $pct = max(0.0, min(100.0, $pct > 0 ? $pct : 100.0)); $bps = (int) round($pct * 100); sort($eligible_unit_amounts_minor); $chosen = array_slice($eligible_unit_amounts_minor, 0, $discount_units); $discount_minor = 0; foreach ($chosen as $unit_minor) { $discount_minor += (int) floor(((int) $unit_minor * $bps) / 10000); } $discount = min($subtotal, max(0.0, ((float) $discount_minor) / 100.0)); } else if ($cart_rule === 'tiered' && ($type === 'percent' || $type === 'flat')) { $hosted_type = $type === 'flat' ? 'amount' : 'percent'; $tiers = self::parse_tier_rules_text($tier_rules_text, $hosted_type); if (empty($tiers)) { return ['ok' => false, 'message' => __('This discount code does not apply to items in your cart.', '2download-connector')]; } $chosen = null; foreach ($tiers as $tr) { $minq = (int) ($tr['min_quantity'] ?? 0); if ($minq > 0 && $minq <= $basis_count) { $chosen = $tr; } } if (!$chosen) { return ['ok' => false, 'message' => __('This discount code does not apply to items in your cart.', '2download-connector')]; } if ($hosted_type === 'percent') { $bps = (int) ($chosen['percent_off_bps'] ?? 0); $discount_minor = (int) floor(((int) $eligible_subtotal_minor * $bps) / 10000); $discount = min($subtotal, max(0.0, ((float) $discount_minor) / 100.0)); } else { $minor = (int) ($chosen['amount_off_minor'] ?? 0); $discount = min($subtotal, max(0.0, min(((float) $eligible_subtotal_minor) / 100.0, ((float) $minor) / 100.0))); } } else if ($cart_rule === 'cheapest' && $type === 'percent') { $qty = max(0, (int) $cheapest_item_quantity); $bps = (int) round(max(0.0, min(100.0, $cheapest_item_percent_off)) * 100); if ($qty <= 0 || $bps <= 0 || $eligible_count < $qty) { return ['ok' => false, 'message' => __('This discount code does not apply to items in your cart.', '2download-connector')]; } sort($eligible_unit_amounts_minor); $chosen_units = array_slice($eligible_unit_amounts_minor, 0, $qty); $discount_minor = 0; foreach ($chosen_units as $unit_minor) { $discount_minor += (int) floor(((int) $unit_minor * $bps) / 10000); } $discount = min($subtotal, max(0.0, ((float) $discount_minor) / 100.0)); } else if ($cart_rule === 'bundle' && $type === 'fixed_price') { $set_qty = max(0, (int) $bundle_set_quantity); $sets = $set_qty > 0 ? (int) floor($basis_count / $set_qty) : 0; if ($set_qty <= 0 || $sets <= 0 || $eligible_count < ($sets * $set_qty)) { return ['ok' => false, 'message' => __('This discount code does not apply to items in your cart.', '2download-connector')]; } $fixed_price_minor = (int) round(max(0.0, $amount) * 100); rsort($eligible_unit_amounts_minor); $take = $sets * $set_qty; $bundle_units = array_slice($eligible_unit_amounts_minor, 0, $take); $bundle_subtotal_minor = array_sum($bundle_units); $discount_minor = max(0, $bundle_subtotal_minor - ($sets * $fixed_price_minor)); $discount = min($subtotal, max(0.0, ((float) $discount_minor) / 100.0)); } else if ($type === 'flat') { $discount = min($subtotal, max(0.0, min(((float) $eligible_subtotal_minor) / 100.0, $amount))); } else if ($type === 'fixed_price') { $fixed_price_minor = (int) round(max(0.0, $amount) * 100); $discount_minor = max(0, $eligible_subtotal_minor - $fixed_price_minor); $discount = min($subtotal, max(0.0, ((float) $discount_minor) / 100.0)); } else { $percent = max(0.0, min(100.0, $amount)); $discount = (((float) $eligible_subtotal_minor) / 100.0) * ($percent / 100.0); $discount = min($subtotal, max(0.0, $discount)); } if ($max_discount !== '') { $cap = (float) $max_discount; if ($cap > 0) { $discount = min($discount, $cap); } } return [ 'ok' => true, 'code' => $code, 'discount' => (float) round($discount, 2), 'discount_post_id' => $discount_post_id, ]; } private static function parse_ids_csv(string $csv): array { $csv = trim($csv); if ($csv === '') { return []; } $parts = array_filter(array_map('trim', explode(',', $csv))); $out = []; foreach ($parts as $p) { $id = (int) $p; if ($id > 0) { $out[] = $id; } } return array_values(array_unique($out)); } private static function find_discount_post_id(string $code, int $seller_user_id = 0): int { $args = [ 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'numberposts' => 1, 'fields' => 'ids', 'no_found_rows' => true, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required lookup by discount code. 'meta_query' => [ [ 'key' => '_ToDownload_discount_code', 'value' => $code, 'compare' => '=', ], ], ]; $seller_user_id = (int) $seller_user_id; if ($seller_user_id > 0) { $args['author'] = $seller_user_id; } $posts = get_posts($args); return !empty($posts) ? (int) $posts[0] : 0; } public static function on_order_complete(int $order_id): void { $order = Orders::get_order($order_id); if (!$order) { return; } $notes = (string) ($order['notes'] ?? ''); if ($notes === '') { return; } // Look for marker like: "ToDownload_discount_code: SAVE10" if (!preg_match('/ToDownload_discount_code:\s*([A-Z0-9_-]+)/', $notes, $m)) { return; } $code = strtoupper(trim((string) $m[1])); if ($code === '') { return; } $seller_user_id = (int) ($order['seller_user_id'] ?? 0); $discount_post_id = self::find_discount_post_id($code, $seller_user_id); if (!$discount_post_id) { return; } $usage_count = (int) get_post_meta($discount_post_id, '_ToDownload_discount_usage_count', true); update_post_meta($discount_post_id, '_ToDownload_discount_usage_count', $usage_count + 1); } }