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 );
}
}