$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 = '';
$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'] ) ) : '' ) . '
';
}
}
}