true, 'sms_driver' => '', 'sms_gateways' => [], 'sms_policy' => Gateways::POLICY_FAILOVER, 'email_enabled' => false, 'email_driver' => WpMail::ID, 'log_sends' => true, 'admin_phones' => '', 'events' => [], 'quiet_enabled' => true, 'quiet_from' => '22:00', 'quiet_to' => '08:00', 'weekly_cap' => 2, 'holdout_percent' => 10, 'recent_days' => 7, 'test_phones' => '', ]; // Each provider contributes its own defaults, so a driver's credentials // live beside the rest of this module's settings without the module // needing to know what any provider requires. foreach ( [ DriverRegistry::CHANNEL_SMS, DriverRegistry::CHANNEL_EMAIL ] as $channel ) { foreach ( DriverRegistry::all( $channel ) as $driver ) { foreach ( $driver->settingsSchema() as $key => $field ) { $defaults[ $key ] = $field['default'] ?? ''; } } } return $defaults; } /** * {@inheritDoc} */ public function settingsSchema(): array { $schema = [ 'sms_enabled' => [ 'type' => 'checkbox', 'label' => __( 'ارسال پیامک فعال باشد', 'rahkar-toolkit' ), 'default' => true, 'section' => 'general', ], 'email_enabled' => [ 'type' => 'checkbox', 'label' => __( 'ارسال ایمیل فعال باشد', 'rahkar-toolkit' ), 'help' => __( 'برای کاربرانی که به‌جای شماره موبایل، نشانی ایمیل وارد می‌کنند.', 'rahkar-toolkit' ), 'default' => false, 'section' => 'general', ], 'email_driver' => [ 'type' => 'select', 'label' => __( 'روش ارسال ایمیل', 'rahkar-toolkit' ), 'options' => DriverRegistry::choices( DriverRegistry::CHANNEL_EMAIL ), 'default' => WpMail::ID, 'section' => 'general', ], 'admin_phones' => [ 'type' => 'text', 'label' => __( 'شماره موبایل مدیران', 'rahkar-toolkit' ), 'help' => __( 'برای اطلاع‌رسانی‌هایی که به کارکنان فروشگاه می‌رود. چند شماره را با ویرگول جدا کنید. هر رویداد می‌تواند شماره اختصاصی خودش را هم داشته باشد.', 'rahkar-toolkit' ), 'default' => '', 'section' => 'general', ], 'utm_links' => [ 'type' => 'checkbox', 'label' => __( 'برچسب UTM روی لینک‌های پیام', 'rahkar-toolkit' ), 'help' => __( 'به لینک‌هایی که ماژول‌ها می‌فرستند (بازیابی سبد، دعوت به نظر، خبر موجودی) برچسب استاندارد UTM اضافه می‌شود تا آنالیتیکس و داشبورد فروش بفهمند کدام پیام بازدید یا سفارش آورد. مقدارها ثابت‌اند و فقط می‌شود خاموششان کرد.', 'rahkar-toolkit' ), 'default' => true, 'section' => 'general', ], 'log_sends' => [ 'type' => 'checkbox', 'label' => __( 'ثبت لاگ ارسال‌ها', 'rahkar-toolkit' ), 'help' => __( 'شناسه پیام سرویس‌دهنده ثبت می‌شود تا در صورت شکایت مشتری قابل پیگیری باشد. کد یک‌بار مصرف هرگز در لاگ ذخیره نمی‌شود.', 'rahkar-toolkit' ), 'default' => true, 'section' => 'general', ], ]; // Send rules (SendPolicy). They apply to promotional and service messages // only; a login code or an order confirmation is never held back. $schema['quiet_enabled'] = [ 'type' => 'checkbox', 'label' => __( 'ساعت سکوت', 'rahkar-toolkit' ), 'help' => __( 'پیام تبلیغاتی و پیام‌هایی مثل دعوت به نظر در این ساعت‌ها نمی‌روند و صبح فرستاده می‌شوند، نه حذف. پیامک سفارش و کد ورود همیشه بی‌درنگ می‌روند.', 'rahkar-toolkit' ), 'default' => true, 'section' => 'policy', ]; $schema['quiet_from'] = [ 'type' => 'time', 'label' => __( 'شروع ساعت سکوت', 'rahkar-toolkit' ), 'help' => __( 'به وقت سایت.', 'rahkar-toolkit' ), 'default' => '22:00', 'section' => 'policy', ]; $schema['quiet_to'] = [ 'type' => 'time', 'label' => __( 'پایان ساعت سکوت', 'rahkar-toolkit' ), 'help' => __( 'بازه می‌تواند از نیمه‌شب رد شود. اگر هر دو ساعت یکی باشند، بازه‌ای در کار نیست.', 'rahkar-toolkit' ), 'default' => '08:00', 'section' => 'policy', ]; $schema['weekly_cap'] = [ 'type' => 'number', 'label' => __( 'سقف جریان تبلیغاتی در هفته، برای هر شماره', 'rahkar-toolkit' ), 'help' => __( 'هر جریان یک بار شمرده می‌شود: سه یادآور یک سبد یک جریان است. صفر یعنی بدون سقف.', 'rahkar-toolkit' ), 'default' => 2, 'min' => 0, 'max' => 10, 'section' => 'policy', ]; $schema['holdout_percent'] = [ 'type' => 'number', 'label' => __( 'سهم گروه کنترل (درصد)', 'rahkar-toolkit' ), 'help' => __( 'این سهم از مشتری‌ها پیام تبلیغاتی نمی‌گیرند تا داشبورد راهکار فروش بتواند فروش افزایشی هر ماژول را بسنجد. بین ۵ و ۲۰.', 'rahkar-toolkit' ), 'default' => SendPolicy::HOLDOUT_DEFAULT, 'min' => SendPolicy::HOLDOUT_MIN, 'max' => SendPolicy::HOLDOUT_MAX, 'section' => 'policy', ]; $schema['recent_days'] = [ 'type' => 'number', 'label' => __( 'خرید تازه (روز)', 'rahkar-toolkit' ), 'help' => __( 'کسی که در این چند روز خرید کرده، وارد جریان تبلیغاتی تازه‌ای مثل بازگشت مشتری یا یادآور خرید مجدد نمی‌شود. سبد رهاشده و خرید دوم این قاعده را ندارند. صفر یعنی خاموش. به ماژول «شاخص‌های مشتری» نیاز دارد.', 'rahkar-toolkit' ), 'default' => SendPolicy::RECENT_DEFAULT, 'min' => 0, 'max' => 30, 'section' => 'policy', ]; $schema['test_phones'] = [ 'type' => 'textarea', 'label' => __( 'شماره‌های آزمایشی', 'rahkar-toolkit' ), 'help' => __( 'شماره‌ی خودتان و همکاران، هر کدام در یک خط. این شماره‌ها همیشه پیام تبلیغاتی را می‌گیرند، به گروه کنترل و سقف هفتگی نمی‌خورند، و در آمار داشبورد راهکار فروش شمرده نمی‌شوند. برای آزمایش یادآور سبد و بقیه‌ی جریان‌ها.', 'rahkar-toolkit' ), 'default' => '', 'section' => 'policy', ]; // Gateway credentials are not listed here: they have their own screen, // which can show whether each gateway is configured, test it and order it // — none of which a list of text fields can do. return $schema; } /** * {@inheritDoc} */ public function settingsSections(): array { return [ 'general' => __( 'عمومی', 'rahkar-toolkit' ), 'policy' => __( 'قواعد ارسال', 'rahkar-toolkit' ), ]; } /** * {@inheritDoc} */ public function register(): void { // The queue worker needs this handler in background contexts, and // `sendNow` needs it wherever a code is requested. Registering the hook is // cheap; the Notifier itself only loads when a job actually runs — which is // true only because the hook is named through the shared constant. Spelling // it `Notifier::JOB_HOOK` would load the notifier to read the constant, and // the comment above would have been quietly false. add_action( Hooks::NOTIFICATION_SEND, static fn( array $payload ) => Plugin::make( Notifier::class )->handleJob( $payload ) ); add_action( Hooks::BLACKLIST_CHECK, static fn( array $payload ) => Plugin::make( \Rahkar\WP\Notifications\BlacklistCheck::class )->run( $payload ) ); if ( defined( 'WP_CLI' ) && WP_CLI ) { \WP_CLI::add_command( 'rahkar sms-count', [ SmsCountCommand::class, 'run' ] ); } // Registered wherever an order status can change: the admin screen, a // gateway callback arriving as REST or a webhook, WP-CLI, and the // background context that drains the queue. if ( Manifest::flag( 'wc' ) ) { // Priority 20: after WooCommerce has settled stock, totals and dates, so a // message quoting the order total quotes the final one. add_action( 'woocommerce_order_status_changed', static fn( $orderId, $from, $to, $order ) => Plugin::make( OrderNotifier::class )->onStatusChanged( $orderId, $from, $to, $order ), 20, 4 ); add_action( Hooks::ORDER_NOTIFICATION, static fn( array $payload ) => Plugin::make( OrderNotifier::class )->handleJob( $payload ) ); } // A privacy erasure removes the person's flow entries; the opt-out row stays. add_filter( 'wp_privacy_personal_data_erasers', static fn( $erasers ) => SendPrivacy::registerEraser( $erasers ) ); // Per-event pattern codes reach the SMS driver through this filter, so the // driver never has to know an events system exists. add_filter( 'rahkar_wp_sms_pattern', static function ( string $pattern, string $event ): string { $definition = OrderEvents::all()[ $event ] ?? null; if ( null === $definition ) { return $pattern; } $configured = EventSettings::for( $definition )['pattern']; return '' !== $configured ? $configured : $pattern; }, 10, 2 ); if ( is_admin() ) { add_action( 'wp_ajax_rahkar_test_notification', static fn() => Plugin::make( TestSender::class )->handle() ); // The pattern picker beside every pattern-code field. The action is // spelled out rather than read from `PatternPicker::ACTION`, which // would load the class on every admin request to learn a string. add_action( 'wp_ajax_rahkar_wp_patterns', static fn() => \Rahkar\WP\Admin\PatternPicker::handleAjax() ); // The submenu is declared here rather than inside the screen class: the // old `EventsPage->register()` hooked `admin_menu`, which fires on every // admin request, so the 400-line screen was opened on all of them. Only // the render callback needs the class, and only when the screen is shown. add_action( 'admin_menu', static function (): void { add_submenu_page( Menu::SLUG, __( 'درگاه‌های پیامک', 'rahkar-toolkit' ), __( 'درگاه‌های پیامک', 'rahkar-toolkit' ), Menu::CAPABILITY, Menu::SLUG . '-gateways', static fn() => Plugin::make( GatewaysPage::class )->render() ); add_submenu_page( Menu::SLUG, __( 'اطلاع‌رسانی‌ها', 'rahkar-toolkit' ), __( 'اطلاع‌رسانی‌ها', 'rahkar-toolkit' ), Menu::CAPABILITY, Menu::SLUG . '-events', static fn() => Plugin::make( EventsPage::class )->render() ); add_submenu_page( Menu::SLUG, __( 'فهرست لغو', 'rahkar-toolkit' ), __( 'فهرست لغو', 'rahkar-toolkit' ), Menu::CAPABILITY, Menu::SLUG . '-optouts', static fn() => Plugin::make( OptoutsPage::class )->render() ); }, 15 ); } } }