false, 'show_ui' => false, 'supports' => array( 'title' ), 'map_meta_cap' => true ) ); } ); add_action( 'admin_menu', array( $this, 'menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'assets' ) ); add_action( 'rest_api_init', array( $this, 'routes' ) ); } public function menu(): void { add_submenu_page( 'woocommerce', 'DigiDiscount', 'DigiDiscount', 'manage_woocommerce', 'digidiscount', array( $this, 'page' ) ); } public function page(): void { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } echo '
' . esc_html__( 'Loading your campaigns...', 'digidiscount' ) . '
'; } public function assets( $hook ): void { if ( 'woocommerce_page_digidiscount' !== $hook ) { return; } wp_enqueue_style( 'digidiscount-admin', plugins_url( 'assets/admin.css', DIGIDISCOUNT_FILE ), array(), DIGIDISCOUNT_VERSION ); wp_enqueue_script( 'digidiscount-admin', plugins_url( 'assets/admin.js', DIGIDISCOUNT_FILE ), array(), DIGIDISCOUNT_VERSION, true ); wp_localize_script( 'digidiscount-admin', 'DigiDiscountAdmin', apply_filters( 'digidiscount_admin_config', array( 'api' => rest_url( 'digidiscount/v1/' ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'schema' => Rules::schema(), 'types' => Rules::types(), 'defaults' => Rules::defaults(), 'edition' => '', 'previewFields' => array(), 'currency' => get_woocommerce_currency(), 'mode' => get_option( 'digidiscount_mode', 'best' ), 'timezone' => wp_timezone_string(), 'links' => apply_filters( 'digidiscount_admin_links', array() ), 'strings' => array( 'eyebrow' => __( 'PRICING, WITH PURPOSE', 'digidiscount' ), 'title' => __( 'Your next great offer.', 'digidiscount' ), 'intro' => __( 'Create thoughtful discounts. Keep every campaign in control.', 'digidiscount' ), 'new' => __( 'Create campaign', 'digidiscount' ), 'campaigns' => __( 'Campaigns', 'digidiscount' ), 'active' => __( 'Enabled', 'digidiscount' ), 'scheduled' => __( 'Scheduled', 'digidiscount' ), 'paused' => __( 'Paused', 'digidiscount' ), 'expired' => __( 'Expired', 'digidiscount' ), 'search' => __( 'Search campaigns...', 'digidiscount' ), 'edit' => __( 'Edit', 'digidiscount' ), 'duplicate' => __( 'Duplicate', 'digidiscount' ), 'delete' => __( 'Delete', 'digidiscount' ), 'confirm' => __( 'Delete this campaign permanently?', 'digidiscount' ), 'save' => __( 'Save campaign', 'digidiscount' ), 'cancel' => __( 'Back to campaigns', 'digidiscount' ), 'saved' => __( 'Campaign saved.', 'digidiscount' ), 'empty' => __( 'Make your first offer.', 'digidiscount' ), 'emptyText' => __( 'Start with a product, category or cart discount. Preview it before enabling.', 'digidiscount' ), 'type' => __( 'Campaign type', 'digidiscount' ), 'typeHelp' => __( 'Choose the offer you want to create.', 'digidiscount' ), 'basics' => __( 'The essentials', 'digidiscount' ), 'audience' => __( 'Products and audience', 'digidiscount' ), 'discount' => __( 'The offer', 'digidiscount' ), 'conditions' => __( 'When it applies', 'digidiscount' ), 'schedule' => __( 'Timing', 'digidiscount' ), 'display' => __( 'Customer experience', 'digidiscount' ), 'advanced' => __( 'Advanced controls', 'digidiscount' ), 'summary' => __( 'Campaign summary', 'digidiscount' ), 'preview' => __( 'Preview calculation', 'digidiscount' ), 'previewHelp' => __( 'Enter one product ID and quantity per line, separated by a comma. Real product prices are used. Taxes follow your entered-price basis; shipping and other coupons are excluded.', 'digidiscount' ), 'previewItems' => __( 'Test basket', 'digidiscount' ), 'previewResult' => __( 'Estimated campaign discount', 'digidiscount' ), 'previewHint' => __( 'Preview ignores the enabled switch and date schedule. Other conditions still apply.', 'digidiscount' ), 'working' => __( 'Working...', 'digidiscount' ), 'error' => __( 'Something went wrong. Please try again.', 'digidiscount' ), 'choose' => __( 'Search and select...', 'digidiscount' ), 'remove' => __( 'Remove', 'digidiscount' ), 'noResults' => __( 'No matches found.', 'digidiscount' ), 'mode' => __( 'When campaigns overlap', 'digidiscount' ), 'modeHelp' => __( 'Choose the best saving, the first matching campaign by priority, or combine campaigns that allow stacking.', 'digidiscount' ), 'best' => __( 'Best monetary saving', 'digidiscount' ), 'priority' => __( 'First by priority', 'digidiscount' ), 'stack' => __( 'Combine permitted campaigns', 'digidiscount' ), 'settingsSaved' => __( 'Processing preference saved.', 'digidiscount' ), 'unavailable' => __( 'Extension unavailable', 'digidiscount' ), 'extensionRequired' => __( 'The extension that defines these campaign conditions is unavailable. The saved campaign has been preserved.', 'digidiscount' ), 'currency' => __( 'Currency', 'digidiscount' ), 'timezone' => __( 'Store timezone', 'digidiscount' ), 'unit' => __( 'per item', 'digidiscount' ), 'unsaved' => __( 'Leave without saving these changes?', 'digidiscount' ), 'couponWarning' => wc_coupons_enabled() ? '' : __( 'Enable coupons in WooCommerce settings to use automatic discounts.', 'digidiscount' ), ), ) ) ); } public function permissions_check() { if ( current_user_can( 'manage_woocommerce' ) ) { return true; } return new \WP_Error( 'digidiscount_forbidden', __( 'You do not have permission to manage campaigns.', 'digidiscount' ), array( 'status' => rest_authorization_required_code() ) ); } public function routes(): void { register_rest_route( 'digidiscount/v1', '/rules', array( array( 'methods' => 'GET', 'callback' => static fn() => array_map( static fn( $rule ) => array_merge( $rule, array( 'available' => Rules::supported( $rule ) ) ), Rules::all() ), 'permission_callback' => array( $this, 'permissions_check' ) ), array( 'methods' => 'POST', 'callback' => array( $this, 'save' ), 'permission_callback' => array( $this, 'permissions_check' ) ), ) ); register_rest_route( 'digidiscount/v1', '/rules/(?P\d+)', array( array( 'methods' => 'PUT', 'callback' => array( $this, 'save' ), 'permission_callback' => array( $this, 'permissions_check' ) ), array( 'methods' => 'DELETE', 'callback' => array( $this, 'delete' ), 'permission_callback' => array( $this, 'permissions_check' ) ), ) ); register_rest_route( 'digidiscount/v1', '/lookup', array( 'methods' => 'GET', 'callback' => array( $this, 'lookup' ), 'permission_callback' => array( $this, 'permissions_check' ) ) ); register_rest_route( 'digidiscount/v1', '/settings', array( 'methods' => 'POST', 'callback' => static function ( $request ) { $mode = sanitize_key( $request->get_param( 'mode' ) ?? 'best' ); $allowed = array( 'best', 'priority', 'stack' ); if ( ! in_array( $mode, $allowed, true ) ) { return new \WP_Error( 'digidiscount_mode', __( 'Unsupported processing mode.', 'digidiscount' ), array( 'status' => 400 ) ); } update_option( 'digidiscount_mode', $mode, false ); return array( 'mode' => $mode ); }, 'permission_callback' => array( $this, 'permissions_check' ) ) ); register_rest_route( 'digidiscount/v1', '/preview', array( 'methods' => 'POST', 'callback' => array( $this, 'preview' ), 'permission_callback' => array( $this, 'permissions_check' ) ) ); } public function save( $request ) { $id = absint( $request->get_param( 'id' ) ); if ( $id && ! Storage::is_campaign( $id ) ) { return new \WP_Error( 'digidiscount_missing', __( 'Campaign not found.', 'digidiscount' ), array( 'status' => 404 ) ); } $old = $id ? Storage::rule( $id ) : array(); if ( $old && ! Rules::supported( $old ) ) { return new \WP_Error( 'digidiscount_conditions', __( 'The extension that defines these campaign conditions is unavailable. The saved campaign has been preserved.', 'digidiscount' ), array( 'status' => 409 ) ); } $rule = Rules::sanitize( (array) $request->get_json_params() ); if ( is_wp_error( $rule ) ) { return $rule; } $id = wp_insert_post( array( 'ID' => $id, 'post_type' => Storage::POST_TYPE, 'post_status' => 'publish', 'post_title' => wp_slash( $rule['name'] ) ), true ); if ( is_wp_error( $id ) ) { return $id; } update_post_meta( $id, Storage::META_KEY, wp_slash( $rule ) ); $rule['id'] = (string) $id; return $rule; } public function delete( $request ) { $id = absint( $request['id'] ); if ( ! Storage::is_campaign( $id ) ) { return new \WP_Error( 'digidiscount_missing', __( 'Campaign not found.', 'digidiscount' ), array( 'status' => 404 ) ); } wp_delete_post( $id, true ); return array( 'deleted' => true ); } public function lookup( $request ): array { $source = sanitize_key( $request->get_param( 'source' ) ?? 'products' ); $search = sanitize_text_field( $request->get_param( 'search' ) ?? '' ); $ids = array_slice( array_filter( array_map( 'absint', explode( ',', (string) $request->get_param( 'ids' ) ) ) ), 0, 500 ); if ( 'products' === $source ) { if ( ! $ids ) { $store = \WC_Data_Store::load( 'product' ); $ids = $search ? array_slice( $store->search_products( $search, '', true ), 0, 20 ) : wc_get_products( array( 'limit' => 20, 'return' => 'ids', 'status' => 'publish' ) ); } $out = array(); foreach ( $ids as $id ) { $product = wc_get_product( $id ); if ( $product ) { $out[] = array( 'id' => $id, 'label' => '#' . $id . ' ' . wp_strip_all_tags( $product->get_formatted_name() ) ); } } return $out; } $taxonomies = array( 'categories' => 'product_cat' ); if ( ! isset( $taxonomies[ $source ] ) || ! taxonomy_exists( $taxonomies[ $source ] ) ) { return (array) apply_filters( 'digidiscount_lookup_results', array(), $source, $search, $ids ); } $terms = get_terms( array_merge( array( 'taxonomy' => $taxonomies[ $source ], 'hide_empty' => false ), $ids ? array( 'include' => $ids ) : array( 'search' => $search, 'number' => 20 ) ) ); return is_wp_error( $terms ) ? array() : array_map( static fn( $term ) => array( 'id' => $term->term_id, 'label' => $term->name ), $terms ); } public function preview( $request ) { $data = (array) $request->get_json_params(); $rule = Rules::sanitize( (array) ( $data['rule'] ?? array() ) ); if ( is_wp_error( $rule ) ) { return $rule; } $factor = 10 ** wc_get_price_decimals(); $lines = array(); foreach ( array_slice( (array) ( $data['items'] ?? array() ), 0, 30 ) as $item ) { $product = wc_get_product( absint( $item['id'] ?? 0 ) ); if ( ! $product || ! $product->is_type( array( 'simple', 'variation' ) ) ) { continue; } $id = $product->get_parent_id() ?: $product->get_id(); $cats = wc_get_product_term_ids( $id, 'product_cat' ); foreach ( $cats as $cat ) { $cats = array_merge( $cats, get_ancestors( $cat, 'product_cat', 'taxonomy' ) ); } $qty = min( 10000, max( 1, absint( $item['qty'] ?? 1 ) ) ); $key = (string) $product->get_id(); $lines[ $key ] = array( 'key' => $key, 'product_id' => $id, 'variation_id' => $product->is_type( 'variation' ) ? $product->get_id() : 0, 'qty' => $qty, 'total' => (int) round( (float) $product->get_price() * $qty * $factor ), 'categories' => $cats, 'sale' => $product->is_on_sale(), 'tags' => wc_get_product_term_ids( $id, 'product_tag' ), 'brands' => taxonomy_exists( 'product_brand' ) ? wc_get_product_term_ids( $id, 'product_brand' ) : array() ); } $rule['id'] = 'preview'; $rule['enabled'] = true; $rule['start'] = $rule['end'] = 0; foreach ( array( 'min_total', 'max_discount' ) as $key ) { $rule[ $key ] = (int) round( $rule[ $key ] * $factor ); } if ( 'percent' !== $rule['kind'] ) { $rule['amount'] = (int) round( $rule['amount'] * $factor ); } $rule = apply_filters( 'digidiscount_runtime_rule', $rule, $factor ); $uid = get_current_user_id(); $user = get_user_by( 'id', $uid ); $context = array( 'currency' => get_woocommerce_currency(), 'now' => time(), 'coupons' => array(), 'user_id' => $uid, 'roles' => $user ? $user->roles : array(), 'gateway' => '' ); $context = apply_filters( 'digidiscount_preview_context', $context, $data ); $result = Engine::calculate( $lines, array( $rule ), $context ); return array( 'discount' => array_sum( $result['discounts'] ) / $factor, 'currency' => get_woocommerce_currency(), 'result' => $result ); } }