-- ============================================= -- OMADA MULTI-TENANT WIFI PLATFORM -- Final Schema -- ============================================= SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ============================================= -- TABLE 1: clients -- ============================================= DROP TABLE IF EXISTS `clients`; CREATE TABLE `clients` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `business_name` VARCHAR(100) NOT NULL, `contact_name` VARCHAR(100) NOT NULL, `email` VARCHAR(255) NOT NULL, `phone` VARCHAR(20) NOT NULL, `payment_display_name` VARCHAR(100) DEFAULT NULL, `payment_phone` VARCHAR(20) DEFAULT NULL, `payment_provider` VARCHAR(32) DEFAULT NULL, `payment_instructions` VARCHAR(255) DEFAULT NULL, `payment_qr_image_url` VARCHAR(512) DEFAULT NULL, `manual_sales_enabled` TINYINT(1) NOT NULL DEFAULT 0, `portal_payment_mode` VARCHAR(24) NOT NULL DEFAULT 'both', `password_hash` VARCHAR(255) NOT NULL, `commission_rate` DECIMAL(5,4) NOT NULL DEFAULT 0.0500, `balance` DECIMAL(12,2) NOT NULL DEFAULT 0.00, `total_earned` DECIMAL(12,2) NOT NULL DEFAULT 0.00, `total_withdrawn` DECIMAL(12,2) NOT NULL DEFAULT 0.00, `is_active` TINYINT(1) NOT NULL DEFAULT 1, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_email` (`email`), UNIQUE KEY `uk_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 2: client_staff -- ============================================= DROP TABLE IF EXISTS `client_staff`; CREATE TABLE `client_staff` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `client_id` INT UNSIGNED NOT NULL, `name` VARCHAR(100) NOT NULL, `email` VARCHAR(255) NOT NULL, `phone` VARCHAR(20) DEFAULT NULL, `password_hash` VARCHAR(255) NOT NULL, `role` ENUM('manager','viewer') NOT NULL DEFAULT 'viewer', `is_active` TINYINT(1) NOT NULL DEFAULT 1, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_staff_email` (`email`), KEY `idx_staff_client` (`client_id`), CONSTRAINT `fk_staff_client` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 3: devices -- ============================================= DROP TABLE IF EXISTS `devices`; CREATE TABLE `devices` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `client_id` INT UNSIGNED NOT NULL, `name` VARCHAR(100) NOT NULL, `location` VARCHAR(255) DEFAULT NULL, `omada_site_id` VARCHAR(64) DEFAULT NULL, `omada_portal_id` VARCHAR(64) DEFAULT NULL, `device_username` VARCHAR(50) DEFAULT NULL, `device_password` VARCHAR(100) DEFAULT NULL, `ssid_name` VARCHAR(64) DEFAULT NULL, `ssid_password` VARCHAR(64) DEFAULT NULL, `omada_wlan_id` VARCHAR(64) DEFAULT NULL, `omada_ssid_id` VARCHAR(64) DEFAULT NULL, `mac` VARCHAR(17) NOT NULL, `model` VARCHAR(50) DEFAULT NULL, `firmware_version` VARCHAR(100) DEFAULT NULL, `ip` VARCHAR(45) DEFAULT NULL, `public_ip` VARCHAR(45) DEFAULT NULL, `portal_html_path` VARCHAR(255) DEFAULT NULL, `status` ENUM('pending','adopting','online','offline','removed') NOT NULL DEFAULT 'pending', `registered_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `adopted_at` TIMESTAMP NULL DEFAULT NULL, `last_seen_at` TIMESTAMP NULL DEFAULT NULL, `billing_status` ENUM('trial','active','suspended','cancelled') NOT NULL DEFAULT 'trial', `billing_expires_at` TIMESTAMP NULL DEFAULT NULL, `monthly_fee` DECIMAL(10,2) NOT NULL DEFAULT 20000.00, `cpu_util` TINYINT UNSIGNED DEFAULT NULL, `mem_util` TINYINT UNSIGNED DEFAULT NULL, `client_count` SMALLINT UNSIGNED DEFAULT 0, `guest_count` SMALLINT UNSIGNED DEFAULT 0, `uptime_seconds` INT UNSIGNED DEFAULT 0, `download_bytes` BIGINT UNSIGNED DEFAULT 0, `upload_bytes` BIGINT UNSIGNED DEFAULT 0, `needs_upgrade` TINYINT(1) NOT NULL DEFAULT 0, UNIQUE KEY `uk_device_mac` (`mac`), UNIQUE KEY `uk_omada_site` (`omada_site_id`), KEY `idx_device_client` (`client_id`), KEY `idx_device_status` (`status`), KEY `idx_device_billing` (`billing_status`, `billing_expires_at`), CONSTRAINT `fk_device_client` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 4: device_payments -- ============================================= DROP TABLE IF EXISTS `device_payments`; CREATE TABLE `device_payments` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `client_id` INT UNSIGNED NOT NULL, `device_id` INT UNSIGNED NOT NULL, `amount` DECIMAL(10,2) NOT NULL DEFAULT 20000.00, `reference` VARCHAR(64) NOT NULL, `snippe_payment_id` VARCHAR(100) DEFAULT NULL, `period_start` DATE NOT NULL, `period_end` DATE NOT NULL, `status` ENUM('pending','completed','failed') NOT NULL DEFAULT 'pending', `notes` VARCHAR(255) DEFAULT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `completed_at` TIMESTAMP NULL DEFAULT NULL, UNIQUE KEY `uk_devpay_ref` (`reference`), KEY `idx_devpay_client` (`client_id`), KEY `idx_devpay_device` (`device_id`), KEY `idx_devpay_status` (`status`), CONSTRAINT `fk_devpay_client` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`), CONSTRAINT `fk_devpay_device` FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 5: device_status_log -- ============================================= DROP TABLE IF EXISTS `device_status_log`; CREATE TABLE `device_status_log` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `device_id` INT UNSIGNED NOT NULL, `old_status` VARCHAR(20) NOT NULL, `new_status` VARCHAR(20) NOT NULL, `reason` VARCHAR(255) DEFAULT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY `idx_devlog_device` (`device_id`), KEY `idx_devlog_time` (`created_at`), CONSTRAINT `fk_devlog_device` FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 6: wifi_plans -- ============================================= DROP TABLE IF EXISTS `wifi_plans`; CREATE TABLE `wifi_plans` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `client_id` INT UNSIGNED NOT NULL, `device_id` INT UNSIGNED NOT NULL, `name` VARCHAR(50) NOT NULL, `duration_seconds` INT UNSIGNED NOT NULL, `price` DECIMAL(10,2) NOT NULL, `down_limit` SMALLINT UNSIGNED NOT NULL DEFAULT 2, `down_unit` TINYINT UNSIGNED NOT NULL DEFAULT 2 COMMENT '1=Kbps 2=Mbps', `up_limit` SMALLINT UNSIGNED NOT NULL DEFAULT 2, `up_unit` TINYINT UNSIGNED NOT NULL DEFAULT 2 COMMENT '1=Kbps 2=Mbps', `display_order` TINYINT UNSIGNED NOT NULL DEFAULT 0, `is_active` TINYINT(1) NOT NULL DEFAULT 1, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY `idx_plan_client` (`client_id`), KEY `idx_plan_device` (`device_id`), KEY `idx_plan_active` (`device_id`, `is_active`, `display_order`), CONSTRAINT `fk_plan_client` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`), CONSTRAINT `fk_plan_device` FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 7: payments -- ============================================= DROP TABLE IF EXISTS `payments`; CREATE TABLE `payments` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `reference` VARCHAR(64) NOT NULL, `client_id` INT UNSIGNED NOT NULL, `device_id` INT UNSIGNED NOT NULL, `plan_id` INT UNSIGNED NOT NULL, `phone` VARCHAR(20) DEFAULT NULL, `phone_provider` ENUM('mpesa','selcom','airtel','tigo','halopesa','bank','other') NOT NULL DEFAULT 'mpesa', `payment_channel` ENUM('snippe','tenant_direct_manual') NOT NULL DEFAULT 'snippe', `verification_source` ENUM('webhook','tenant_confirmed') DEFAULT NULL, `counts_toward_balance` TINYINT(1) NOT NULL DEFAULT 1, `confirmed_by_client_id` INT UNSIGNED DEFAULT NULL, `confirmed_at` TIMESTAMP NULL DEFAULT NULL, `notes` VARCHAR(255) DEFAULT NULL, `gross_amount` DECIMAL(10,2) NOT NULL, `snippe_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `commission_rate` DECIMAL(5,4) NOT NULL DEFAULT 0.0500, `commission_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `client_credit` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `status` ENUM('pending','completed','failed','refunded','voided') NOT NULL DEFAULT 'pending', `snippe_payment_id` VARCHAR(100) DEFAULT NULL, `access_token_code` VARCHAR(20) DEFAULT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `completed_at` TIMESTAMP NULL DEFAULT NULL, UNIQUE KEY `uk_reference` (`reference`), KEY `idx_pay_client` (`client_id`), KEY `idx_pay_device` (`device_id`), KEY `idx_pay_status` (`status`), KEY `idx_pay_completed` (`completed_at`), CONSTRAINT `fk_pay_client` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`), CONSTRAINT `fk_pay_device` FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`), CONSTRAINT `fk_pay_plan` FOREIGN KEY (`plan_id`) REFERENCES `wifi_plans`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 8: access_tokens -- ============================================= DROP TABLE IF EXISTS `access_tokens`; CREATE TABLE `access_tokens` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `client_id` INT UNSIGNED NOT NULL, `device_id` INT UNSIGNED NOT NULL, `plan_id` INT UNSIGNED NOT NULL, `payment_id` BIGINT UNSIGNED DEFAULT NULL, `code` VARCHAR(20) NOT NULL, `duration_seconds` INT UNSIGNED NOT NULL, `down_limit` SMALLINT UNSIGNED NOT NULL DEFAULT 2, `down_unit` TINYINT UNSIGNED NOT NULL DEFAULT 2, `up_limit` SMALLINT UNSIGNED NOT NULL DEFAULT 2, `up_unit` TINYINT UNSIGNED NOT NULL DEFAULT 2, `status` ENUM('unused','active','expired','revoked') NOT NULL DEFAULT 'unused', `locked_mac` VARCHAR(17) DEFAULT NULL, `activated_at` TIMESTAMP NULL DEFAULT NULL, `expires_at` TIMESTAMP NULL DEFAULT NULL, `total_seconds_used` INT UNSIGNED NOT NULL DEFAULT 0, `total_bytes_down` BIGINT UNSIGNED NOT NULL DEFAULT 0, `total_bytes_up` BIGINT UNSIGNED NOT NULL DEFAULT 0, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_token_code` (`code`), KEY `idx_token_client` (`client_id`), KEY `idx_token_device` (`device_id`), KEY `idx_token_status` (`status`), KEY `idx_token_expires` (`status`, `expires_at`), KEY `idx_token_payment` (`payment_id`), CONSTRAINT `fk_token_client` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`), CONSTRAINT `fk_token_device` FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`), CONSTRAINT `fk_token_plan` FOREIGN KEY (`plan_id`) REFERENCES `wifi_plans`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 9: sessions -- ============================================= DROP TABLE IF EXISTS `sessions`; CREATE TABLE `sessions` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `access_token_id` BIGINT UNSIGNED DEFAULT NULL, `client_id` INT UNSIGNED NOT NULL, `device_id` INT UNSIGNED DEFAULT NULL, `client_mac` VARCHAR(17) NOT NULL, `client_ip` VARCHAR(45) DEFAULT NULL, `client_name` VARCHAR(100) DEFAULT NULL, `ssid` VARCHAR(64) DEFAULT NULL, `started_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `last_seen_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `ended_at` TIMESTAMP NULL DEFAULT NULL, `duration_seconds` INT UNSIGNED NOT NULL DEFAULT 0, `bytes_down` BIGINT UNSIGNED NOT NULL DEFAULT 0, `bytes_up` BIGINT UNSIGNED NOT NULL DEFAULT 0, `rssi` SMALLINT DEFAULT NULL, `signal_level` TINYINT UNSIGNED DEFAULT NULL, `is_active` TINYINT(1) NOT NULL DEFAULT 1, KEY `idx_sess_token` (`access_token_id`), KEY `idx_sess_client` (`client_id`), KEY `idx_sess_device` (`device_id`), KEY `idx_sess_mac` (`client_mac`), KEY `idx_sess_active` (`is_active`, `client_id`), KEY `idx_sess_time` (`started_at`), CONSTRAINT `fk_sess_token` FOREIGN KEY (`access_token_id`) REFERENCES `access_tokens`(`id`), CONSTRAINT `fk_sess_client` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`), CONSTRAINT `fk_sess_device` FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 10: token_deauth_events -- ============================================= DROP TABLE IF EXISTS `token_deauth_events`; CREATE TABLE `token_deauth_events` ( `token_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY, `client_id` INT UNSIGNED NOT NULL, `device_id` INT UNSIGNED NOT NULL, `omada_site_id` VARCHAR(64) NOT NULL, `locked_mac` VARCHAR(17) NOT NULL, `status` ENUM('succeeded') NOT NULL DEFAULT 'succeeded', `succeeded_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY `idx_token_deauth_mac` (`device_id`, `locked_mac`), KEY `idx_token_deauth_site` (`omada_site_id`), CONSTRAINT `fk_token_deauth_token` FOREIGN KEY (`token_id`) REFERENCES `access_tokens`(`id`) ON DELETE CASCADE, CONSTRAINT `fk_token_deauth_client` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON DELETE CASCADE, CONSTRAINT `fk_token_deauth_device` FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 11: payouts -- ============================================= DROP TABLE IF EXISTS `payouts`; CREATE TABLE `payouts` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `client_id` INT UNSIGNED NOT NULL, `amount` DECIMAL(10,2) NOT NULL, `payout_fee` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `net_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `destination_phone` VARCHAR(20) NOT NULL, `destination_provider` ENUM('mpesa','airtel','tigo','halopesa') NOT NULL DEFAULT 'mpesa', `status` ENUM('pending','processing','completed','failed') NOT NULL DEFAULT 'pending', `snippe_disbursement_id` VARCHAR(100) DEFAULT NULL, `failure_reason` VARCHAR(255) DEFAULT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `completed_at` TIMESTAMP NULL DEFAULT NULL, KEY `idx_payout_client` (`client_id`), KEY `idx_payout_status` (`status`), CONSTRAINT `fk_payout_client` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 12: balance_ledger -- ============================================= DROP TABLE IF EXISTS `balance_ledger`; CREATE TABLE `balance_ledger` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `client_id` INT UNSIGNED NOT NULL, `type` ENUM('wifi_sale','payout','payout_reversal','adjustment','refund') NOT NULL, `amount` DECIMAL(10,2) NOT NULL, `balance_after` DECIMAL(12,2) NOT NULL, `reference_type` ENUM('payment','payout','manual') NOT NULL, `reference_id` BIGINT UNSIGNED NOT NULL, `description` VARCHAR(255) DEFAULT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY `idx_ledger_client` (`client_id`), KEY `idx_ledger_time` (`created_at`), KEY `idx_ledger_ref` (`reference_type`, `reference_id`), CONSTRAINT `fk_ledger_client` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 13: omada_alerts -- ============================================= DROP TABLE IF EXISTS `omada_alerts`; CREATE TABLE `omada_alerts` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `client_id` INT UNSIGNED NOT NULL, `device_id` INT UNSIGNED DEFAULT NULL, `omada_alert_id` VARCHAR(64) NOT NULL, `alert_key` VARCHAR(50) NOT NULL, `module` VARCHAR(50) DEFAULT NULL, `level` ENUM('info','warning','error','critical') NOT NULL DEFAULT 'info', `content` TEXT NOT NULL, `device_mac` VARCHAR(17) DEFAULT NULL, `resolved` TINYINT(1) NOT NULL DEFAULT 0, `alert_time` TIMESTAMP NOT NULL, `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_omada_alert` (`omada_alert_id`), KEY `idx_alert_client` (`client_id`), KEY `idx_alert_device` (`device_id`), KEY `idx_alert_unresolved` (`client_id`, `resolved`), CONSTRAINT `fk_alert_client` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`), CONSTRAINT `fk_alert_device` FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 13: sms_log -- ============================================= DROP TABLE IF EXISTS `sms_log`; CREATE TABLE `sms_log` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `phone` VARCHAR(20) NOT NULL, `message` VARCHAR(320) NOT NULL, `status` ENUM('sent','delivered','failed') NOT NULL, `gateway_id` VARCHAR(100) DEFAULT NULL, `error` VARCHAR(255) DEFAULT NULL, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `delivered_at` TIMESTAMP NULL DEFAULT NULL, KEY `idx_sms_phone` (`phone`), KEY `idx_sms_status` (`status`), KEY `idx_sms_time` (`created_at`), KEY `idx_sms_gateway` (`gateway_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TRIGGERS -- ============================================= DROP TRIGGER IF EXISTS `trg_payment_completed`; DELIMITER $$ CREATE TRIGGER `trg_payment_completed` BEFORE UPDATE ON `payments` FOR EACH ROW BEGIN DECLARE v_new_balance DECIMAL(12,2); IF OLD.status = 'pending' AND NEW.status = 'completed' THEN SET NEW.snippe_fee = ROUND(NEW.gross_amount * 0.005, 2); SET NEW.commission_amount = ROUND(NEW.gross_amount * NEW.commission_rate, 2); SET NEW.client_credit = NEW.gross_amount - NEW.snippe_fee - NEW.commission_amount; SET NEW.completed_at = CURRENT_TIMESTAMP; UPDATE `clients` SET `balance` = `balance` + NEW.client_credit, `total_earned` = `total_earned` + NEW.client_credit WHERE `id` = NEW.client_id; SET v_new_balance = (SELECT `balance` FROM `clients` WHERE `id` = NEW.client_id); INSERT INTO `balance_ledger` (`client_id`, `type`, `amount`, `balance_after`, `reference_type`, `reference_id`, `description`) VALUES (NEW.client_id, 'wifi_sale', NEW.client_credit, v_new_balance, 'payment', NEW.id, CONCAT('WiFi sale: ', NEW.gross_amount, ' TZS (fee: ', NEW.snippe_fee, ', comm: ', NEW.commission_amount, ')')); END IF; END$$ DELIMITER ; DROP TRIGGER IF EXISTS `trg_payout_requested`; DELIMITER $$ CREATE TRIGGER `trg_payout_requested` AFTER INSERT ON `payouts` FOR EACH ROW BEGIN DECLARE v_new_balance DECIMAL(12,2); IF NEW.status = 'pending' THEN UPDATE `clients` SET `balance` = `balance` - NEW.amount WHERE `id` = NEW.client_id; SET v_new_balance = (SELECT `balance` FROM `clients` WHERE `id` = NEW.client_id); INSERT INTO `balance_ledger` (`client_id`, `type`, `amount`, `balance_after`, `reference_type`, `reference_id`, `description`) VALUES (NEW.client_id, 'payout', -NEW.amount, v_new_balance, 'payout', NEW.id, CONCAT('Payout request: ', NEW.amount, ' TZS to ', NEW.destination_phone)); END IF; END$$ DELIMITER ; DROP TRIGGER IF EXISTS `trg_payout_failed`; DELIMITER $$ CREATE TRIGGER `trg_payout_failed` BEFORE UPDATE ON `payouts` FOR EACH ROW BEGIN DECLARE v_new_balance DECIMAL(12,2); IF OLD.status = 'pending' AND NEW.status = 'failed' THEN UPDATE `clients` SET `balance` = `balance` + OLD.amount WHERE `id` = OLD.client_id; SET v_new_balance = (SELECT `balance` FROM `clients` WHERE `id` = OLD.client_id); INSERT INTO `balance_ledger` (`client_id`, `type`, `amount`, `balance_after`, `reference_type`, `reference_id`, `description`) VALUES (OLD.client_id, 'payout_reversal', OLD.amount, v_new_balance, 'payout', OLD.id, CONCAT('Payout failed: ', IFNULL(NEW.failure_reason, 'unknown'))); END IF; END$$ DELIMITER ; DROP TRIGGER IF EXISTS `trg_payout_completed`; DELIMITER $$ CREATE TRIGGER `trg_payout_completed` BEFORE UPDATE ON `payouts` FOR EACH ROW BEGIN IF OLD.status IN ('pending','processing') AND NEW.status = 'completed' THEN SET NEW.completed_at = CURRENT_TIMESTAMP; UPDATE `clients` SET `total_withdrawn` = `total_withdrawn` + OLD.amount WHERE `id` = OLD.client_id; END IF; END$$ DELIMITER ; -- ============================================= -- TABLE 14: device_portal_settings -- ============================================= DROP TABLE IF EXISTS `device_portal_settings`; CREATE TABLE `device_portal_settings` ( `device_id` INT UNSIGNED NOT NULL PRIMARY KEY, `display_name` VARCHAR(100) DEFAULT NULL COMMENT 'Overrides business_name on the portal', `welcome_text` VARCHAR(255) DEFAULT NULL COMMENT 'Tagline shown under the name', `logo_url` VARCHAR(500) DEFAULT NULL COMMENT 'Full URL to logo image', `support_phone` VARCHAR(20) DEFAULT NULL COMMENT 'Support number shown to guests', `primary_color` VARCHAR(7) NOT NULL DEFAULT '#4361ee' COMMENT 'Buttons and accents', `bg_color` VARCHAR(7) NOT NULL DEFAULT '#1a1a2e' COMMENT 'Page background', `manual_sales_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Enable direct/manual sales for this hotspot', `portal_payment_mode` VARCHAR(24) NOT NULL DEFAULT 'both' COMMENT 'online_only | direct_only | both', `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT `fk_portal_settings_device` FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; DROP TABLE IF EXISTS `device_portal_payment_options`; CREATE TABLE `device_portal_payment_options` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `device_id` INT UNSIGNED NOT NULL, `provider_name` VARCHAR(100) NOT NULL, `image_path` VARCHAR(255) NOT NULL, `display_order` INT NOT NULL DEFAULT 0, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY `idx_portal_payment_device` (`device_id`), CONSTRAINT `fk_portal_payment_option_device` FOREIGN KEY (`device_id`) REFERENCES `devices`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 15: password_resets -- ============================================= DROP TABLE IF EXISTS `password_resets`; CREATE TABLE `password_resets` ( `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `phone` VARCHAR(20) NOT NULL, `otp` VARCHAR(6) NOT NULL, `expires_at` TIMESTAMP NOT NULL, `used` TINYINT(1) NOT NULL DEFAULT 0, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY `idx_reset_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ============================================= -- TABLE 15: system_config -- ============================================= DROP TABLE IF EXISTS `system_config`; CREATE TABLE `system_config` ( `key` VARCHAR(64) NOT NULL PRIMARY KEY, `value` VARCHAR(255) NOT NULL, `description` VARCHAR(255) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO `system_config` (`key`, `value`, `description`) VALUES ('admin_phone', '+255769590766', 'System admin phone — receives critical error alerts'); SET FOREIGN_KEY_CHECKS = 1;