diff --git a/modules/billing/admin.php b/modules/billing/admin.php
index b08e4690..61f9444a 100644
--- a/modules/billing/admin.php
+++ b/modules/billing/admin.php
@@ -24,6 +24,7 @@ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
diff --git a/modules/billing/admin_coupons.php b/modules/billing/admin_coupons.php
new file mode 100644
index 00000000..80bcd63c
--- /dev/null
+++ b/modules/billing/admin_coupons.php
@@ -0,0 +1,414 @@
+ 0) {
+ $error = "Coupon code '$code' already exists.";
+ } else {
+ $sql = "INSERT INTO {$table_prefix}billing_coupons
+ (code, name, description, discount_percent, usage_type, game_filter_type, game_filter_list, max_uses, expires, is_active)
+ VALUES ('$code', '$name', '$description', $discount_percent, '$usage_type', '$game_filter_type', " .
+ ($game_filter_list === 'NULL' ? 'NULL' : "'$game_filter_list'") . ", $max_uses, $expires, 1)";
+
+ if (mysqli_query($db, $sql)) {
+ $status = "Coupon '$code' added successfully.";
+ } else {
+ $error = "Error adding coupon: " . mysqli_error($db);
+ }
+ }
+ }
+
+ // Update existing coupon
+ elseif (isset($_POST['update_coupon'])) {
+ $coupon_id = intval($_POST['coupon_id']);
+ $code = mysqli_real_escape_string($db, trim($_POST['code']));
+ $name = mysqli_real_escape_string($db, trim($_POST['name']));
+ $description = mysqli_real_escape_string($db, trim($_POST['description']));
+ $discount_percent = floatval($_POST['discount_percent']);
+ $usage_type = mysqli_real_escape_string($db, $_POST['usage_type']);
+ $game_filter_type = mysqli_real_escape_string($db, $_POST['game_filter_type']);
+ $game_filter_list = isset($_POST['game_filter_list']) && $_POST['game_filter_type'] === 'specific_games'
+ ? mysqli_real_escape_string($db, json_encode($_POST['game_filter_list']))
+ : 'NULL';
+ $max_uses = !empty($_POST['max_uses']) ? intval($_POST['max_uses']) : 'NULL';
+ $expires = !empty($_POST['expires']) ? "'" . mysqli_real_escape_string($db, $_POST['expires']) . "'" : 'NULL';
+ $is_active = isset($_POST['is_active']) ? 1 : 0;
+
+ $sql = "UPDATE {$table_prefix}billing_coupons SET
+ code = '$code',
+ name = '$name',
+ description = '$description',
+ discount_percent = $discount_percent,
+ usage_type = '$usage_type',
+ game_filter_type = '$game_filter_type',
+ game_filter_list = " . ($game_filter_list === 'NULL' ? 'NULL' : "'$game_filter_list'") . ",
+ max_uses = $max_uses,
+ expires = $expires,
+ is_active = $is_active
+ WHERE coupon_id = $coupon_id";
+
+ if (mysqli_query($db, $sql)) {
+ $status = "Coupon updated successfully.";
+ } else {
+ $error = "Error updating coupon: " . mysqli_error($db);
+ }
+ }
+
+ // Delete coupon
+ elseif (isset($_POST['delete_coupon'])) {
+ $coupon_id = intval($_POST['coupon_id']);
+ if (mysqli_query($db, "DELETE FROM {$table_prefix}billing_coupons WHERE coupon_id = $coupon_id")) {
+ $status = "Coupon deleted successfully.";
+ } else {
+ $error = "Error deleting coupon: " . mysqli_error($db);
+ }
+ }
+ }
+}
+
+// Get all available games from server configs
+$game_options = [];
+$games_dir = __DIR__ . '/../../config_games/server_configs/';
+if (is_dir($games_dir)) {
+ $files = scandir($games_dir);
+ foreach ($files as $file) {
+ if (pathinfo($file, PATHINFO_EXTENSION) === 'xml' && strpos($file, '.bak') === false) {
+ $game_key = str_replace('.xml', '', $file);
+ $game_options[] = $game_key;
+ }
+ }
+ sort($game_options);
+}
+
+// Get all coupons
+$coupons_result = mysqli_query($db, "SELECT * FROM {$table_prefix}billing_coupons ORDER BY created_date DESC");
+?>
+
+
+
+
+
+ Admin — Coupon Management
+
+
+
+
+
+
+
+
Coupon Management
+
+
+
+
+
+
+
+
+
+
+
Add New Coupon
+
+
+
+
Existing Coupons
+
+ 0): ?>
+
+
+
No coupons found. Add your first coupon above.
+
+
+
+
+
+
+
+
diff --git a/modules/billing/create_coupons_table.sql b/modules/billing/create_coupons_table.sql
new file mode 100644
index 00000000..07c9093f
--- /dev/null
+++ b/modules/billing/create_coupons_table.sql
@@ -0,0 +1,106 @@
+-- Enhanced coupon system for billing module
+-- This creates a flexible coupon system with game filters and usage tracking
+
+-- Drop existing table if upgrading from old coupon module
+DROP TABLE IF EXISTS `ogp_billing_coupons`;
+
+-- Create enhanced coupons table
+CREATE TABLE `ogp_billing_coupons` (
+ `coupon_id` INT(11) NOT NULL AUTO_INCREMENT,
+ `code` VARCHAR(50) NOT NULL UNIQUE,
+ `name` VARCHAR(255) NOT NULL DEFAULT '',
+ `description` TEXT,
+ `discount_percent` DECIMAL(5,2) NOT NULL DEFAULT 0.00,
+ `usage_type` ENUM('one_time', 'permanent') NOT NULL DEFAULT 'one_time',
+ `game_filter_type` ENUM('all_games', 'specific_games') NOT NULL DEFAULT 'all_games',
+ `game_filter_list` TEXT COMMENT 'JSON array of game keys when game_filter_type=specific_games',
+ `max_uses` INT(11) DEFAULT NULL COMMENT 'NULL for unlimited uses',
+ `current_uses` INT(11) NOT NULL DEFAULT 0,
+ `expires` DATETIME DEFAULT NULL,
+ `created_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `created_by` INT(11) DEFAULT NULL,
+ `is_active` TINYINT(1) NOT NULL DEFAULT 1,
+ PRIMARY KEY (`coupon_id`),
+ UNIQUE KEY `idx_code` (`code`),
+ KEY `idx_active_expires` (`is_active`, `expires`),
+ KEY `idx_created_by` (`created_by`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
+
+-- Add coupon_id field to billing_orders if it doesn't exist
+SET @tablename = 'ogp_billing_orders';
+SET @checkIfColumnExists = (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = @tablename
+ AND COLUMN_NAME = 'coupon_id'
+);
+
+SET @addColumn = IF(@checkIfColumnExists = 0,
+ 'ALTER TABLE `ogp_billing_orders` ADD COLUMN `coupon_id` INT(11) DEFAULT NULL AFTER `user_id`, ADD KEY `idx_coupon` (`coupon_id`)',
+ 'SELECT "Column coupon_id already exists in ogp_billing_orders"'
+);
+
+PREPARE stmt FROM @addColumn;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- Add coupon_id field to billing_invoices if it doesn't exist
+SET @tablename = 'ogp_billing_invoices';
+SET @checkIfColumnExists = (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = @tablename
+ AND COLUMN_NAME = 'coupon_id'
+);
+
+SET @addColumn = IF(@checkIfColumnExists = 0,
+ 'ALTER TABLE `ogp_billing_invoices` ADD COLUMN `coupon_id` INT(11) DEFAULT NULL AFTER `user_id`, ADD KEY `idx_coupon` (`coupon_id`)',
+ 'SELECT "Column coupon_id already exists in ogp_billing_invoices"'
+);
+
+PREPARE stmt FROM @addColumn;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- Add discount_amount field to billing_invoices to track actual discount applied
+SET @checkIfColumnExists = (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'ogp_billing_invoices'
+ AND COLUMN_NAME = 'discount_amount'
+);
+
+SET @addColumn = IF(@checkIfColumnExists = 0,
+ 'ALTER TABLE `ogp_billing_invoices` ADD COLUMN `discount_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 AFTER `amount`',
+ 'SELECT "Column discount_amount already exists in ogp_billing_invoices"'
+);
+
+PREPARE stmt FROM @addColumn;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- Add discount_amount field to billing_orders to track permanent discounts
+SET @checkIfColumnExists = (
+ SELECT COUNT(*)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'ogp_billing_orders'
+ AND COLUMN_NAME = 'discount_amount'
+);
+
+SET @addColumn = IF(@checkIfColumnExists = 0,
+ 'ALTER TABLE `ogp_billing_orders` ADD COLUMN `discount_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 AFTER `price`',
+ 'SELECT "Column discount_amount already exists in ogp_billing_orders"'
+);
+
+PREPARE stmt FROM @addColumn;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- Sample coupons for testing
+INSERT INTO `ogp_billing_coupons` (`code`, `name`, `description`, `discount_percent`, `usage_type`, `game_filter_type`, `game_filter_list`, `expires`) VALUES
+('WELCOME10', 'Welcome 10% Off', 'New customer welcome discount - 10% off any game', 10.00, 'one_time', 'all_games', NULL, DATE_ADD(NOW(), INTERVAL 1 YEAR)),
+('ARMA25', 'Arma Series 25% Off', 'Save 25% on any Arma game server', 25.00, 'permanent', 'specific_games', '["arma2_win32", "arma2oa_win32", "arma3_linux32", "arma3_linux64", "arma3_win64", "arma-reforger_linux64", "arma-reforger_win64"]', NULL);