$cap, 'public' => 'register' === $a ) ); } $this->commissions = new Commissions( $this ); $this->commissions->boot(); ( new Storefront( $this ) )->boot(); ( new Shipping( $this ) )->boot(); add_filter( 'woocommerce_is_purchasable', array( $this, 'purchasable' ), 20, 2 ); add_filter( 'woocommerce_get_price_html', array( $this, 'price_html' ), 20, 2 ); add_action( 'woocommerce_single_product_summary', array( $this, 'vacation_notice' ), 7 ); add_action( 'delete_user', function ( $uid ) { global $wpdb; if ( self::is_vendor( $uid ) ) { $wpdb->delete( $wpdb->prefix . 'frontglut_withdrawals', array( 'vendor_id' => $uid ) ); } } ); } /* ---------------- settings ---------------- */ public function defaults( $d ) { $d['vendors'] = array( 'approval' => 1, 'registration_open' => 0, // Off until the administrator opens vendor applications. 'slug_base' => 'store', 'commission_type' => 'percent', 'commission_percent' => 10, 'commission_flat' => 0, 'cat_overrides' => '', 'commission_on' => 'processing', 'withdraw_min' => 50, 'withdraw_days' => 0, 'withdraw_methods' => array( 'paypal', 'bank' ), 'auto_request' => 0, 'split_orders' => 1, 'vendor_shipping' => 1, ); return $d; } public function schema( $s ) { $s['vendors'] = array( 'label' => __( 'Marketplace', 'frontglut' ), 'order' => 29, 'fields' => array( 'vendors.provider' => array( 'type' => 'select', 'label' => __( 'Vendor engine', 'frontglut' ), 'options' => array( 'native' => 'FrontGlut (built-in)', 'dokan' => 'Dokan (read-only adapter: labels & store links)' ), 'help' => __( 'With Dokan selected, order splitting and commissions are handled by Dokan.', 'frontglut' ) ), 'vendors.registration_open' => array( 'type' => 'checkbox', 'label' => __( 'Allow people to apply as vendors', 'frontglut' ), 'help' => __( 'Use the [frontglut_vendor_register] shortcode on a page.', 'frontglut' ) ), 'vendors.approval' => array( 'type' => 'checkbox', 'label' => __( 'New vendors require admin approval', 'frontglut' ) ), 'vendors.slug_base' => array( 'type' => 'text', 'label' => __( 'Store URL base', 'frontglut' ), 'help' => __( 'e.g. "store" gives /store/my-shop/ (re-save permalinks after changing).', 'frontglut' ) ), 'vendors.split_orders' => array( 'type' => 'checkbox', 'label' => __( 'Split multi-vendor carts into one sub-order per vendor', 'frontglut' ) ), 'vendors.vendor_shipping' => array( 'type' => 'checkbox', 'label' => __( 'Calculate shipping per vendor (add the "Vendor shipping" method to a shipping zone)', 'frontglut' ) ), 'vendors.commission_type' => array( 'type' => 'select', 'label' => __( 'Marketplace commission type', 'frontglut' ), 'options' => array( 'percent' => 'Percentage of sale', 'flat' => 'Flat amount per item', 'combined' => 'Percentage + flat per item' ) ), 'vendors.commission_percent' => array( 'type' => 'number', 'label' => __( 'Commission %', 'frontglut' ), 'min' => 0, 'max' => 100 ), 'vendors.commission_flat' => array( 'type' => 'number', 'label' => __( 'Flat commission per item', 'frontglut' ), 'min' => 0 ), 'vendors.cat_overrides' => array( 'type' => 'textarea', 'label' => __( 'Category commission overrides', 'frontglut' ), 'help' => __( 'One per line: category-slug|percent. Product and vendor overrides win over category, category wins over global.', 'frontglut' ) ), 'vendors.commission_on' => array( 'type' => 'select', 'label' => __( 'Record earnings when order is', 'frontglut' ), 'options' => array( 'processing' => 'Processing (pending until completed)', 'completed' => 'Completed only' ) ), 'vendors.withdraw_min' => array( 'type' => 'number', 'label' => __( 'Minimum withdrawal', 'frontglut' ), 'min' => 0 ), 'vendors.withdraw_days' => array( 'type' => 'number', 'label' => __( 'Hold earnings for N days after completion', 'frontglut' ), 'min' => 0 ), 'vendors.withdraw_methods' => array( 'type' => 'checkboxes', 'label' => __( 'Payout methods', 'frontglut' ), 'options' => array( 'paypal' => 'PayPal', 'bank' => 'Bank transfer', 'manual' => 'Manual / other' ) ), 'vendors.auto_request' => array( 'type' => 'checkbox', 'label' => __( 'Create withdrawal requests automatically each week for balances above the minimum', 'frontglut' ) ), ) ); return $s; } public function emails( $t ) { $t['vendor_applied'] = array( 'label' => __( 'Admin: vendor application', 'frontglut' ), 'subject' => 'New vendor application: {store}', 'body' => "{vendor} applied to sell on {site_name}.\nStore: {store}\nReview: {review_url}", 'vars' => 'vendor, store, review_url' ); $t['vendor_approved'] = array( 'label' => __( 'Vendor: approved', 'frontglut' ), 'subject' => 'Your store was approved', 'body' => "Hi {name},\n\nYour store \"{store}\" is approved. Open your dashboard: {dashboard_url}", 'vars' => 'name, store, dashboard_url' ); $t['vendor_rejected'] = array( 'label' => __( 'Vendor: rejected / disabled', 'frontglut' ), 'subject' => 'Update on your store application', 'body' => "Hi {name},\n\nYour store \"{store}\" is not active. {reason}", 'vars' => 'name, store, reason' ); $t['withdraw_paid'] = array( 'label' => __( 'Vendor: payout sent', 'frontglut' ), 'subject' => 'Your payout of {amount} was sent', 'body' => "Hi {name},\n\nWe sent {amount} via {method}.\n{note}", 'vars' => 'name, amount, method, note' ); $t['withdraw_rejected'] = array( 'label' => __( 'Vendor: payout rejected', 'frontglut' ), 'subject' => 'Your withdrawal request was declined', 'body' => "Hi {name},\n\nYour request for {amount} was declined.\n{note}", 'vars' => 'name, amount, note' ); return $t; } /* ---------------- helpers ---------------- */ public function ensure_role() { $caps = array( 'read' => true, 'upload_files' => true, 'frontglut_access_dashboard' => true, 'frontglut_manage_posts' => true, 'frontglut_upload_files' => true, 'frontglut_manage_products' => true, 'frontglut_manage_orders' => true, 'frontglut_manage_coupons' => true, 'frontglut_view_reports' => true, 'frontglut_manage_customers' => true, 'frontglut_manage_reviews' => true, 'frontglut_is_vendor' => true, 'frontglut_manage_tickets' => true ); $r = get_role( self::ROLE ); if ( ! $r ) { add_role( self::ROLE, __( 'Vendor', 'frontglut' ), $caps ); } $admin = get_role( 'administrator' ); if ( $admin ) { $admin->add_cap( 'frontglut_manage_vendors' ); } $sm = get_role( 'shop_manager' ); if ( $sm ) { $sm->add_cap( 'frontglut_manage_vendors' ); } } /** Provider adapter: "native" (this plugin) or "dokan" (read-only: labels, store links). */ public static function provider() { return 'dokan' === Settings::get( 'vendors.provider', 'native' ) && function_exists( 'dokan_is_user_seller' ) ? 'dokan' : 'native'; } public static function is_vendor( $uid ) { $uid = (int) $uid; if ( 'dokan' === self::provider() ) { return $uid && dokan_is_user_seller( $uid, true ); } return $uid && 'approved' === get_user_meta( $uid, 'frontglut_vendor_status', true ); } public static function store( $uid ) { $s = get_user_meta( (int) $uid, 'frontglut_store', true ); $s = is_array( $s ) ? $s : array(); if ( 'dokan' === self::provider() && function_exists( 'dokan_get_store_info' ) ) { $d = dokan_get_store_info( (int) $uid ); $s = array( 'name' => $d['store_name'] ?? '', 'slug' => get_userdata( (int) $uid ) ? get_userdata( (int) $uid )->user_nicename : '', 'phone' => $d['phone'] ?? '' ); } return array_merge( array( 'name' => '', 'slug' => '', 'logo' => 0, 'banner' => 0, 'description' => '', 'phone' => '', 'email' => '', 'street' => '', 'city' => '', 'state' => '', 'zip' => '', 'country' => '', 'map' => '', 'hours' => array(), 'facebook' => '', 'twitter' => '', 'instagram' => '', 'youtube' => '', 'website' => '', 'policy_shipping' => '', 'policy_refund' => '', 'vacation' => 0, 'vacation_msg' => '', 'vacation_until' => '', 'catalog_mode' => 0, 'hide_price' => 0, 'ship_rates' => '', 'ship_default' => '', 'ship_time' => '', 'payout_method' => 'paypal', 'payout_paypal' => '', 'payout_bank' => '', 'commission' => array() ), $s ); } public static function by_slug( $slug ) { if ( 'dokan' === self::provider() ) { $u = get_user_by( 'slug', sanitize_title( $slug ) ); return $u ? (int) $u->ID : 0; } $q = new \WP_User_Query( array( 'meta_query' => array( array( 'key' => 'frontglut_store_slug', 'value' => sanitize_title( $slug ) ) ), 'number' => 1, 'fields' => 'ID' ) ); $r = $q->get_results(); return $r ? (int) $r[0] : 0; } public static function store_url( $uid ) { if ( 'dokan' === self::provider() && function_exists( 'dokan_get_store_url' ) ) { return dokan_get_store_url( (int) $uid ); } $st = self::store( $uid ); $base = Settings::get( 'vendors.slug_base', 'store' ); return $st['slug'] ? home_url( '/' . $base . '/' . $st['slug'] . '/' ) : ''; } public static function product_vendor( $product_id ) { $a = (int) get_post_field( 'post_author', $product_id ); return self::is_vendor( $a ) ? $a : 0; } public function unique_slug( $name, $uid ) { $base = sanitize_title( $name ) ?: 'store-' . $uid; $slug = $base; $i = 2; while ( ( $o = self::by_slug( $slug ) ) && (int) $o !== (int) $uid || in_array( $slug, array( 'page', 'feed', 'embed' ), true ) ) { $slug = $base . '-' . $i++; } return $slug; } /* ---------------- application ---------------- */ private function apply_fields() { $f = array( '\FrontGlut\Modules\Products\Products', 'fd' ); $d = array( '\FrontGlut\Modules\Products\Products', 'fd' ); return apply_filters( 'frontglut_vendor_apply_fields', array( \FrontGlut\Modules\Products\Products::fd( 'store_name', 'text', __( 'Store name', 'frontglut' ), array( 'required' => 1 ) ), \FrontGlut\Modules\Products\Products::fd( 'phone', 'tel', __( 'Phone', 'frontglut' ), array( 'width' => '50' ) ), \FrontGlut\Modules\Products\Products::fd( 'country', 'text', __( 'Country', 'frontglut' ), array( 'width' => '50' ) ), \FrontGlut\Modules\Products\Products::fd( 'description', 'textarea', __( 'About your store', 'frontglut' ) ), ) ); } public function sc_register() { Assets::need(); if ( ! Settings::get( 'vendors.registration_open' ) ) { return '
' . Ui::notice( esc_html__( 'Vendor registration is closed.', 'frontglut' ), 'warn' ) . '
'; } if ( is_user_logged_in() ) { $st = get_user_meta( get_current_user_id(), 'frontglut_vendor_status', true ); if ( 'approved' === $st ) { return '
' . Ui::notice( esc_html__( 'You are already a vendor.', 'frontglut' ), 'ok' ) . Ui::btn( __( 'Open dashboard', 'frontglut' ), Dashboard::url( 'store' ), 'frontglut-primary' ) . '
'; } if ( 'pending' === $st ) { return '
' . Ui::notice( esc_html__( 'Your application is awaiting approval.', 'frontglut' ), 'info' ) . '
'; } $h = '

' . esc_html__( 'Become a vendor', 'frontglut' ) . '

' . Ui::form_open( 'vendor_apply', array( 'after' => 'reload' ) ) . \FrontGlut\Modules\Forms\Forms::render_fields( $this->apply_fields(), array() ); return $h . Ui::form_close( __( 'Apply', 'frontglut' ) ) . '
'; } $reg = \FrontGlut\Modules\Forms\Forms::default_form( 'register' ); $h = '

' . esc_html__( 'Open your store', 'frontglut' ) . '

' . Ui::form_open( 'vendor_register', array( 'after' => 'reload' ) ) . \FrontGlut\Modules\Forms\Forms::honeypot(); $h .= '

' . esc_html__( 'Account', 'frontglut' ) . '

' . \FrontGlut\Modules\Forms\Forms::render_fields( $reg['fields'], array(), 'frontglut_f' ) . '

' . esc_html__( 'Store', 'frontglut' ) . '

' . \FrontGlut\Modules\Forms\Forms::render_fields( $this->apply_fields(), array(), 'frontglut_s' ); if ( \FrontGlut\Core\Captcha::active_for( 'register' ) ) { $h .= \FrontGlut\Core\Captcha::render(); } return $h . Ui::form_close( __( 'Create account & apply', 'frontglut' ) ) . '
'; } private function make_application( $uid, array $v ) { $st = self::store( $uid ); $st['name'] = $v['store_name']; $st['phone'] = $v['phone'] ?? ''; $st['country'] = $v['country'] ?? ''; $st['description'] = $v['description'] ?? ''; $st['slug'] = $this->unique_slug( $v['store_name'], $uid ); update_user_meta( $uid, 'frontglut_store', $st ); update_user_meta( $uid, 'frontglut_store_slug', $st['slug'] ); $approval = Settings::get( 'vendors.approval' ); update_user_meta( $uid, 'frontglut_vendor_status', $approval ? 'pending' : 'approved' ); $u = get_userdata( $uid ); if ( ! $approval ) { $u->add_role( self::ROLE ); } Mailer::send_template( 'vendor_applied', get_option( 'admin_email' ), array( 'vendor' => $u->display_name, 'store' => $st['name'], 'review_url' => Dashboard::url( 'vendors' ) ) ); /* translators: %s: value. */ Notify::send_to_cap( 'frontglut_manage_vendors', sprintf( __( 'Vendor application: %s', 'frontglut' ), $st['name'] ), '', Dashboard::url( 'vendors' ) ); Audit::log( 'vendor_applied', (string) $uid ); do_action( 'frontglut_vendor_applied', $uid, ! $approval, $v ); return $approval ? 'pending' : 'approved'; } public function ajax_apply( $req ) { $uid = get_current_user_id(); if ( ! Settings::get( 'vendors.registration_open' ) ) { Ajax::fail( __( 'Vendor registration is closed.', 'frontglut' ) ); } if ( get_user_meta( $uid, 'frontglut_vendor_status', true ) ) { Ajax::fail( __( 'You already applied.', 'frontglut' ) ); } list( $v, $err ) = Fields::process( $this->apply_fields(), isset( $req['frontglut_f'] ) ? $req['frontglut_f'] : array() ); if ( $err ) { Ajax::fail( __( 'Please fix the highlighted fields.', 'frontglut' ), 400, array( 'errors' => $err ) ); } $state = $this->make_application( $uid, $v ); return array( 'message' => 'approved' === $state ? __( 'Your store is ready!', 'frontglut' ) : __( 'Application sent. We will email you when it is reviewed.', 'frontglut' ) ); } public function ajax_register( $req ) { if ( ! Settings::get( 'vendors.registration_open' ) || is_user_logged_in() ) { Ajax::fail( __( 'Not available.', 'frontglut' ), 403 ); } if ( ! \FrontGlut\Modules\Forms\Forms::check_honeypot( $req ) ) { Ajax::fail( __( 'Spam detected.', 'frontglut' ) ); } if ( \FrontGlut\Core\Captcha::active_for( 'register' ) ) { $c = \FrontGlut\Core\Captcha::verify( $req ); if ( is_wp_error( $c ) ) { Ajax::fail( $c->get_error_message() ); } } if ( ! RateLimiter::hit( 'vreg', 5, HOUR_IN_SECONDS ) ) { Ajax::fail( __( 'Too many attempts.', 'frontglut' ), 429 ); } list( $sv, $serr ) = Fields::process( $this->apply_fields(), isset( $req['frontglut_s'] ) ? $req['frontglut_s'] : array() ); if ( $serr ) { Ajax::fail( __( 'Please fix the highlighted fields.', 'frontglut' ), 400, array( 'errors' => $serr ) ); } return $this->call_register( $req ); } private function call_register( $req ) { // Create the user inline (mirrors Auth::ajax_register but returns instead of exiting). $form = \FrontGlut\Modules\Forms\Forms::default_form( 'register' ); $post = isset( $req['frontglut_f'] ) && is_array( $req['frontglut_f'] ) ? $req['frontglut_f'] : array(); list( $v, $errors ) = Fields::process( $form['fields'], $post ); $login = isset( $v['user_login'] ) ? sanitize_user( $v['user_login'], true ) : ''; $email = $v['user_email'] ?? ''; $pass = isset( $post['user_pass'] ) && is_string( $post['user_pass'] ) ? $post['user_pass'] : ''; if ( $login && ( username_exists( $login ) || ! validate_username( $login ) ) ) { $errors['user_login'] = __( 'This username is not available.', 'frontglut' ); } if ( $email && email_exists( $email ) ) { $errors['user_email'] = __( 'This email is already registered.', 'frontglut' ); } if ( isset( $post['user_pass2'] ) && $pass !== (string) $post['user_pass2'] ) { $errors['user_pass2'] = __( 'Passwords do not match.', 'frontglut' ); } if ( strlen( $pass ) < 8 ) { $errors['user_pass'] = __( 'Password must be at least 8 characters.', 'frontglut' ); } if ( $errors ) { Ajax::fail( __( 'Please fix the highlighted fields.', 'frontglut' ), 400, array( 'errors' => $errors ) ); } $uid = wp_insert_user( array( 'user_login' => $login, 'user_email' => $email, 'user_pass' => $pass, 'role' => \FrontGlut\Core\Caps::registration_role( class_exists( 'WooCommerce' ) ? 'customer' : 'subscriber' ) ) ); if ( is_wp_error( $uid ) ) { Ajax::fail( $uid->get_error_message() ); } do_action( 'frontglut_user_registered', $uid, $v ); list( $sv ) = Fields::process( $this->apply_fields(), isset( $req['frontglut_s'] ) ? $req['frontglut_s'] : array() ); $state = $this->make_application( $uid, $sv ); // Same email-verification / admin-approval rules as the normal registration form. list( $gate_msg, $auto ) = \FrontGlut\Modules\Auth\Auth::account_gate( $uid, $login, $email ); Audit::log( 'registered', $login, 'vendor', $uid ); if ( ! $auto ) { return array( 'message' => $gate_msg, 'redirect' => '' ); } wp_set_current_user( $uid ); wp_set_auth_cookie( $uid, true ); return array( 'message' => 'approved' === $state ? __( 'Welcome! Your store is ready.', 'frontglut' ) : __( 'Account created. Your store application awaits approval.', 'frontglut' ), 'redirect' => Dashboard::url() ); } /* ---------------- menu / dashboard ---------------- */ public function menu( $i ) { $i['store'] = array( 'label' => __( 'My store', 'frontglut' ), 'icon' => 'store', 'group' => 'market', 'order' => 30, 'cap' => 'frontglut_is_vendor', 'render' => array( $this, 'screen_store' ) ); $i['earnings'] = array( 'label' => __( 'Earnings', 'frontglut' ), 'icon' => 'money-alt', 'group' => 'market', 'order' => 31, 'cap' => 'frontglut_is_vendor', 'render' => array( $this->commissions, 'screen_earnings' ) ); $i['vendors'] = array( 'label' => __( 'Vendors', 'frontglut' ), 'icon' => 'businessperson', 'group' => 'market', 'order' => 32, 'cap' => 'frontglut_manage_vendors', 'render' => array( $this, 'screen_vendors' ), 'badge' => array( $this, 'pending_count' ) ); $i['commissions'] = array( 'label' => __( 'Commissions', 'frontglut' ), 'icon' => 'chart-line', 'group' => 'market', 'order' => 33, 'cap' => 'frontglut_manage_vendors', 'render' => array( $this->commissions, 'screen_commissions' ) ); $i['withdrawals'] = array( 'label' => __( 'Withdrawals', 'frontglut' ), 'icon' => 'bank', 'group' => 'market', 'order' => 34, 'cap' => 'frontglut_manage_vendors', 'render' => array( $this->commissions, 'screen_withdrawals' ), 'badge' => array( $this->commissions, 'pending_withdrawals' ) ); return $i; } public function pending_count() { $q = new \WP_User_Query( array( 'meta_key' => 'frontglut_vendor_status', 'meta_value' => 'pending', 'fields' => 'ID', 'number' => 1 ) ); return (int) $q->get_total(); } public function widgets( $w ) { $w['vendor_earnings'] = array( 'title' => __( 'My earnings', 'frontglut' ), 'order' => 5, 'size' => 1, 'cap' => 'frontglut_is_vendor', 'render' => function () { $b = $this->commissions->balance( get_current_user_id() ); return '
' . Ui::stat( __( 'Available', 'frontglut' ), wc_price( $b['available'] ) ) . Ui::stat( __( 'Pending', 'frontglut' ), wc_price( $b['pending'] ) ) . '

' . Ui::btn( __( 'Withdraw', 'frontglut' ), Dashboard::url( 'earnings' ), 'frontglut-sm frontglut-primary' ) . ' ' . ( self::store_url( get_current_user_id() ) ? Ui::btn( __( 'View my store', 'frontglut' ), self::store_url( get_current_user_id() ), 'frontglut-sm' ) : '' ) . '

'; } ); return $w; } /* ---------------- store settings ---------------- */ public function screen_store() { $uid = get_current_user_id(); if ( ! self::is_vendor( $uid ) && ! current_user_can( 'manage_options' ) ) { return Ui::notice( esc_html__( 'Your store is not active.', 'frontglut' ), 'warn' ); } $st = self::store( $uid ); $days = array( 'mon' => 'Monday', 'tue' => 'Tuesday', 'wed' => 'Wednesday', 'thu' => 'Thursday', 'fri' => 'Friday', 'sat' => 'Saturday', 'sun' => 'Sunday' ); $hf = ''; foreach ( $days as $k => $l ) { $h = $st['hours'][ $k ] ?? array(); $hf .= ''; } $hf .= '
' . esc_html__( 'Day', 'frontglut' ) . '' . esc_html__( 'Open', 'frontglut' ) . '' . esc_html__( 'Close', 'frontglut' ) . '' . esc_html__( 'Closed', 'frontglut' ) . '
' . esc_html( $l ) . '
'; $fd = function ( $key, $type, $label, $extra = array() ) { return \FrontGlut\Modules\Products\Products::fd( $key, $type, $label, $extra ); }; $tabs = array( 'store' => array( __( 'Store', 'frontglut' ), array( $fd( 'name', 'text', __( 'Store name', 'frontglut' ), array( 'required' => 1 ) ), $fd( 'logo', 'image', __( 'Logo', 'frontglut' ), array( 'width' => '50' ) ), $fd( 'banner', 'image', __( 'Banner', 'frontglut' ), array( 'width' => '50' ) ), $fd( 'description', 'textarea', __( 'About the store', 'frontglut' ) ), $fd( 'phone', 'tel', __( 'Phone', 'frontglut' ), array( 'width' => '50' ) ), $fd( 'email', 'email', __( 'Public email (for the contact form)', 'frontglut' ), array( 'width' => '50' ) ), $fd( 'street', 'text', __( 'Street', 'frontglut' ) ), $fd( 'city', 'text', __( 'City', 'frontglut' ), array( 'width' => '33' ) ), $fd( 'state', 'text', __( 'State', 'frontglut' ), array( 'width' => '33' ) ), $fd( 'zip', 'text', __( 'ZIP', 'frontglut' ), array( 'width' => '33' ) ), $fd( 'country', 'text', __( 'Country', 'frontglut' ) ), $fd( 'map', 'url', __( 'Map link (Google Maps / OpenStreetMap)', 'frontglut' ) ), ) ), 'social' => array( __( 'Social', 'frontglut' ), array( $fd( 'website', 'url', __( 'Website', 'frontglut' ), array( 'width' => '50' ) ), $fd( 'facebook', 'url', 'Facebook', array( 'width' => '50' ) ), $fd( 'twitter', 'url', 'X / Twitter', array( 'width' => '50' ) ), $fd( 'instagram', 'url', 'Instagram', array( 'width' => '50' ) ), $fd( 'youtube', 'url', 'YouTube', array( 'width' => '50' ) ), ) ), 'policy' => array( __( 'Policies', 'frontglut' ), array( $fd( 'policy_shipping', 'textarea', __( 'Shipping policy', 'frontglut' ) ), $fd( 'policy_refund', 'textarea', __( 'Refund policy', 'frontglut' ) ), ) ), 'shipping' => array( __( 'Shipping', 'frontglut' ), array( $fd( 'ship_default', 'number', __( 'Default shipping fee', 'frontglut' ), array( 'min' => 0, 'help' => __( 'Charged once per order for destinations not listed below. Needs the "Vendor shipping" method in a WooCommerce shipping zone.', 'frontglut' ) ) ), $fd( 'ship_rates', 'textarea', __( 'Rates by country', 'frontglut' ), array( 'help' => __( 'One per line: US|5.00 — country code, fee. Use * for everything else.', 'frontglut' ) ) ), $fd( 'ship_time', 'text', __( 'Processing time', 'frontglut' ), array( 'placeholder' => '1-2 business days' ) ), ) ), 'payout' => array( __( 'Payout', 'frontglut' ), array( $fd( 'payout_method', 'select', __( 'Preferred method', 'frontglut' ), array( 'options' => array( 'paypal' => 'PayPal', 'bank' => 'Bank transfer', 'manual' => 'Other' ) ) ), $fd( 'payout_paypal', 'email', __( 'PayPal email', 'frontglut' ) ), $fd( 'payout_bank', 'textarea', __( 'Bank details', 'frontglut' ), array( 'help' => __( 'Account holder, IBAN/account number, bank & SWIFT.', 'frontglut' ) ) ), ) ), 'modes' => array( __( 'Vacation & catalog', 'frontglut' ), array( $fd( 'vacation', 'checkbox', __( 'Vacation mode (products cannot be bought)', 'frontglut' ) ), $fd( 'vacation_until', 'date', __( 'Back on', 'frontglut' ), array( 'width' => '50' ) ), $fd( 'vacation_msg', 'text', __( 'Message shown to shoppers', 'frontglut' ), array( 'width' => '50' ) ), $fd( 'catalog_mode', 'checkbox', __( 'Catalog mode (hide add to cart)', 'frontglut' ) ), $fd( 'hide_price', 'checkbox', __( 'Also hide prices', 'frontglut' ) ), ) ), ); $vals = $st; $tb = '
'; $pn = ''; $first = true; foreach ( $tabs as $id => $t ) { $tb .= ''; $pn .= '
' . \FrontGlut\Modules\Forms\Forms::render_fields( $t[1], $vals, 'frontglut_f', 0 ) . '
'; $first = false; } $tb .= '
'; $pn .= '
' . $hf . '
'; $url = self::store_url( $uid ); return Ui::header( __( 'My store', 'frontglut' ), $url ? '' . esc_html__( 'View store page', 'frontglut' ) . '' : '' ) . '
' . Ui::form_open( 'vendor_store_save', array( 'after' => 'reload' ) ) . '
' . $tb . $pn . '
' . Ui::form_close( __( 'Save store', 'frontglut' ) ) . '
'; } public function ajax_store_save( $req ) { $uid = get_current_user_id(); if ( ! self::is_vendor( $uid ) && ! current_user_can( 'manage_options' ) ) { Ajax::fail( __( 'Your store is not active.', 'frontglut' ), 403 ); } $fd = function ( $k, $t ) { return \FrontGlut\Modules\Products\Products::fd( $k, $t, $k ); }; $defs = array( $fd( 'name', 'text' ), $fd( 'logo', 'image' ), $fd( 'banner', 'image' ), $fd( 'description', 'textarea' ), $fd( 'phone', 'tel' ), $fd( 'email', 'email' ), $fd( 'street', 'text' ), $fd( 'city', 'text' ), $fd( 'state', 'text' ), $fd( 'zip', 'text' ), $fd( 'country', 'text' ), $fd( 'map', 'url' ), $fd( 'website', 'url' ), $fd( 'facebook', 'url' ), $fd( 'twitter', 'url' ), $fd( 'instagram', 'url' ), $fd( 'youtube', 'url' ), $fd( 'policy_shipping', 'textarea' ), $fd( 'policy_refund', 'textarea' ), $fd( 'ship_default', 'number' ), $fd( 'ship_rates', 'textarea' ), $fd( 'ship_time', 'text' ), $fd( 'payout_method', 'select' ), $fd( 'payout_paypal', 'email' ), $fd( 'payout_bank', 'textarea' ), $fd( 'vacation', 'checkbox' ), $fd( 'vacation_until', 'date' ), $fd( 'vacation_msg', 'text' ), $fd( 'catalog_mode', 'checkbox' ), $fd( 'hide_price', 'checkbox' ), ); foreach ( $defs as &$d ) { if ( 'payout_method' === $d['key'] ) { $d['options'] = array( 'paypal' => 'PayPal', 'bank' => 'Bank transfer', 'manual' => 'Other' ); } if ( 'name' === $d['key'] ) { $d['required'] = 1; } } unset( $d ); list( $v, $err ) = Fields::process( $defs, isset( $req['frontglut_f'] ) ? $req['frontglut_f'] : array() ); if ( $err ) { Ajax::fail( __( 'Please fix the highlighted fields.', 'frontglut' ), 400, array( 'errors' => $err ) ); } $st = self::store( $uid ); foreach ( $v as $k => $val ) { if ( in_array( $k, array( 'logo', 'banner' ), true ) ) { $ids = Uploads::filter_owned( (array) $val ); $st[ $k ] = $ids ? (int) $ids[0] : 0; Uploads::claim( $ids, 0 ); } else { $st[ $k ] = $val; } } $hours = array(); foreach ( (array) ( $req['hours'] ?? array() ) as $day => $h ) { if ( in_array( $day, array( 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun' ), true ) ) { $hours[ $day ] = array( 'open' => preg_match( '/^\d{2}:\d{2}$/', $h['open'] ?? '' ) ? $h['open'] : '09:00', 'close' => preg_match( '/^\d{2}:\d{2}$/', $h['close'] ?? '' ) ? $h['close'] : '17:00', 'closed' => ! empty( $h['closed'] ) ? 1 : 0 ); } } $st['hours'] = $hours; if ( ! $st['slug'] ) { $st['slug'] = $this->unique_slug( $st['name'], $uid ); } update_user_meta( $uid, 'frontglut_store', $st ); update_user_meta( $uid, 'frontglut_store_slug', $st['slug'] ); Audit::log( 'store_saved', (string) $uid ); return array( 'message' => __( 'Store saved.', 'frontglut' ) ); } /* ---------------- admin: vendors list ---------------- */ public function screen_vendors() { $a = Request::route( 'a', 'key', 'list' ); $id = Request::route( 'id', 'int', 0 ); if ( 'view' === $a && $id ) { return $this->vendor_detail( $id ); } $status = Request::filter( 'status', 'key', '' ); $pg = Request::page(); $args = array( 'number' => 20, 'paged' => $pg, 'meta_query' => array( $status ? array( 'key' => 'frontglut_vendor_status', 'value' => $status ) : array( 'key' => 'frontglut_vendor_status', 'compare' => 'EXISTS' ) ) ); $uq = new \WP_User_Query( $args ); $rows = array(); foreach ( $uq->get_results() as $u ) { $st = self::store( $u->ID ); $s = get_user_meta( $u->ID, 'frontglut_vendor_status', true ); $bal = $this->commissions->balance( $u->ID ); $act = ''; if ( 'pending' === $s ) { $act .= Ui::action_btn( __( 'Approve', 'frontglut' ), 'vendor_admin_status', array( 'id' => $u->ID, 'status' => 'approved' ), 'frontglut-primary' ) . ''; } elseif ( 'approved' === $s ) { $act .= Ui::action_btn( __( 'Disable', 'frontglut' ), 'vendor_admin_status', array( 'id' => $u->ID, 'status' => 'disabled' ), 'frontglut-danger', true ); } else { $act .= Ui::action_btn( __( 'Enable', 'frontglut' ), 'vendor_admin_status', array( 'id' => $u->ID, 'status' => 'approved' ), 'frontglut-primary' ); } $rows[] = array( 'store' => '' . esc_html( $st['name'] ?: $u->display_name ) . '
' . esc_html( $u->user_email ) . '', 'status' => Ui::status_badge( 'approved' === $s ? 'active' : $s ), 'products' => (int) count_user_posts( $u->ID, 'product' ), 'balance' => wc_price( $bal['available'] ), 'since' => Ui::date( $u->user_registered ), 'act' => '
' . $act . '
', ); } $pills = '
'; foreach ( array( '' => 'All', 'pending' => 'Pending', 'approved' => 'Active', 'disabled' => 'Disabled' ) as $k => $l ) { $pills .= '' . esc_html( $l ) . ''; } $pills .= '
'; return Ui::header( __( 'Vendors', 'frontglut' ) ) . $pills . Ui::table( array( 'store' => __( 'Store', 'frontglut' ), 'status' => __( 'Status', 'frontglut' ), 'products' => __( 'Products', 'frontglut' ), 'balance' => __( 'Balance', 'frontglut' ), 'since' => __( 'Joined', 'frontglut' ), 'act' => '' ), $rows, array( 'empty' => __( 'No vendors yet.', 'frontglut' ) ) ) . Ui::pager( $uq->get_total(), 20, $pg, array( 'status' => $status ) ); } private function vendor_detail( $id ) { $u = get_userdata( $id ); if ( ! $u || ! get_user_meta( $id, 'frontglut_vendor_status', true ) ) { return Ui::notice( esc_html__( 'Vendor not found.', 'frontglut' ), 'warn' ); } $st = self::store( $id ); $c = $st['commission'] ? $st['commission'] : array( 'type' => '', 'percent' => '', 'flat' => '' ); $b = $this->commissions->balance( $id ); $h = Ui::header( $st['name'] ?: $u->display_name, Ui::btn( __( '← Vendors', 'frontglut' ), Dashboard::url( 'vendors' ) ) . ( self::store_url( $id ) ? ' ' . esc_html__( 'Store page', 'frontglut' ) . '' : '' ), $u->user_email ); $h .= '
' . Ui::stat( __( 'Available', 'frontglut' ), wc_price( $b['available'] ) ) . Ui::stat( __( 'Pending', 'frontglut' ), wc_price( $b['pending'] ) ) . Ui::stat( __( 'Withdrawn', 'frontglut' ), wc_price( $b['withdrawn'] ) ) . Ui::stat( __( 'Products', 'frontglut' ), (int) count_user_posts( $id, 'product' ) ) . '
'; $h .= '

' . esc_html__( 'Commission override', 'frontglut' ) . '

' . esc_html__( 'Leave empty to use the marketplace default.', 'frontglut' ) . '

' . Ui::form_open( 'vendor_commission_save', array( 'after' => 'reload' ) ) . Ui::hidden( 'id', $id ); $h .= '
' . Ui::form_close( __( 'Save', 'frontglut' ) ) . '
'; return $h; } public function ajax_commission_save( $req ) { $id = (int) ( $req['id'] ?? 0 ); if ( ! self::is_vendor( $id ) ) { Ajax::fail( __( 'Vendor not found.', 'frontglut' ), 404 ); } $st = self::store( $id ); $type = in_array( $req['type'] ?? '', array( 'percent', 'flat', 'combined' ), true ) ? $req['type'] : ''; $st['commission'] = $type ? array( 'type' => $type, 'percent' => is_numeric( $req['percent'] ?? '' ) ? min( 100, max( 0, (float) $req['percent'] ) ) : '', 'flat' => is_numeric( $req['flat'] ?? '' ) ? max( 0, (float) $req['flat'] ) : '' ) : array(); update_user_meta( $id, 'frontglut_store', $st ); Audit::log( 'vendor_commission', (string) $id ); return array( 'message' => __( 'Commission override saved.', 'frontglut' ) ); } public function ajax_admin_status( $req ) { $id = (int) ( $req['id'] ?? 0 ); $u = get_userdata( $id ); $to = in_array( $req['status'] ?? '', array( 'approved', 'disabled' ), true ) ? $req['status'] : ''; if ( ! $u || ! $to || ! get_user_meta( $id, 'frontglut_vendor_status', true ) ) { Ajax::fail( __( 'Invalid request.', 'frontglut' ) ); } update_user_meta( $id, 'frontglut_vendor_status', $to ); $st = self::store( $id ); if ( 'approved' === $to ) { $u->add_role( self::ROLE ); Mailer::send_template( 'vendor_approved', $u->user_email, array( 'name' => $u->display_name, 'store' => $st['name'], 'dashboard_url' => Dashboard::url( 'store' ) ) ); Notify::send( $id, __( 'Your store is approved', 'frontglut' ), '', Dashboard::url( 'store' ), 'ok' ); } else { $u->remove_role( self::ROLE ); Mailer::send_template( 'vendor_rejected', $u->user_email, array( 'name' => $u->display_name, 'store' => $st['name'], 'reason' => sanitize_textarea_field( $req['reason'] ?? '' ) ) ); } Audit::log( 'vendor_' . $to, (string) $id ); return array( 'message' => __( 'Vendor updated.', 'frontglut' ) ); } /* ---------------- order scope ---------------- */ public function orders_scope( $args, $uid ) { if ( self::is_vendor( $uid ) ) { return array( 'meta_key' => '_frontglut_vendor_id', 'meta_value' => $uid ); // phpcs:ignore WordPress.DB.SlowDBQuery } return $args; } public function can_view_order( $ok, $order, $uid ) { return $ok || ( self::is_vendor( $uid ) && (int) $order->get_meta( '_frontglut_vendor_id' ) === (int) $uid ); } /* ---------------- vacation / catalog ---------------- */ private function vendor_off( $product_id ) { $v = self::product_vendor( $product_id ); if ( ! $v ) { return null; } $s = self::store( $v ); if ( ! empty( $s['vacation'] ) && ( empty( $s['vacation_until'] ) || strtotime( $s['vacation_until'] . ' 23:59:59' ) >= time() ) ) { return 'vacation'; } if ( ! empty( $s['catalog_mode'] ) ) { return 'catalog'; } return null; } public function purchasable( $ok, $product ) { $id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(); return $ok && ! $this->vendor_off( $id ); } public function price_html( $html, $product ) { $id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(); $v = self::product_vendor( $id ); if ( $v && ! empty( self::store( $v )['hide_price'] ) && ! empty( self::store( $v )['catalog_mode'] ) ) { return ''; } return $html; } public function vacation_notice() { global $product; if ( $product && 'vacation' === $this->vendor_off( $product->get_id() ) ) { $s = self::store( self::product_vendor( $product->get_id() ) ); /* translators: %s: value. */ echo '

' . esc_html( $s['vacation_msg'] ? $s['vacation_msg'] : __( 'This store is on vacation.', 'frontglut' ) ) . ( $s['vacation_until'] ? ' ' . esc_html( sprintf( __( 'Back on %s.', 'frontglut' ), $s['vacation_until'] ) ) : '' ) . '

'; } } }